React translations

This commit is contained in:
Reece
2025-05-29 17:26:32 +01:00
parent 5f862f55d9
commit 09f05ac8d0
106 changed files with 128926 additions and 196 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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 }]) => (

View File

@@ -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>