mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
React translations
This commit is contained in:
File diff suppressed because one or more lines are too long
+99
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
-99
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -13,8 +13,8 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-DAILjWej.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DxdJ27yt.css">
|
||||
<script type="module" crossorigin src="/assets/index-C7ZkpjP3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CEZrmp6h.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1620
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
+1561
File diff suppressed because it is too large
Load Diff
Generated
+102
-7
@@ -22,9 +22,13 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.9.0",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"web-vitals": "^2.1.4"
|
||||
@@ -2693,6 +2697,14 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
@@ -3353,6 +3365,14 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
@@ -3367,6 +3387,52 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz",
|
||||
"integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
|
||||
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
|
||||
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -4036,7 +4102,6 @@
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -4056,22 +4121,19 @@
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -4711,6 +4773,31 @@
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz",
|
||||
"integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
|
||||
@@ -5430,7 +5517,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -5678,6 +5765,14 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.9.0",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex, ThemeIcon } from "@mantine/core";
|
||||
import { Dropzone, MIME_TYPES } from "@mantine/dropzone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
|
||||
import { GlobalWorkerOptions } from "pdfjs-dist";
|
||||
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
|
||||
GlobalWorkerOptions.workerSrc = "/pdf.worker.js";
|
||||
|
||||
export interface FileWithUrl extends File {
|
||||
@@ -63,6 +64,7 @@ interface FileCardProps {
|
||||
}
|
||||
|
||||
function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const thumb = usePdfThumbnail(file);
|
||||
|
||||
return (
|
||||
@@ -120,7 +122,7 @@ function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) {
|
||||
onClick={onRemove}
|
||||
mt={4}
|
||||
>
|
||||
Remove
|
||||
{t("delete", "Remove")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -142,6 +144,7 @@ const FileManager: React.FC<FileManagerProps> = ({
|
||||
setPdfFile,
|
||||
setCurrentView,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const handleDrop = (uploadedFiles: File[]) => {
|
||||
setFiles((prevFiles) => (allowMultiple ? [...prevFiles, ...uploadedFiles] : uploadedFiles));
|
||||
};
|
||||
@@ -171,13 +174,13 @@ const FileManager: React.FC<FileManagerProps> = ({
|
||||
>
|
||||
<Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>
|
||||
<Text size="md">
|
||||
Drag PDF files here or click to select
|
||||
{t("fileChooser.dragAndDropPDF", "Drag PDF files here or click to select")}
|
||||
</Text>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
{files.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">
|
||||
No files uploaded yet.
|
||||
{t("noFileSelected", "No files uploaded yet.")}
|
||||
</Text>
|
||||
) : (
|
||||
<Box>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* Language selector grid responsive layout */
|
||||
.languageGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.languageItem {
|
||||
border-right: 2px solid var(--mantine-color-gray-3);
|
||||
}
|
||||
|
||||
.languageItem:nth-child(4n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Responsive breakpoints */
|
||||
@media (max-width: 600px) {
|
||||
.languageGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.languageItem:nth-child(4n) {
|
||||
border-right: 2px solid var(--mantine-color-gray-3);
|
||||
}
|
||||
|
||||
.languageItem:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 601px) and (max-width: 900px) {
|
||||
.languageGrid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.languageItem:nth-child(4n) {
|
||||
border-right: 2px solid var(--mantine-color-gray-3);
|
||||
}
|
||||
|
||||
.languageItem:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-mantine-color-scheme="dark"] .languageItem {
|
||||
border-right-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(4n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(2n) {
|
||||
border-right-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(3n) {
|
||||
border-right-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
|
||||
/* Responsive text visibility */
|
||||
.languageText {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.languageText {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Menu, Button, ScrollArea, useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import styles from './LanguageSelector.module.css';
|
||||
|
||||
const LanguageSelector: React.FC = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const languageOptions = Object.entries(supportedLanguages)
|
||||
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
||||
.map(([code, name]) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
}));
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] ||
|
||||
supportedLanguages['en-GB'];
|
||||
|
||||
return (
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
width={600}
|
||||
position="bottom-start"
|
||||
offset={8}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<LanguageIcon style={{ fontSize: 18 }} />}
|
||||
styles={{
|
||||
root: {
|
||||
border: 'none',
|
||||
color: colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7],
|
||||
'&:hover': {
|
||||
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className={styles.languageText}>
|
||||
{currentLanguage}
|
||||
</span>
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown
|
||||
style={{
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
border: colorScheme === 'dark' ? `1px solid ${theme.colors.dark[4]}` : `1px solid ${theme.colors.gray[3]}`,
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={190} type="scroll">
|
||||
<div className={styles.languageGrid}>
|
||||
{languageOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={styles.languageItem}
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
fullWidth
|
||||
onClick={() => handleLanguageChange(option.value)}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: '4px',
|
||||
minHeight: '32px',
|
||||
padding: '4px 8px',
|
||||
justifyContent: 'flex-start',
|
||||
backgroundColor: option.value === i18n.language ? (
|
||||
colorScheme === 'dark' ? theme.colors.blue[8] : theme.colors.blue[1]
|
||||
) : 'transparent',
|
||||
color: option.value === i18n.language ? (
|
||||
colorScheme === 'dark' ? theme.colors.blue[2] : theme.colors.blue[7]
|
||||
) : (
|
||||
colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7]
|
||||
),
|
||||
'&:hover': {
|
||||
backgroundColor: option.value === i18n.language ? (
|
||||
colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2]
|
||||
) : (
|
||||
colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
|
||||
),
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: '13px',
|
||||
fontWeight: option.value === i18n.language ? 600 : 400,
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import {
|
||||
Paper, Button, Group, Text, Stack, Center, Checkbox, ScrollArea, Box, Tooltip, ActionIcon, Notification
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import UndoIcon from "@mui/icons-material/Undo";
|
||||
import RedoIcon from "@mui/icons-material/Redo";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
@@ -28,6 +29,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
downloadUrl,
|
||||
setDownloadUrl,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPages, setSelectedPages] = useState<number[]>([]);
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -61,20 +63,20 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
};
|
||||
|
||||
// Example action handlers (replace with real API calls)
|
||||
const handleRotateLeft = () => setStatus("Rotated left: " + selectedPages.join(", "));
|
||||
const handleRotateRight = () => setStatus("Rotated right: " + selectedPages.join(", "));
|
||||
const handleDelete = () => setStatus("Deleted: " + selectedPages.join(", "));
|
||||
const handleMoveLeft = () => setStatus("Moved left: " + selectedPages.join(", "));
|
||||
const handleMoveRight = () => setStatus("Moved right: " + selectedPages.join(", "));
|
||||
const handleSplit = () => setStatus("Split at: " + selectedPages.join(", "));
|
||||
const handleInsertPageBreak = () => setStatus("Inserted page break at: " + selectedPages.join(", "));
|
||||
const handleAddFile = () => setStatus("Add file not implemented in demo");
|
||||
const handleRotateLeft = () => setStatus(t("pageEditor.rotatedLeft", "Rotated left: ") + selectedPages.join(", "));
|
||||
const handleRotateRight = () => setStatus(t("pageEditor.rotatedRight", "Rotated right: ") + selectedPages.join(", "));
|
||||
const handleDelete = () => setStatus(t("pageEditor.deleted", "Deleted: ") + selectedPages.join(", "));
|
||||
const handleMoveLeft = () => setStatus(t("pageEditor.movedLeft", "Moved left: ") + selectedPages.join(", "));
|
||||
const handleMoveRight = () => setStatus(t("pageEditor.movedRight", "Moved right: ") + selectedPages.join(", "));
|
||||
const handleSplit = () => setStatus(t("pageEditor.splitAt", "Split at: ") + selectedPages.join(", "));
|
||||
const handleInsertPageBreak = () => setStatus(t("pageEditor.insertedPageBreak", "Inserted page break at: ") + selectedPages.join(", "));
|
||||
const handleAddFile = () => setStatus(t("pageEditor.addFileNotImplemented", "Add file not implemented in demo"));
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<Paper shadow="xs" radius="md" p="md">
|
||||
<Center>
|
||||
<Text color="dimmed">No PDF loaded. Please upload a PDF to edit.</Text>
|
||||
<Text color="dimmed">{t("pageEditor.noPdfLoaded", "No PDF loaded. Please upload a PDF to edit.")}</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
);
|
||||
@@ -85,14 +87,14 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
<Group align="flex-start" gap="lg">
|
||||
{/* Sidebar */}
|
||||
<Stack w={180} gap="xs">
|
||||
<Text fw={600} size="lg">PDF Multitool</Text>
|
||||
<Button onClick={selectAll} fullWidth variant="light">Select All</Button>
|
||||
<Button onClick={deselectAll} fullWidth variant="light">Deselect All</Button>
|
||||
<Button onClick={handleUndo} leftSection={<UndoIcon fontSize="small" />} fullWidth disabled={undoStack.length === 0}>Undo</Button>
|
||||
<Button onClick={handleRedo} leftSection={<RedoIcon fontSize="small" />} fullWidth disabled={redoStack.length === 0}>Redo</Button>
|
||||
<Button onClick={handleAddFile} leftSection={<AddIcon fontSize="small" />} fullWidth>Add File</Button>
|
||||
<Button onClick={handleInsertPageBreak} leftSection={<ContentCutIcon fontSize="small" />} fullWidth>Insert Page Break</Button>
|
||||
<Button onClick={handleSplit} leftSection={<ContentCutIcon fontSize="small" />} fullWidth>Split</Button>
|
||||
<Text fw={600} size="lg">{t("pageEditor.title", "PDF Multitool")}</Text>
|
||||
<Button onClick={selectAll} fullWidth variant="light">{t("multiTool.selectAll", "Select All")}</Button>
|
||||
<Button onClick={deselectAll} fullWidth variant="light">{t("multiTool.deselectAll", "Deselect All")}</Button>
|
||||
<Button onClick={handleUndo} leftSection={<UndoIcon fontSize="small" />} fullWidth disabled={undoStack.length === 0}>{t("multiTool.undo", "Undo")}</Button>
|
||||
<Button onClick={handleRedo} leftSection={<RedoIcon fontSize="small" />} fullWidth disabled={redoStack.length === 0}>{t("multiTool.redo", "Redo")}</Button>
|
||||
<Button onClick={handleAddFile} leftSection={<AddIcon fontSize="small" />} fullWidth>{t("multiTool.addFile", "Add File")}</Button>
|
||||
<Button onClick={handleInsertPageBreak} leftSection={<ContentCutIcon fontSize="small" />} fullWidth>{t("multiTool.insertPageBreak", "Insert Page Break")}</Button>
|
||||
<Button onClick={handleSplit} leftSection={<ContentCutIcon fontSize="small" />} fullWidth>{t("multiTool.split", "Split")}</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl || "#"}
|
||||
@@ -103,7 +105,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
variant="light"
|
||||
disabled={!downloadUrl}
|
||||
>
|
||||
Download All
|
||||
{t("multiTool.downloadAll", "Download All")}
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
@@ -115,7 +117,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
variant="light"
|
||||
disabled={!downloadUrl || selectedPages.length === 0}
|
||||
>
|
||||
Download Selected
|
||||
{t("multiTool.downloadSelected", "Download Selected")}
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
@@ -123,34 +125,34 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
onClick={() => setFile && setFile(null)}
|
||||
fullWidth
|
||||
>
|
||||
Close PDF
|
||||
{t("pageEditor.closePdf", "Close PDF")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Main multitool area */}
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Group mb="sm">
|
||||
<Tooltip label="Rotate Left">
|
||||
<Tooltip label={t("multiTool.rotateLeft", "Rotate Left")}>
|
||||
<ActionIcon onClick={handleRotateLeft} disabled={selectedPages.length === 0} color="blue" variant="light">
|
||||
<RotateLeftIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Rotate Right">
|
||||
<Tooltip label={t("multiTool.rotateRight", "Rotate Right")}>
|
||||
<ActionIcon onClick={handleRotateRight} disabled={selectedPages.length === 0} color="blue" variant="light">
|
||||
<RotateRightIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete">
|
||||
<Tooltip label={t("delete", "Delete")}>
|
||||
<ActionIcon onClick={handleDelete} disabled={selectedPages.length === 0} color="red" variant="light">
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Move Left">
|
||||
<Tooltip label={t("multiTool.moveLeft", "Move Left")}>
|
||||
<ActionIcon onClick={handleMoveLeft} disabled={selectedPages.length === 0} color="gray" variant="light">
|
||||
<ArrowBackIosNewIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Move Right">
|
||||
<Tooltip label={t("multiTool.moveRight", "Move Right")}>
|
||||
<ActionIcon onClick={handleMoveRight} disabled={selectedPages.length === 0} color="gray" variant="light">
|
||||
<ArrowForwardIosIcon />
|
||||
</ActionIcon>
|
||||
@@ -163,7 +165,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
<Checkbox
|
||||
checked={selectedPages.includes(page)}
|
||||
onChange={() => togglePage(page)}
|
||||
label={`Page ${page}`}
|
||||
label={t("page", "Page") + ` ${page}`}
|
||||
/>
|
||||
<Box
|
||||
w={60}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Tool = {
|
||||
icon: React.ReactNode;
|
||||
@@ -17,6 +18,7 @@ interface ToolPickerProps {
|
||||
}
|
||||
|
||||
const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, toolRegistry }) => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredTools = Object.entries(toolRegistry).filter(([_, { name }]) =>
|
||||
@@ -26,7 +28,7 @@ const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, tool
|
||||
return (
|
||||
<Box >
|
||||
<TextInput
|
||||
placeholder="Search tools..."
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
mb="md"
|
||||
@@ -35,7 +37,7 @@ const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, tool
|
||||
<Stack align="flex-start">
|
||||
{filteredTools.length === 0 ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
No tools found
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
) : (
|
||||
filteredTools.map(([id, { icon, name }]) => (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme } from "@mantine/core";
|
||||
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||
@@ -25,6 +26,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
sidebarsVisible,
|
||||
setSidebarsVisible,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useMantineTheme();
|
||||
const [numPages, setNumPages] = useState<number>(0);
|
||||
const [pageImages, setPageImages] = useState<string[]>([]);
|
||||
@@ -176,13 +178,13 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
{!pdfFile ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Stack align="center">
|
||||
<Text c="dimmed">No PDF loaded. Click to upload a PDF.</Text>
|
||||
<Text c="dimmed">{t("viewer.noPdfLoaded", "No PDF loaded. Click to upload a PDF.")}</Text>
|
||||
<Button
|
||||
component="label"
|
||||
variant="outline"
|
||||
color="blue"
|
||||
>
|
||||
Choose PDF
|
||||
{t("viewer.choosePdf", "Choose PDF")}
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
@@ -209,7 +211,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
>
|
||||
<Stack gap="xl" align="center" >
|
||||
{pageImages.length === 0 && (
|
||||
<Text color="dimmed">No pages to display.</Text>
|
||||
<Text color="dimmed">{t("viewer.noPagesToDisplay", "No pages to display.")}</Text>
|
||||
)}
|
||||
{dualPage
|
||||
? Array.from({ length: Math.ceil(pageImages.length / 2) }).map((_, i) => (
|
||||
@@ -372,7 +374,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
radius="xl"
|
||||
onClick={() => setDualPage(v => !v)}
|
||||
style={{ minWidth: 36 }}
|
||||
title={dualPage ? "Single Page View" : "Dual Page View"}
|
||||
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
|
||||
>
|
||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
||||
</Button>
|
||||
@@ -383,7 +385,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
radius="xl"
|
||||
onClick={() => setSidebarsVisible(!sidebarsVisible)}
|
||||
style={{ minWidth: 36 }}
|
||||
title={sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
|
||||
title={sidebarsVisible ? t("viewer.hideSidebars", "Hide Sidebars") : t("viewer.showSidebars", "Show Sidebars")}
|
||||
>
|
||||
<ViewSidebarIcon
|
||||
fontSize="small"
|
||||
@@ -401,7 +403,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
radius="xl"
|
||||
onClick={() => setZoom(z => Math.max(0.1, z - 0.1))}
|
||||
style={{ minWidth: 32, padding: 0 }}
|
||||
title="Zoom out"
|
||||
title={t("viewer.zoomOut", "Zoom out")}
|
||||
>−</Button>
|
||||
<span style={{ minWidth: 40, textAlign: "center" }}>{Math.round(zoom * 100)}%</span>
|
||||
<Button
|
||||
@@ -411,7 +413,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
radius="xl"
|
||||
onClick={() => setZoom(z => Math.min(5, z + 0.1))}
|
||||
style={{ minWidth: 32, padding: 0 }}
|
||||
title="Zoom in"
|
||||
title={t("viewer.zoomIn", "Zoom in")}
|
||||
>+</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Re-export react-i18next hook with our custom types
|
||||
export { useTranslation } from 'react-i18next';
|
||||
|
||||
// You can add custom hooks here later if needed
|
||||
// For example, a hook that returns commonly used translations
|
||||
import { useTranslation as useI18nTranslation } from 'react-i18next';
|
||||
|
||||
export const useCommonTranslations = () => {
|
||||
const { t } = useI18nTranslation();
|
||||
|
||||
return {
|
||||
submit: t('genericSubmit'),
|
||||
selectPdf: t('pdfPrompt'),
|
||||
selectPdfs: t('multiPdfPrompt'),
|
||||
selectImages: t('imgPrompt'),
|
||||
loading: t('loading', 'Loading...'), // fallback if not found
|
||||
error: t('error._value', 'Error'),
|
||||
success: t('success', 'Success'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
// Define supported languages (based on your existing translations)
|
||||
export const supportedLanguages = {
|
||||
'en-GB': 'English (UK)',
|
||||
'en-US': 'English (US)',
|
||||
'ar-AR': 'العربية',
|
||||
'az-AZ': 'Azərbaycan Dili',
|
||||
'bg-BG': 'Български',
|
||||
'ca-CA': 'Català',
|
||||
'cs-CZ': 'Česky',
|
||||
'da-DK': 'Dansk',
|
||||
'de-DE': 'Deutsch',
|
||||
'el-GR': 'Ελληνικά',
|
||||
'es-ES': 'Español',
|
||||
'eu-ES': 'Euskara',
|
||||
'fa-IR': 'فارسی',
|
||||
'fr-FR': 'Français',
|
||||
'ga-IE': 'Gaeilge',
|
||||
'hi-IN': 'हिंदी',
|
||||
'hr-HR': 'Hrvatski',
|
||||
'hu-HU': 'Magyar',
|
||||
'id-ID': 'Bahasa Indonesia',
|
||||
'it-IT': 'Italiano',
|
||||
'ja-JP': '日本語',
|
||||
'ko-KR': '한국어',
|
||||
'ml-ML': 'മലയാളം',
|
||||
'nl-NL': 'Nederlands',
|
||||
'no-NB': 'Norsk',
|
||||
'pl-PL': 'Polski',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'pt-PT': 'Português',
|
||||
'ro-RO': 'Română',
|
||||
'ru-RU': 'Русский',
|
||||
'sk-SK': 'Slovensky',
|
||||
'sl-SI': 'Slovenščina',
|
||||
'sr-LATN-RS': 'Srpski',
|
||||
'sv-SE': 'Svenska',
|
||||
'th-TH': 'ไทย',
|
||||
'tr-TR': 'Türkçe',
|
||||
'uk-UA': 'Українська',
|
||||
'vi-VN': 'Tiếng Việt',
|
||||
'zh-BO': 'བོད་ཡིག',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '繁體中文',
|
||||
};
|
||||
|
||||
// RTL languages (based on your existing language.direction property)
|
||||
export const rtlLanguages = ['ar-AR', 'fa-IR'];
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en-GB',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: false, // Set to false to avoid suspense issues with SSR
|
||||
},
|
||||
});
|
||||
|
||||
// Set document direction based on language
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
const isRTL = rtlLanguages.includes(lng);
|
||||
document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
|
||||
document.documentElement.lang = lng;
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -5,6 +5,7 @@ import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/c
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import './i18n'; // Initialize i18next
|
||||
|
||||
|
||||
const container = document.getElementById('root');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user