mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
React translations
This commit is contained in:
@@ -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>
|
||||
|
||||
71
frontend/src/components/LanguageSelector.module.css
Normal file
71
frontend/src/components/LanguageSelector.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
125
frontend/src/components/LanguageSelector.tsx
Normal file
125
frontend/src/components/LanguageSelector.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
20
frontend/src/hooks/useTranslation.ts
Normal file
20
frontend/src/hooks/useTranslation.ts
Normal file
@@ -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'),
|
||||
};
|
||||
};
|
||||
87
frontend/src/i18n.ts
Normal file
87
frontend/src/i18n.ts
Normal file
@@ -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');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
@@ -15,6 +16,7 @@ import CompressPdfPanel from "../tools/Compress";
|
||||
import MergePdfPanel from "../tools/Merge";
|
||||
import PageEditor from "../components/PageEditor";
|
||||
import Viewer from "../components/Viewer";
|
||||
import LanguageSelector from "../components/LanguageSelector";
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
|
||||
@@ -29,10 +31,11 @@ type ToolRegistry = {
|
||||
[key: string]: ToolRegistryEntry;
|
||||
};
|
||||
|
||||
const toolRegistry: ToolRegistry = {
|
||||
split: { icon: <ContentCutIcon />, name: "Split PDF", component: SplitPdfPanel, view: "viewer" },
|
||||
compress: { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel, view: "viewer" },
|
||||
merge: { icon: <AddToPhotosIcon />, name: "Merge PDFs", component: MergePdfPanel, view: "fileManager" },
|
||||
// Base tool registry without translations
|
||||
const baseToolRegistry = {
|
||||
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" },
|
||||
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
|
||||
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
|
||||
};
|
||||
|
||||
const VIEW_OPTIONS = [
|
||||
@@ -152,10 +155,18 @@ const TOOL_PARAMS = {
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
|
||||
// Create translated tool registry
|
||||
const toolRegistry: ToolRegistry = {
|
||||
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
||||
compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") },
|
||||
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
|
||||
};
|
||||
|
||||
// Core app state
|
||||
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("tool") || "split");
|
||||
const [currentView, setCurrentView] = useState<string>(searchParams.get("view") || "viewer");
|
||||
@@ -286,6 +297,9 @@ export default function HomePage() {
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -296,6 +310,7 @@ export default function HomePage() {
|
||||
>
|
||||
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</Button>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -395,7 +410,7 @@ export default function HomePage() {
|
||||
style={{ position: "fixed", top: 16, right: 16, zIndex: 200 }}
|
||||
onClick={() => setSidebarsVisible((v) => !v)}
|
||||
>
|
||||
{sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
|
||||
{t("sidebar.toggle", sidebarsVisible ? "Hide Sidebars" : "Show Sidebars")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
||||
|
||||
export interface CompressProps {
|
||||
@@ -13,6 +14,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
setDownloadUrl,
|
||||
setLoading,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
|
||||
@@ -63,9 +65,9 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text fw={500} mb={4}>Select files to compress:</Text>
|
||||
<Text fw={500} mb={4}>{t("multiPdfDropPrompt", "Select files to compress:")}</Text>
|
||||
<Stack gap={4}>
|
||||
{files.length === 0 && <Text c="dimmed" size="sm">No files loaded.</Text>}
|
||||
{files.length === 0 && <Text c="dimmed" size="sm">{t("noFileSelected")}</Text>}
|
||||
{files.map((file, idx) => (
|
||||
<Checkbox
|
||||
key={file.name + idx}
|
||||
@@ -76,7 +78,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
))}
|
||||
</Stack>
|
||||
<Stack gap={4} mb={14}>
|
||||
<Text size="sm" style={{ minWidth: 140 }}>Compression Level</Text>
|
||||
<Text size="sm" style={{ minWidth: 140 }}>{t("compress.selectText.2", "Compression Level")}</Text>
|
||||
<Slider
|
||||
min={1}
|
||||
max={9}
|
||||
@@ -92,23 +94,23 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
/>
|
||||
</Stack>
|
||||
<Checkbox
|
||||
label="Convert images to grayscale"
|
||||
label={t("compress.grayscale.label", "Convert images to grayscale")}
|
||||
checked={grayscale}
|
||||
onChange={e => setGrayscale(e.currentTarget.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Remove PDF metadata"
|
||||
label={t("removeMetadata.submit", "Remove PDF metadata")}
|
||||
checked={removeMetadata}
|
||||
onChange={e => setRemoveMetadata(e.currentTarget.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Aggressive compression (may reduce quality)"
|
||||
label={t("compress.selectText.1.1", "Aggressive compression (may reduce quality)")}
|
||||
checked={aggressive}
|
||||
onChange={e => setAggressive(e.currentTarget.checked)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Expected output size (e.g. 2MB, 500KB)"
|
||||
placeholder="Optional"
|
||||
label={t("compress.selectText.5", "Expected output size")}
|
||||
placeholder={t("compress.selectText.5", "e.g. 25MB, 10.8MB, 25KB")}
|
||||
value={expectedSize}
|
||||
onChange={e => setExpectedSize(e.currentTarget.value)}
|
||||
/>
|
||||
@@ -119,7 +121,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
fullWidth
|
||||
mt="md"
|
||||
>
|
||||
Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
|
||||
{t("compress.submit", "Compress")} {t("pdfPrompt", "PDF")}{selected.filter(Boolean).length > 1 ? "s" : ""}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface MergePdfPanelProps {
|
||||
files: File[];
|
||||
@@ -18,6 +19,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
params,
|
||||
updateParams,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||
@@ -31,7 +33,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
const handleMerge = async () => {
|
||||
const filesToMerge = files.filter((_, index) => selectedFiles[index]);
|
||||
if (filesToMerge.length < 2) {
|
||||
setErrorMessage("Please select at least two PDFs to merge.");
|
||||
setErrorMessage(t("multiPdfPrompt")); // "Select PDFs (2+)"
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text fw={500} size="lg">Merge PDFs</Text>
|
||||
<Text fw={500} size="lg">{t("merge.header")}</Text>
|
||||
<Stack gap={4}>
|
||||
{files.map((file, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
@@ -89,7 +91,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
</Stack>
|
||||
{selectedCount < 2 && (
|
||||
<Text size="sm" c="red">
|
||||
Please select at least two PDFs to merge.
|
||||
{t("multiPdfPrompt")}
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
@@ -98,7 +100,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
disabled={selectedCount < 2 || isLoading}
|
||||
mt="md"
|
||||
>
|
||||
Merge PDFs
|
||||
{t("merge.submit")}
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<Alert color="red" mt="sm">
|
||||
@@ -114,11 +116,11 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
variant="light"
|
||||
mt="md"
|
||||
>
|
||||
Download Merged PDF
|
||||
{t("downloadPdf")}
|
||||
</Button>
|
||||
)}
|
||||
<Checkbox
|
||||
label="Remove Duplicates"
|
||||
label={t("merge.removeCertSign")}
|
||||
checked={removeDuplicates}
|
||||
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Paper,
|
||||
} from "@mantine/core";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
|
||||
export interface SplitPdfPanelProps {
|
||||
@@ -38,6 +39,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
params,
|
||||
updateParams,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [status, setStatus] = useState("");
|
||||
@@ -61,7 +63,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
setStatus("Please upload a PDF first.");
|
||||
setStatus(t("noFileSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("Processing split...");
|
||||
setStatus(t("loading"));
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
@@ -108,13 +110,13 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
const blob = new Blob([response.data], { type: "application/zip" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
setDownloadUrl(url);
|
||||
setStatus("Download ready.");
|
||||
setStatus(t("downloadComplete"));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
setErrorMessage(
|
||||
error.response?.data || "An error occurred while splitting the PDF."
|
||||
error.response?.data || t("error.pdfPassword", "An error occurred while splitting the PDF.")
|
||||
);
|
||||
setStatus("Split failed.");
|
||||
setStatus(t("error._value", "Split failed."));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -124,21 +126,21 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm" mb={16}>
|
||||
<Select
|
||||
label="Split Mode"
|
||||
label={t("split-by-size-or-count.type.label", "Split Mode")}
|
||||
value={mode}
|
||||
onChange={(v) => v && updateParams({ mode: v })}
|
||||
data={[
|
||||
{ value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
|
||||
{ value: "bySections", label: "Split by Grid Sections" },
|
||||
{ value: "bySizeOrCount", label: "Split by Size or Count" },
|
||||
{ value: "byChapters", label: "Split by Chapters" },
|
||||
{ value: "byPages", label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },
|
||||
{ value: "bySections", label: t("split-by-sections.title", "Split by Grid Sections") },
|
||||
{ value: "bySizeOrCount", label: t("split-by-size-or-count.title", "Split by Size or Count") },
|
||||
{ value: "byChapters", label: t("splitByChapters.title", "Split by Chapters") },
|
||||
]}
|
||||
/>
|
||||
|
||||
{mode === "byPages" && (
|
||||
<TextInput
|
||||
label="Pages"
|
||||
placeholder="e.g. 1,3,5-10"
|
||||
label={t("split.splitPages", "Pages")}
|
||||
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
|
||||
value={pages}
|
||||
onChange={(e) => updateParams({ pages: e.target.value })}
|
||||
/>
|
||||
@@ -147,23 +149,25 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
{mode === "bySections" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Horizontal Divisions"
|
||||
label={t("split-by-sections.horizontal.label", "Horizontal Divisions")}
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={hDiv}
|
||||
onChange={(e) => updateParams({ hDiv: e.target.value })}
|
||||
placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Vertical Divisions"
|
||||
label={t("split-by-sections.vertical.label", "Vertical Divisions")}
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={vDiv}
|
||||
onChange={(e) => updateParams({ vDiv: e.target.value })}
|
||||
placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Merge sections into one PDF"
|
||||
label={t("split-by-sections.merge", "Merge sections into one PDF")}
|
||||
checked={merge}
|
||||
onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
|
||||
/>
|
||||
@@ -173,18 +177,18 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
{mode === "bySizeOrCount" && (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label="Split Type"
|
||||
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||
value={splitType}
|
||||
onChange={(v) => v && updateParams({ splitType: v })}
|
||||
data={[
|
||||
{ value: "size", label: "By Size" },
|
||||
{ value: "pages", label: "By Page Count" },
|
||||
{ value: "docs", label: "By Document Count" },
|
||||
{ value: "size", label: t("split-by-size-or-count.type.size", "By Size") },
|
||||
{ value: "pages", label: t("split-by-size-or-count.type.pageCount", "By Page Count") },
|
||||
{ value: "docs", label: t("split-by-size-or-count.type.docCount", "By Document Count") },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label="Split Value"
|
||||
placeholder="e.g. 10MB or 5 pages"
|
||||
label={t("split-by-size-or-count.value.label", "Split Value")}
|
||||
placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")}
|
||||
value={splitValue}
|
||||
onChange={(e) => updateParams({ splitValue: e.target.value })}
|
||||
/>
|
||||
@@ -194,18 +198,18 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
{mode === "byChapters" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Bookmark Level"
|
||||
label={t("splitByChapters.bookmarkLevel", "Bookmark Level")}
|
||||
type="number"
|
||||
value={bookmarkLevel}
|
||||
onChange={(e) => updateParams({ bookmarkLevel: e.target.value })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Include Metadata"
|
||||
label={t("splitByChapters.includeMetadata", "Include Metadata")}
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Allow Duplicate Bookmarks"
|
||||
label={t("splitByChapters.allowDuplicates", "Allow Duplicate Bookmarks")}
|
||||
checked={allowDuplicates}
|
||||
onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })}
|
||||
/>
|
||||
@@ -213,18 +217,18 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
)}
|
||||
|
||||
<Button type="submit" loading={isLoading} fullWidth>
|
||||
{isLoading ? "Processing..." : "Split PDF"}
|
||||
{isLoading ? t("loading") : t("split.submit", "Split PDF")}
|
||||
</Button>
|
||||
|
||||
{status && <p className="text-xs text-gray-600">{status}</p>}
|
||||
|
||||
{errorMessage && (
|
||||
<Notification color="red" title="Error" onClose={() => setErrorMessage(null)}>
|
||||
<Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)}>
|
||||
{errorMessage}
|
||||
</Notification>
|
||||
)}
|
||||
|
||||
{status === "Download ready." && downloadUrl && (
|
||||
{status === t("downloadComplete") && downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
@@ -233,7 +237,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
color="green"
|
||||
fullWidth
|
||||
>
|
||||
Download Split PDF
|
||||
{t("downloadPdf", "Download Split PDF")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user