diff --git a/archived_files/Header/STJ/Header.js b/archived_files/Header/STJ/Header.js index ac40c1cf..6a69f038 100644 --- a/archived_files/Header/STJ/Header.js +++ b/archived_files/Header/STJ/Header.js @@ -8,7 +8,7 @@ import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownR import KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import logoSTJ from '../../../static/logoSTJ.png'; +import logoApp from '../../../static/logoApp.png'; import loadComponent from '../../../utils/loadComponents'; const TooltipIcon = loadComponent('TooltipIcon', 'TooltipIcon'); @@ -70,7 +70,7 @@ export default class Header extends React.Component { }}> <> - logoSTJ + logoApp - } + { + this.getPrivateSpaceId() + ? t("private space") + ' - ' + this.getPrivateSpaceId() + : t("title") + } + + - - {`Versão: ${VERSION}`} - - - {/* TODO: update help document */} - + + {/* Queue Monitor - Compact View */} + + + + + + + + {t("version")}: {VERSION} + + + + @@ -321,79 +374,103 @@ function App() { marginLeft: 'auto', marginRight: 'auto', justifyContent: 'space-between', - zIndex: '5', - // border: '1px solid #000000', - paddingTop: '0.5rem', - paddingBottom: '0.5rem', + alignItems: 'center', + paddingTop: 'var(--spacing-sm)', + paddingBottom: 'var(--spacing-sm)', + marginBottom: 'var(--spacing-sm)', }}> - - - { - this.state.currentFolderPathList.map((folder, index) => { - const name = index > 0 ? folder : "Início"; - const folderDepth = this.state.currentFolderPathList.length; - - if (this.state.searchMenu && index > 0) - return null; - - // Show hint of collapsed names when inside deep folder - if (folderDepth > 3 && index === 1) { - return ( - -

... /

-
- ) - } - - // Hide intermediate folder names when inside deep folder - if (folderDepth > 3 && index > 0 && index < folderDepth - 2) return null; - - // If not in menu or inside document "folder" containing original and results, - // make current folder non-clickable (folder names are clickable to go back) - if (!this.state.currentFileName && index > 0 && index === folderDepth - 1) { - return

- {name} -

- } else return ( - + { + this.state.currentFolderPathList.map((folder, index) => { + const name = index > 0 ? folder : t("start"); + const folderDepth = this.state.currentFolderPathList.length; + + if (this.state.searchMenu && index > 0) + return null; + + // Show hint of collapsed names when inside deep folder + if (folderDepth > 3 && index === 1) { + return ( + + ... + / + + ) + } + + // Hide intermediate folder names when inside deep folder + if (folderDepth > 3 && index > 0 && index < folderDepth - 2) return null; + + // If not in menu or inside document "folder" containing original and results, + // make current folder non-clickable (folder names are clickable to go back) + if (!this.state.currentFileName && index > 0 && index === folderDepth - 1) { + return ( + - -

/

-
+ {name} + ) - }) - } -

+ } else return ( + + + / + + ) + }) + } + {this.state.currentFileName && ( + {this.state.currentFileName} -

-
+ + )}
@@ -442,6 +519,8 @@ function App() { } /> } /> + } /> + } /> } > } /> diff --git a/website/src/Components/Admin/ConfigManager.js b/website/src/Components/Admin/ConfigManager.js index ef148cb2..0ffaf465 100644 --- a/website/src/Components/Admin/ConfigManager.js +++ b/website/src/Components/Admin/ConfigManager.js @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useRef, useState} from "react"; import axios from "axios"; import { useNavigate } from "react-router"; +import { useTranslation } from 'react-i18next'; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -12,6 +13,7 @@ import FormControlLabel from "@mui/material/FormControlLabel"; import FormLabel from "@mui/material/FormLabel"; import Radio from "@mui/material/Radio"; import RadioGroup from "@mui/material/RadioGroup"; +import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; @@ -35,6 +37,7 @@ import ConfirmActionPopup from 'Components/Form/ConfirmActionPopup'; import CheckboxList from 'Components/Form/CheckboxList'; import Footer from 'Components/Footer/Footer'; import TooltipIcon from "Components/TooltipIcon/TooltipIcon"; +import InfoTooltip from 'Components/Form/InfoTooltip'; const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; const ADMIN_HOME = (process.env.REACT_APP_BASENAME !== null && process.env.REACT_APP_BASENAME !== "") @@ -49,6 +52,7 @@ const _emptydict = {}; const ConfigManager = (props) => { const navigate = useNavigate(); + const { t } = useTranslation(); const [defaultConfig, setDefaultConfig] = useState(_emptydict); const [existingConfigNames, setExistingConfigNames] = useState(_emptylist); @@ -251,7 +255,7 @@ const ConfigManager = (props) => { function changeDpi(value) { value = value.trim(); if (!(/^[1-9][0-9]*$/.test(value))) { - errorNotifRef.current.openNotif("O valor de DPI deve ser um número inteiro!"); + errorNotifRef.current.openNotif(t("admin.dpi_integer_error")); } setDpiVal(value); setUncommittedChanges(true); @@ -346,7 +350,7 @@ const ConfigManager = (props) => { }) .then(response => { if (response.status !== 200) { - throw new Error("Não foi possível concluir o pedido."); + throw new Error(t("admin.request_failed")); } if (response.data["success"]) { successNotifRef.current.openNotif(response.data["message"]); @@ -377,7 +381,7 @@ const ConfigManager = (props) => { }) .then(response => { if (response.status !== 200) { - throw new Error(response.data["message"] || "Não foi possível concluir o pedido."); + throw new Error(response.data["message"] || t("admin.request_failed")); } if (response.data["success"]) { successNotifRef.current.openNotif(response.data["message"]); @@ -398,14 +402,14 @@ const ConfigManager = (props) => { function openSaveConfigPopup(e) { e.stopPropagation(); setConfirmPopupOpened(true); - setConfirmPopupMessage(`Guardar a configuração "${configName}"`); + setConfirmPopupMessage(`${t("admin.save_configuration")} "${configName}"`); setConfirmPopupSubmitCallback(() => saveConfig); // set value as function saveConfig } function openDeleteConfigPopup(e) { e.stopPropagation(); setConfirmPopupOpened(true); - setConfirmPopupMessage(`Tem a certeza que quer apagar a configuração "${configName}"?`); + setConfirmPopupMessage(`${t("admin.confirm_delete_configuration")} "${configName}"?`); setConfirmPopupSubmitCallback(() => deleteConfig); // set value as function deleteConfig } @@ -485,7 +489,7 @@ const ConfigManager = (props) => { paddingTop: '1rem', }}> - Gerir Configurações de OCR + {t("admin.manage_ocr_configurations")} @@ -513,13 +517,19 @@ const ConfigManager = (props) => { className="toolbarTitle" style={{fontSize: "1.5rem", display: "flex", flexDirection: "row"}} > - A alterar configuração + {t("admin.editing_configuration")}   option} + getOptionLabel={(option) => { + // Try to get translation for presets, fallback to raw name + const translationKey = `presets.${option}`; + const translated = t(translationKey); + // If translation key not found, i18next returns the key itself + return translated !== translationKey ? translated : option; + }} autoSelect onChange={(e, newValue) => changeConfigName(newValue, true)} renderInput={(params) => ( @@ -527,7 +537,7 @@ const ConfigManager = (props) => { {...params} required error={!validConfigName} - placeholder="nome" + placeholder={t("name")} variant="outlined" size="small" sx={{ @@ -550,7 +560,7 @@ const ConfigManager = (props) => { {configName ? { openDeleteConfigPopup(e)} icon={} /> @@ -570,11 +580,11 @@ const ConfigManager = (props) => { className="toolbarTitle" style={{fontSize: "1.5rem"}} > - A criar nova configuração: + {t("admin.creating_new_configuration")}   changeConfigName(e.target.value)} @@ -599,8 +609,8 @@ const ConfigManager = (props) => { onClick={() => toggleEditingExistingConfig()} > {isEditingExistingConfig - ? "Terminar" - : "Alterar Configuração Existente" + ? t("finish") + : t("alter existing config") } @@ -610,12 +620,12 @@ const ConfigManager = (props) => { className="menuFunctionButton" onClick={() => resetParameters()} > - Limpar Tudo + {t("clear all")} { startIcon={} onClick={(e) => openSaveConfigPopup(e)} > - Confirmar + {t("confirm")} @@ -648,28 +658,26 @@ const ConfigManager = (props) => { - setOutputList(checked)} - required={configName === "default"} - errorText="Deve selecionar pelo menos um formato de resultado" - /> - - - - + + {t("language")} + + + + setLangList(checked)} showOrder - helperText="Para melhores resultados, selecione por ordem de relevância" + helperText={t("language hint")} required={configName === "default"} - errorText="Deve selecionar pelo menos uma língua" + errorText={t("admin.select_at_least_one_language")} /> @@ -677,27 +685,37 @@ const ConfigManager = (props) => { display: 'flex', flexDirection: 'column', width: '30%', + maxHeight: '65vh', + overflowY: 'auto', + overflowX: 'visible', + paddingRight: '1rem', + paddingLeft: '0.5rem', }}> - changeDpi(e.target.value)} - variant='outlined' - size="small" - className="simpleInput" - sx={{ - "& input:focus:invalid + fieldset": {borderColor: "red", borderWidth: 2} - }} - /> - + + changeDpi(e.target.value)} + variant='outlined' + size="small" + className="simpleInput" + sx={{ + "& input:focus:invalid + fieldset": {borderColor: "red", borderWidth: 2}, + flexGrow: 1, + }} + /> + + + + {/* Engine selector hidden - only TesserOCR is available - Motor de OCR + {t("ocr engine")} { } + */} - Modo do motor + + {t("engine mode")} + + { error={!validSegmentMode || (configName === "default" && segmentMode === -1)} className="simpleDropdown borderTop" > - Segmentação + + {t("segmentation")} + + { error={!validThresholdMethod || (configName === "default" && thresholdMethod === -1)} className="simpleDropdown borderTop" > - Thresholding + + {t("thresholding")} + + { - changeAdditionalParams(e.target.value)} - variant='outlined' - className="simpleInput borderTop" - size="small" - slotProps={{ - inputLabel: {sx: {top: "0.5rem"}} - }} + + changeAdditionalParams(e.target.value)} + variant='outlined' + className="simpleInput borderTop" + size="small" + slotProps={{inputLabel: {sx: {top: "0.5rem"}}}} + sx={{ flexGrow: 1 }} + /> + + + + + + + + {t("output formats")} + + + + setOutputList(checked)} + required={configName === "default"} + errorText={t("admin.select_at_least_one_output")} /> diff --git a/website/src/Components/Admin/Dashboard.js b/website/src/Components/Admin/Dashboard.js index 16f04ca4..63984461 100644 --- a/website/src/Components/Admin/Dashboard.js +++ b/website/src/Components/Admin/Dashboard.js @@ -3,6 +3,7 @@ import {Link, useNavigate} from "react-router"; import axios from "axios"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; +import { useTranslation } from 'react-i18next'; import Footer from 'Components/Footer/Footer'; // const VersionsMenu = loadComponent('Form', 'VersionsMenu'); @@ -18,6 +19,7 @@ const UPDATE_TIME = 30; // period of fetching system info, in seconds const Dashboard = (props) => { const navigate = useNavigate(); + const { t } = useTranslation(); const [freeSpace, setFreeSpace] = useState(""); const [freeSpacePercent, setFreeSpacePercent] = useState(""); @@ -66,7 +68,7 @@ const Dashboard = (props) => { flexDirection: 'row', alignItems: "center", }}> - Armazenamento livre: {freeSpace} ({freeSpacePercent}%) + {t("admin.free_storage")}: {freeSpace} ({freeSpacePercent}%) @@ -94,7 +96,7 @@ const Dashboard = (props) => { className="adminMenuButton" onClick={() => navigate('/admin/storage')} > - Gerir Armazenamento + {t("admin.manage_storage")} @@ -111,7 +113,7 @@ const Dashboard = (props) => { className="adminMenuButton" sx={{width: '100%'}} > - Ver Workers e Processos + {t("admin.view_workers_processes")} diff --git a/website/src/Components/Admin/LoginPage.js b/website/src/Components/Admin/LoginPage.js index 56993bbf..5d5ebf97 100644 --- a/website/src/Components/Admin/LoginPage.js +++ b/website/src/Components/Admin/LoginPage.js @@ -1,6 +1,7 @@ import React, {useRef, useState} from 'react'; import axios from 'axios'; import {useLocation, useNavigate} from "react-router"; +import { useTranslation } from 'react-i18next'; import Notification from 'Components/Notifications/Notification'; @@ -9,6 +10,7 @@ const API_URL = `${window.location.protocol}//${window.location.host}/${process. const LoginPage = ({ isAuthenticated = false, setLoggedIn = null }) => { const navigate = useNavigate(); const location = useLocation(); + const { t } = useTranslation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -26,7 +28,7 @@ const LoginPage = ({ isAuthenticated = false, setLoggedIn = null }) => { .catch((error) => { console.log(error); if (error.status === 400) { - errorNotif.current.openNotif("Email ou password incorretos"); + errorNotif.current.openNotif(t("admin.email_password_incorrect")); } else { errorNotif.current.openNotif(error.message); } @@ -43,10 +45,10 @@ const LoginPage = ({ isAuthenticated = false, setLoggedIn = null }) => {
-

OCR Admin Login

+

{t("admin.login_title")}

- + { />
- + setPassword(e.target.value)} />
- +
); diff --git a/website/src/Components/Admin/StorageManager.js b/website/src/Components/Admin/StorageManager.js index 27208359..69c8868b 100644 --- a/website/src/Components/Admin/StorageManager.js +++ b/website/src/Components/Admin/StorageManager.js @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import axios from "axios"; import { useNavigate } from "react-router"; +import { useTranslation } from 'react-i18next'; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -31,14 +32,14 @@ const ADMIN_HOME = (process.env.REACT_APP_BASENAME !== null && process.env.REACT const numberHoursRegex = /^[1-9][0-9]*$/; const dayRegex = /^([1-9]|0[1-9]|[1-2][0-9]|3[0-1])$/; -const weekDaysOptions = [ - { value: "mon", description: "Segunda-feira"}, - { value: "tue", description: "Terça-feira"}, - { value: "wed", description: "Quarta-feira"}, - { value: "thu", description: "Quinta-feira"}, - { value: "fri", description: "Sexta-feira"}, - { value: "sat", description: "Sábado"}, - { value: "sun", description: "Domingo"}, +const getWeekDaysOptions = (t) => [ + { value: "mon", description: t("weekdays.monday")}, + { value: "tue", description: t("weekdays.tuesday")}, + { value: "wed", description: t("weekdays.wednesday")}, + { value: "thu", description: t("weekdays.thursday")}, + { value: "fri", description: t("weekdays.friday")}, + { value: "sat", description: t("weekdays.saturday")}, + { value: "sun", description: t("weekdays.sunday")}, ] const sizeRegex = /(\d+(?:\.\d+)?) ([A-Za-z]+)/; // match both e.g. "50 KB" and "50.00 KB" @@ -51,17 +52,22 @@ const sizeMap = { const StorageManager = (props) => { const navigate = useNavigate(); + const { t } = useTranslation(); const [freeSpace, setFreeSpace] = useState(""); const [freeSpacePercent, setFreeSpacePercent] = useState(""); const [privateSpaces, setPrivateSpaces] = useState([]); const [apiFiles, setApiFiles] = useState([]); - const [lastCleanup, setLastCleanup] = useState("nunca"); + const [lastCleanup, setLastCleanup] = useState(t("never")); const [maxPrivateSpaceAge, setMaxPrivateSpaceAge] = useState("1"); const [refreshing, setRefreshing] = useState(true); const [lastUpdate, setLastUpdate] = useState(null); + const [maxConcurrentFolders, setMaxConcurrentFolders] = useState(1); + const [activeFolderCount, setActiveFolderCount] = useState(0); + const [queuedFolderCount, setQueuedFolderCount] = useState(0); + const [scheduleType, setScheduleType] = useState("interval"); const [everyHours, setEveryHours] = useState(''); @@ -112,8 +118,49 @@ const StorageManager = (props) => { }); } + function getFolderConcurrency() { + axios.get(API_URL + '/admin/get-folder-concurrency') + .then(({ data }) => { + if (data.success) { + setMaxConcurrentFolders(data.max_concurrent_folders); + setActiveFolderCount(data.active_count); + setQueuedFolderCount(data.queued_count); + } + }) + .catch(err => { + console.error('Failed to fetch folder concurrency:', err); + }); + } + + function saveFolderConcurrency() { + axios.post(API_URL + '/admin/set-folder-concurrency', + { + max_concurrent_folders: parseInt(maxConcurrentFolders) + }, + { + headers: { + 'Content-Type': 'application/json' + }, + }) + .then(response => { + if (response.status !== 200) { + throw new Error(t("admin.request_failed")); + } + if (response.data["success"]) { + successNotif.current.openNotif(t("admin.folder_concurrency_updated")); + getFolderConcurrency(); // Refresh the counts + } else { + throw new Error(response.data["message"]); + } + }) + .catch(err => { + errorNotif.current.openNotif(err.message); + }); + } + useEffect(() => { getStorageInfo(); + getFolderConcurrency(); }, []); const deleteApiDocument = useCallback(() => { @@ -128,7 +175,7 @@ const StorageManager = (props) => { }) .then(response => { if (response.status !== 200) { - throw new Error(response.data["message"] || "Não foi possível concluir o pedido."); + throw new Error(response.data["message"] || t("admin.request_failed")); } if (!response.data["success"]) { throw new Error(response.data["message"]); @@ -154,7 +201,7 @@ const StorageManager = (props) => { }) .then(response => { if (response.status !== 200) { - throw new Error(response.data["message"] || "Não foi possível concluir o pedido."); + throw new Error(response.data["message"] || t("admin.request_failed")); } if (!response.data["success"]) { throw new Error(response.data["message"]); @@ -172,11 +219,11 @@ const StorageManager = (props) => { useEffect(() => { if (deleteSpaceId !== null) { setConfirmPopupOpened(true); - setConfirmPopupMessage(`Tem a certeza que quer apagar o espaço ${deleteSpaceId}?`); + setConfirmPopupMessage(`${t("admin.confirm_delete_space")} ${deleteSpaceId}?`); setConfirmPopupSubmitCallback(() => deletePrivateSpace); // set value as function deletePrivateSpace } else if (deleteApiDocumentId !== null) { setConfirmPopupOpened(true); - setConfirmPopupMessage(`Tem a certeza que quer apagar o documento com ID ${deleteApiDocumentId}?`); + setConfirmPopupMessage(`${t("admin.confirm_delete_document")} ${deleteApiDocumentId}?`); setConfirmPopupSubmitCallback(() => deleteApiDocument); // set value as function deleteApiDocument } }, [deleteSpaceId, deleteApiDocumentId, deletePrivateSpace, deleteApiDocument]) @@ -205,7 +252,7 @@ const StorageManager = (props) => { function handleEveryHoursChange(value) { value = value.trim(); if (!(numberHoursRegex.test(value)) && value !== '') { - errorNotif.current.openNotif("O número de horas deve ser um valor inteiro positivo!"); + errorNotif.current.openNotif(t("admin.hours_positive_integer")); } setEveryHours(value); } @@ -213,7 +260,7 @@ const StorageManager = (props) => { function handleMonthDayChange(value) { value = value.trim(); if (!(dayRegex.test(value)) && value !== "0" && value !== '') { - errorNotif.current.openNotif("O dia deve ser um número entre 1 e 31!"); + errorNotif.current.openNotif(t("admin.day_between_1_31")); } setMonthDay(value); } @@ -253,7 +300,7 @@ const StorageManager = (props) => { function openCleanupPopup(e) { e.stopPropagation(); setConfirmPopupOpened(true); - setConfirmPopupMessage(`Tem a certeza que quer remover as sessões com mais de ${maxPrivateSpaceAge} dia(s)?`); + setConfirmPopupMessage(`${t("admin.confirm_remove_sessions")} ${maxPrivateSpaceAge} ${t("days")}?`); setConfirmPopupSubmitCallback(() => runPrivateSpaceCleanup); // set value as function runPrivateSpaceCleanup } @@ -269,7 +316,7 @@ const StorageManager = (props) => { axios.post(API_URL + "/admin/cleanup-private-spaces") .then(response => { if (response.status !== 200) { - throw new Error("Não foi possível concluir o pedido."); + throw new Error(t("admin.request_failed")); } if (response.data["success"]) { successNotif.current.openNotif(response.data["message"]); @@ -316,7 +363,7 @@ const StorageManager = (props) => { }) .then(response => { if (response.status !== 200) { - throw new Error("Não foi possível concluir o pedido."); + throw new Error(t("admin.request_failed")); } if (response.data["success"]) { successNotif.current.openNotif(response.data["message"]); @@ -372,16 +419,16 @@ const StorageManager = (props) => { flexBasis: '0', }}> - Armazenamento livre: {freeSpace} ({freeSpacePercent}%) + {t("admin.free_storage")}: {freeSpace} ({freeSpacePercent}%) - Última limpeza: {lastCleanup} + {t("admin.last_cleanup")}: {lastCleanup} - Gerir Armazenamento + {t("admin.manage_storage")} { }} className="menuButton" > - Sair + {t("logout")} @@ -417,10 +464,10 @@ const StorageManager = (props) => { startIcon={} onClick={() => getStorageInfo()} > - Refresh + {t("refresh")} - Último update: {lastUpdate ? lastUpdate.toLocaleString("pt-PT") : "nunca"} + {t("admin.last_update")}: {lastUpdate ? lastUpdate.toLocaleString("pt-PT") : t("never")} @@ -430,7 +477,7 @@ const StorageManager = (props) => { onClick={(e) => openCleanupPopup(e)} className="menuButton menuFunctionButton" > - Remover espaços privados com mais de {maxPrivateSpaceAge} dia(s) + {t("admin.remove_private_spaces_older")} {maxPrivateSpaceAge} {t("days")} @@ -472,7 +519,7 @@ const StorageManager = (props) => { width: "fit-content", height: "fit-content", }}> - Documentos de API + {t("admin.api_documents")} { apiFiles.map(([apiFile, info], index) => { return ( @@ -498,7 +545,7 @@ const StorageManager = (props) => { openDeleteApiDocumentPopup(e, apiFile)} icon={} /> @@ -530,7 +577,7 @@ const StorageManager = (props) => { width: "fit-content", height: "fit-content", }}> - Espaços Privados + {t("admin.private_spaces")} { privateSpaces.map(([privateSpace, info], index) => { return ( @@ -560,7 +607,7 @@ const StorageManager = (props) => { openDeleteSpacePopup(e, privateSpace)} icon={} /> @@ -579,13 +626,67 @@ const StorageManager = (props) => { paddingLeft: '10px', borderLeft: '1px solid black', }}> + + + {t("admin.folder_concurrency_title")} + + + + + {t("admin.active_folders")}: {activeFolderCount} + + + {t("admin.queued_folders")}: {queuedFolderCount} + + + + + setMaxConcurrentFolders(e.target.value)} + size="small" + variant="outlined" + inputProps={{ min: 1 }} + sx={{ width: '60%' }} + /> + + + + - Definir horário de limpeza automática + {t("admin.set_cleanup_schedule")} @@ -611,7 +712,7 @@ const StorageManager = (props) => { flexDirection: 'column', }}> } onChange={() => handleScheduleTypeChange("interval")} @@ -622,7 +723,7 @@ const StorageManager = (props) => { flexDirection: 'row', alignItems: 'center', }}> - A cada + {t("every")} { textAlign: "center", }} /> - horas + {t("hours")} @@ -648,7 +749,7 @@ const StorageManager = (props) => { flexDirection: 'column', }}> } onChange={() => handleScheduleTypeChange("weekly")} @@ -657,7 +758,7 @@ const StorageManager = (props) => { { @@ -731,7 +832,7 @@ const StorageManager = (props) => { flexDirection: 'column', }}> } onChange={() => handleScheduleTypeChange("monthly")} @@ -744,7 +845,7 @@ const StorageManager = (props) => { { error={scheduleType === "monthly" && !(dayRegex.test(monthDay))} value={monthDay} onChange={(e) => handleMonthDayChange(e.target.value)} - label="Dia" + label={t("day")} size="small" variant="outlined" className="simpleInput" diff --git a/website/src/Components/EditingMenu/EditingMenu.js b/website/src/Components/EditingMenu/EditingMenu.js index 63c68317..f2953c15 100644 --- a/website/src/Components/EditingMenu/EditingMenu.js +++ b/website/src/Components/EditingMenu/EditingMenu.js @@ -1,5 +1,6 @@ import React from 'react'; import axios from 'axios'; +import { withTranslation } from 'react-i18next'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; @@ -289,12 +290,25 @@ class EditingMenu extends React.Component { this.confirmLeave.current.toggleOpen(); } + constructPath(includeSpaceId = false) { + // Build path correctly, avoiding double slashes + let parts = []; + if (includeSpaceId && this.props.spaceId) { + parts.push(this.props.spaceId); + } + if (this.props.current_folder) { + parts.push(this.props.current_folder); + } + parts.push(this.props.filename); + return parts.join('/'); + } + getContents(page = 1) { - const path = (this.props.current_folder + '/' + this.props.filename).replace(/^\//, ''); + const path = this.constructPath(this.props._private); axios.get(API_URL + '/get-text-content', { params: { _private: this.props._private, - path: (this.props._private ? this.props.spaceId + '/' + path : path), + path: path, page: page, } }) @@ -322,7 +336,7 @@ class EditingMenu extends React.Component { }); }) .catch(err => { - this.errorNotifRef.current.openWithMessage("Não foi possível obter resultados"); + this.errorNotifRef.current.openWithMessage(this.props.t("editing.error_load")); }); } @@ -874,7 +888,7 @@ class EditingMenu extends React.Component { this.setState({uncommittedChanges: false, mustRecreate: !remakeFiles}); window.removeEventListener('beforeunload', this.preventExit); - this.successNot.current.openNotif("Texto submetido com sucesso"); + this.successNot.current.openNotif(this.props.t("editing.success_submit")); if (remakeFiles) { this.leave(); @@ -884,7 +898,7 @@ class EditingMenu extends React.Component { } }) .catch(err => { - this.errorNotifRef.current.openWithMessage("Não foi possível submeter os resultados"); + this.errorNotifRef.current.openWithMessage(this.props.t("editing.error_submit")); }); } @@ -946,7 +960,7 @@ class EditingMenu extends React.Component { - + this.saveChanges(true)} ref={this.confirmLeave} /> {<> @@ -960,7 +974,7 @@ class EditingMenu extends React.Component { component="h2" className="toolbarTitle" > - Editar os resultados do documento + {this.props.t("editing.title")} @@ -975,7 +989,7 @@ class EditingMenu extends React.Component { onClick={() => {this.setState({editLinesMode: false, hoveredId: null})}} startIcon={} > - Terminar + {this.props.t("finish")} : } @@ -1036,7 +1050,7 @@ class EditingMenu extends React.Component { onClick={() => {this.setState({showConfidence: false})}} startIcon={} > - Mostrar Texto Simples + {this.props.t("editing.show_simple_text")} : } @@ -1062,7 +1076,7 @@ class EditingMenu extends React.Component { } startIcon={ }> - Guardar + {this.props.t("editing.save")} @@ -1125,7 +1139,7 @@ class EditingMenu extends React.Component { - Página + {this.props.t("editing.page")}   {this.state.totalPages === 1 ? this.state.currentPage @@ -1202,7 +1216,7 @@ class EditingMenu extends React.Component { backgroundColor: "#f0f0f0", padding: "0px 10px" }}> -

Palavras

+

{this.props.t("editing.words")}

this.requestSyntax()} > - Verificar ortografia + {this.props.t("editing.check_spelling")} { @@ -1354,4 +1368,4 @@ EditingMenu.defaultProps = { closeEditingMenu: null } -export default EditingMenu; +export default withTranslation()(EditingMenu); diff --git a/website/src/Components/FileSystem/DocumentCard.js b/website/src/Components/FileSystem/DocumentCard.js new file mode 100644 index 00000000..5d5280c5 --- /dev/null +++ b/website/src/Components/FileSystem/DocumentCard.js @@ -0,0 +1,465 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Tooltip from '@mui/material/Tooltip'; +import CircularProgress from '@mui/material/CircularProgress'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import SettingsIcon from '@mui/icons-material/Settings'; +import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; +import TuneIcon from '@mui/icons-material/Tune'; +import DownloadIcon from '@mui/icons-material/Download'; +import EditIcon from '@mui/icons-material/Edit'; +import ImageIcon from '@mui/icons-material/Image'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; + +import { withTranslation } from "react-i18next"; +import OcrIcon from 'Components/CustomIcons/OcrIcon'; +import LayoutIcon from 'Components/CustomIcons/LayoutIcon'; +import PdfIcon from 'Components/CustomIcons/PdfIcon'; + +const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; +const BASE_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_BASENAME}`; + +class DocumentCard extends React.Component { + constructor(props) { + super(props); + this.state = { + info: props.info, + contextMenu: null, + imageLoaded: false, + imageError: false, + isHovered: false, + }; + } + + updateInfo(info) { + this.setState({ info: info }); + } + + componentDidUpdate(prevProps) { + if (prevProps.info !== this.props.info && this.props.info !== null) { + this.setState({ info: this.props.info }); + } + } + + handleOptionsClick(event) { + event.stopPropagation(); + this.setState({ + contextMenu: this.state.contextMenu === null + ? { anchorEl: event.currentTarget } + : null + }); + } + + handleContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ + contextMenu: this.state.contextMenu === null + ? { mouseX: event.clientX + 2, mouseY: event.clientY - 6 } + : null + }); + } + + handleCloseContextMenu() { + this.setState({ contextMenu: null }); + } + + documentClicked() { + // Open the Layout Menu (document viewer) when clicking the card + if (this.state.info?.stored === true) { + this.props.createLayout(this.props.name); + } + } + + performOCR(e, usingCustomConfig) { + e.stopPropagation(); + this.handleCloseContextMenu(); + const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; + this.props.performOCR(this.props.name, false, false, customConfig); + } + + configureOCR(e, usingCustomConfig) { + e.stopPropagation(); + this.handleCloseContextMenu(); + const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; + this.props.configureOCR(this.props.name, false, false, customConfig); + } + + createLayout(e) { + e.stopPropagation(); + this.handleCloseContextMenu(); + this.props.createLayout(this.props.name); + } + + editText(e) { + e.stopPropagation(); + this.handleCloseContextMenu(); + this.props.editText(this.props.name); + } + + delete(e) { + e.stopPropagation(); + this.handleCloseContextMenu(); + this.props.deleteItem(this.props.name); + } + + getStatusBadges() { + const info = this.state.info; + if (!info) return null; + + const stored = info["stored"]; + const status = info["status"]; + const ocrInfo = info["ocr"]; + const usingCustomConfig = info?.["config"] && info["config"] !== "default"; + + const badges = []; + + // Priority badges (only show one at a time, in order of priority) + // Show "Preparing" or "Uploading" badge with progress when stored is a number + if (typeof stored === "number") { + const isUploading = status?.stage === "uploading"; + badges.push( + + + {isUploading ? this.props.t("uploading stage") : this.props.t("preparing stage")} ({Math.round(stored)}%) + + ); + return badges; + } + + if (stored === false) { + badges.push( + + + {this.props.t("uploading")} + + ); + return badges; + } + + if (stored === "stuck") { + badges.push( + + + {this.props.t("upload error")} + + ); + return badges; + } + + // Error state + if (status?.stage === "error") { + const errorMsg = status.message + ? this.props.t(status.message) + : this.props.t("error"); + badges.push( + + + {errorMsg} + + ); + return badges; + } + + // Compression in progress + if (status?.stage === "compressing") { + const currentPage = status.current; + const totalPages = status.total; + badges.push( + + + {currentPage && totalPages + ? this.props.t("compressing file page", { current: currentPage, total: totalPages }) + : this.props.t("compressing") + } + + ); + return badges; + } + + // Exporting stage + if (status?.stage === "exporting") { + badges.push( + + + {this.props.t("exporting")} + + ); + return badges; + } + + // Waiting/queued stage - only show if OCR has been requested + if (status?.stage === "waiting" && (ocrInfo || stored === false)) { + badges.push( + + + {this.props.t("preparing ocr")} + + ); + return badges; + } + + // OCR progress badge (in progress) + if (ocrInfo) { + const progress = ocrInfo["progress"]; + const pages = info["pages"]; + + if (progress > 0 && progress < pages) { + badges.push( + + + OCR {progress}/{pages} + + ); + return badges; + } + } + + // Cancelled state + if (status?.stage === "cancelled") { + badges.push( + + {this.props.t("ocr cancelled")} + + ); + return badges; + } + + // Persistent indicators (can show multiple at once) + // OCR complete indicator (top-right) - styled like folder finished badge + if (status?.stage === "post-ocr" && stored === true) { + badges.push( + + ✓ {this.props.t("queue.finished")} + + ); + } + + // Custom OCR config indicator (top-left) + if (usingCustomConfig && stored === true) { + badges.push( + + + + ); + } + + return badges.length > 0 ? badges : null; + } + + render() { + if (!this.state.info) { + return ( + + ); + } + + const info = this.state.info; + const stored = info["stored"]; + const pages = info["pages"]; + const size = info["size"]; + const creation = info["creation"]; + const ocrInfo = info["ocr"]; + const usingCustomConfig = info?.["config"] && info["config"] !== "default"; + + const isProcessing = stored === false || (ocrInfo && ocrInfo["progress"] < pages); + const hasOCR = Boolean(ocrInfo); + + // Use large (600px) thumbnail for better quality in card view + const thumbnailUrl = `${BASE_URL}/${this.props._private ? 'private' : 'images'}/${this.props.thumbnails.large}`; + + return ( + <> + this.documentClicked()} + onContextMenu={(e) => this.handleContextMenu(e)} + onMouseEnter={() => this.setState({ isHovered: true })} + onMouseLeave={() => this.setState({ isHovered: false })} + sx={{ opacity: stored === false ? 0.7 : 1, cursor: stored === true ? 'pointer' : 'default' }} + > + + {!this.state.imageLoaded && !this.state.imageError && ( + + )} + {this.state.imageError ? ( + + ) : ( + {this.props.name} this.setState({ imageLoaded: true })} + onError={() => this.setState({ imageError: true })} + style={{ display: this.state.imageLoaded ? 'block' : 'none' }} + /> + )} + {this.getStatusBadges()} + + + + {this.props.name} + + + {pages} {this.props.t("pages")} + {size} + + {creation && ( + + {creation} + + )} + + + + + this.handleOptionsClick(e)} + sx={{ + position: 'relative', + zIndex: 2, + '&:hover': { + backgroundColor: 'var(--accent-primary)', + color: 'white' + } + }} + > + + + + + + this.handleCloseContextMenu()} + anchorReference={this.state.contextMenu?.anchorEl ? "anchorEl" : "anchorPosition"} + anchorEl={this.state.contextMenu?.anchorEl} + anchorPosition={ + this.state.contextMenu?.mouseY + ? { top: this.state.contextMenu.mouseY, left: this.state.contextMenu.mouseX } + : undefined + } + > + this.documentClicked()} disabled={stored !== true}> + + {this.props.t("see document")} + + + {hasOCR && ( + this.editText(e)} disabled={isProcessing}> + + {this.props.t("edit text")} + + )} + + this.createLayout(e)} disabled={stored !== true}> + + {this.props.t("layout create")} + + + this.performOCR(e, usingCustomConfig)} disabled={isProcessing}> + + {hasOCR ? this.props.t("repeat ocr") : this.props.t("run ocr")} + + + this.configureOCR(e, usingCustomConfig)} disabled={isProcessing}> + {usingCustomConfig ? ( + + ) : ( + + )} + {this.props.t("config ocr")} + + + {hasOCR && ( + <> + { e.stopPropagation(); this.props.getDocument("txt", this.props.name, "txt"); this.handleCloseContextMenu(); }} disabled={isProcessing}> + + {this.props.t("download txt")} + + { e.stopPropagation(); this.props.getDocument("pdf", this.props.name, "pdf"); this.handleCloseContextMenu(); }} disabled={isProcessing}> + + {this.props.t("download pdf")} + + { e.stopPropagation(); this.props.getImages(this.props.name); this.handleCloseContextMenu(); }} disabled={isProcessing}> + + {this.props.t("download images")} + + + )} + + { e.stopPropagation(); this.props.getOriginalFile(this.props.name); this.handleCloseContextMenu(); }} disabled={stored !== true}> + + {this.props.t("download original")} + + + this.delete(e)} sx={{ color: 'var(--red-600)' }}> + + {this.props.t("delete")} + + + + ); + } +} + +DocumentCard.defaultProps = { + name: "", + thumbnails: { small: "", large: "" }, + _private: false, + info: null, + enterDocument: null, + deleteItem: null, + getOriginalFile: null, + getDocument: null, + getEntities: null, + requestEntities: null, + getImages: null, + editText: null, + performOCR: null, + configureOCR: null, + createLayout: null, +}; + +export default withTranslation()(DocumentCard); + + diff --git a/website/src/Components/FileSystem/DocumentRow.js b/website/src/Components/FileSystem/DocumentRow.js index 4ca3ee0d..83e93e1a 100644 --- a/website/src/Components/FileSystem/DocumentRow.js +++ b/website/src/Components/FileSystem/DocumentRow.js @@ -7,7 +7,10 @@ import MenuItem from '@mui/material/MenuItem'; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; +import {withTranslation} from "react-i18next"; + import DoneIcon from '@mui/icons-material/Done'; +import CancelIcon from '@mui/icons-material/Cancel'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import BorderAllIcon from '@mui/icons-material/BorderAll'; import BorderClearIcon from '@mui/icons-material/BorderClear'; @@ -17,6 +20,7 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import SettingsIcon from '@mui/icons-material/Settings'; import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; +import TuneIcon from '@mui/icons-material/Tune'; import FileIcon from 'Components/CustomIcons/FileIcon'; import OcrIcon from 'Components/CustomIcons/OcrIcon'; @@ -47,13 +51,22 @@ class DocumentRow extends React.Component { this.getDelimiterTxt = this.getDelimiterTxt.bind(this); this.getCSV = this.getCSV.bind(this); this.getPdfIndexed = this.getPdfIndexed.bind(this); + this.getPdfIndexedUncompressed = this.getPdfIndexedUncompressed.bind(this); this.getPdfSimple = this.getPdfSimple.bind(this); + this.getPdfSimpleUncompressed = this.getPdfSimpleUncompressed.bind(this); this.getEntities = this.getEntities.bind(this); this.getAlto = this.getAlto.bind(this); this.getHocr = this.getHocr.bind(this); this.getImages = this.getImages.bind(this); } + componentDidUpdate(prevProps) { + // Update local state when info prop changes (e.g., after saving config) + if (prevProps.info !== this.props.info) { + this.updateInfo(this.props.info); + } + } + updateInfo(info) { if (this.state.info?.["status"]?.stage !== "post-ocr" && info?.["status"]?.stage === "post-ocr") { this.setState({info: info, expanded: true}); @@ -152,6 +165,13 @@ class DocumentRow extends React.Component { this.props.getDocument("pdf_indexed", file, "pdf", "_texto_indice"); } + /** + * Export the uncompressed .pdf indexed file + */ + getPdfIndexedUncompressed(file) { + this.props.getDocumentUncompressed("pdf_indexed_uncompressed", file, "pdf", "_texto_indice_uncompressed"); + } + /** * Export the .pdf file */ @@ -159,6 +179,13 @@ class DocumentRow extends React.Component { this.props.getDocument("pdf", file, "pdf", "_texto"); } + /** + * Export the uncompressed .pdf file + */ + getPdfSimpleUncompressed(file) { + this.props.getDocumentUncompressed("pdf_uncompressed", file, "pdf", "_texto_uncompressed"); + } + /** * Export the entities list */ @@ -203,6 +230,11 @@ class DocumentRow extends React.Component { this.props.performOCR(this.props.name, false, this.state.info?.["ocr"] !== undefined, customConfig); } + cancelOCR(e) { + e.stopPropagation(); + this.props.cancelOCR(this.props.name); + } + configureOCR(e, usingCustomConfig) { e.stopPropagation(); const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; @@ -226,10 +258,22 @@ class DocumentRow extends React.Component { render() { const info = this.state.info; - const usingCustomConfig = info?.["config"] && info["config"] !== "default"; - const hasLayoutBoxes = info?.["has_layout"]; - const status = info?.["status"]; - const buttonsDisabled = status.stage !== "waiting" && status.stage !== "post-ocr"; + + // Return loading state if info is not available yet + if (!info) { + return ( + + + + + + ); + } + + const usingCustomConfig = info["config"] && info["config"] !== "default"; + const hasLayoutBoxes = info["has_layout"]; + const status = info["status"]; + const buttonsDisabled = !status || (status?.stage !== "waiting" && status?.stage !== "post-ocr" && status?.stage !== "cancelled"); const uploadIsStuck = info["upload_stuck"] === true; return ( <> @@ -251,17 +295,28 @@ class DocumentRow extends React.Component { this.performOCR(e, usingCustomConfig)} > -  Fazer OCR +  {this.props.t(info?.["ocr"] ? "repeat ocr" : "run ocr")} + {(status?.stage === "ocr" || status?.stage === "exporting" || status?.stage === "compressing") && ( + this.cancelOCR(e)} + > + + + +  {this.props.t("cancel ocr")} + + )} + this.configureOCR(e, usingCustomConfig)} > {usingCustomConfig ? : } -  {usingCustomConfig ? "Editar Configuração" : "Configurar OCR"} +  {usingCustomConfig ? this.props.t("alter existing config") : this.props.t("config ocr")} this.createLayout(e)} > {hasLayoutBoxes ? : } -  {hasLayoutBoxes ? "Alterar Segmentação" : "Definir Segmentação"} +  {hasLayoutBoxes ? this.props.t("edit results") : this.props.t("layout create")} this.editFile(e)} > -  Editar Resultados +  {this.props.t("edit text")} { @@ -307,34 +362,34 @@ class DocumentRow extends React.Component { ? null : (info?.["indexed"] ? this.removeIndex(e)} > -  Desindexar +  {this.props.t("deindex")} : this.indexFile(e)} > -  Indexar +  {this.props.t("index")} ) } this.delete(e)} > -  Apagar +  {this.props.t("delete")} @@ -350,11 +405,11 @@ class DocumentRow extends React.Component { this.handleContextMenu(e)} - sx={{ cursor: loadingStages.has(status.stage) ? "progress" : "context-menu" }} + sx={{ cursor: loadingStages.has(status?.stage) ? "progress" : "context-menu" }} > this.handleOptionsClick(e)} > @@ -364,9 +419,9 @@ class DocumentRow extends React.Component { - {loadingStages.has(status.stage) + {loadingStages.has(status?.stage) ? : { - if (!loadingStages.has(status.stage)) + if (!loadingStages.has(status?.stage)) this.setState({expanded: !this.state.expanded}) }} - sx={{ cursor: loadingStages.has(status.stage) ? "progress" : "pointer" }} + sx={{ cursor: loadingStages.has(status?.stage) ? "progress" : "pointer" }} > {this.state.expanded ? : } {this.props.name} + {usingCustomConfig && ( + + )} - + this.createLayout(e)}> - {info["pages"] ? (info["pages"] + " página(s)") : null} - {info["words"] ? ("\n" + info["words"] + " palavras") : null} + {info["pages"] ? (info["pages"] + " " + this.props.t("page") + "(s)") : null} + {info["words"] ? ("\n" + info["words"] + " " + this.props.t("words")) : null} - {info["total_size"]} + {info["size"]} @@ -428,76 +495,139 @@ class DocumentRow extends React.Component { ? uploadIsStuck ? - Erro ao carregar documento + {this.props.t("upload error")} - : status.stage === "uploading" + : status?.stage === "uploading" ? - - {status.message} + + + {this.props.t("uploading stage")} + {typeof info["stored"] === "number" && ` (${Math.round(info["stored"])}%)`} + - : status.stage === "preparing" + : status?.stage === "preparing" ? - - {status.message} + + + {this.props.t("preparing stage")} + {typeof info["stored"] === "number" && ` (${Math.round(info["stored"])}%)`} + : null - : status.stage === "error" + : status?.stage === "error" ? - {status.message} + + {status.message + ? (status.message_page + ? this.props.t(status.message, { page: status.message_page }) + : status.message_details + ? `${this.props.t(status.message)}: ${status.message_details}` + : this.props.t(status.message) + ) + : this.props.t("upload error") + } + - : status.stage === "waiting" + : status?.stage === "waiting" ? - : status.stage === "ocr" + : status?.stage === "cancelled" + ? + + {this.props.t("ocr cancelled")} + + + + : status?.stage === "ocr" ? - OCR -   - {info["ocr"]["progress"]}/{info["pages"]} -   -   - { /* message is expected to be time estimate */ - status.message + { /* message with current/total for page progress */ + status.message && status.message_current && status.message_total + ? this.props.t(status.message, { current: status.message_current, total: status.message_total }) + : status.message } - : status.stage === "exporting" + : status?.stage === "exporting" ? - {status.message} + + {status.message && status.message_current && status.message_total + ? this.props.t(status.message, { current: status.message_current, total: status.message_total }) + : status.message + ? this.props.t(status.message) + : "" + } + + + + + : status?.stage === "compressing" + ? + + + + {status.current && status.total + ? this.props.t("compressing file page", { current: status.current, total: status.total }) + : status.message + ? this.props.t(status.message) + : this.props.t("compressing") + } + : info["edited_results"] // expected stage when this is true is "post-ocr" so much be checked before ? - Resultados editados, ficheiros por recriar + {this.props.t("edited results files to recreate")} - : status.stage === "post-ocr" + : status?.stage === "post-ocr" ? - + ✓ {this.props.t("queue.finished")} + + + + : status?.stage === "cancelled" + ? + + + {this.props.t("ocr cancelled")} @@ -519,31 +649,67 @@ class DocumentRow extends React.Component { ? } downloadFile={this.getPdfIndexed} /> : null } + {info["pdf_indexed"]?.complete && info["pdf_indexed"]["uncompressed_size"] + ? } + downloadFile={this.getPdfIndexedUncompressed} + /> : null + } {info["pdf"]?.complete ? } downloadFile={this.getPdfSimple} /> : null } + {info["pdf"]?.complete && info["pdf"]["uncompressed_size"] + ? } + downloadFile={this.getPdfSimpleUncompressed} + /> : null + } {info["txt"]?.complete ? + + + + + {this.props.t("empty folder title")} + + + {this.props.t("empty folder description")} + + + ); + } + + render() { + const { items, showViewToggle = true } = this.props; + const { viewMode } = this.state; + + return ( + + {showViewToggle && ( + + + + viewMode !== 'grid' && this.toggleViewMode()} + className={`view-toggle-button ${viewMode === 'grid' ? 'active' : ''}`} + > + + + + + viewMode !== 'list' && this.toggleViewMode()} + className={`view-toggle-button ${viewMode === 'list' ? 'active' : ''}`} + > + + + + + + )} + + {items && items.length === 0 ? ( + this.renderEmptyState() + ) : viewMode === 'grid' ? ( + + {items} + + ) : ( + + {items} + + )} + + ); + } +} + +FileGridView.defaultProps = { + items: [], + showViewToggle: true, + onViewModeChange: null, +}; + +export default withTranslation()(FileGridView); + + diff --git a/website/src/Components/FileSystem/FileSystem.js b/website/src/Components/FileSystem/FileSystem.js index 7809e70b..1336bd2f 100644 --- a/website/src/Components/FileSystem/FileSystem.js +++ b/website/src/Components/FileSystem/FileSystem.js @@ -1,6 +1,9 @@ import React from 'react'; import axios from 'axios'; import dayjs from 'dayjs'; +import { Link } from 'react-router'; + +import { withTranslation } from "react-i18next"; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -17,6 +20,12 @@ import Typography from "@mui/material/Typography"; import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import LockIcon from "@mui/icons-material/Lock"; import NoteAddIcon from "@mui/icons-material/NoteAdd"; +import SyncIcon from "@mui/icons-material/Sync"; +import FlashOnIcon from '@mui/icons-material/FlashOn'; +import GridViewIcon from '@mui/icons-material/GridView'; +import ViewListIcon from '@mui/icons-material/ViewList'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; import visuallyHidden from "@mui/utils/visuallyHidden"; @@ -29,17 +38,22 @@ import OcrMenu from 'Components/OcrMenu/OcrMenu'; import LayoutMenu from 'Components/LayoutMenu/LayoutMenu'; import EditingMenu from 'Components/EditingMenu/EditingMenu'; import FolderMenu from 'Components/Form/FolderMenu'; +import SyncMenu from 'Components/Form/SyncMenu'; import OcrPopup from 'Components/Form/OcrPopup'; import DeletePopup from 'Components/Form/DeletePopup'; import PrivateSpaceMenu from 'Components/Form/PrivateSpaceMenu'; import DocumentRow from "./DocumentRow"; import FolderRow from "./FolderRow"; +import DocumentCard from "./DocumentCard"; +import FolderCard from "./FolderCard"; +import FileGridView from "./FileGridView"; import ReturnButton from './ReturnButton'; +import { MODEL, UN_ARMS, STJ } from 'App'; dayjs.extend(customParseFormat); const UPDATE_PERIOD_SECONDS = 15; -const UPLOAD_UPDATE_SECONDS = 5; +const UPLOAD_UPDATE_SECONDS = 2; // More frequent updates during upload (was 5) const STUCK_CHECK_PERIOD_SECONDS = 2 * 60; // check for stuck uploads every 2 minutes const STUCK_UPLOAD_TIMEOUT_MINUTES = 4; // files still not ready for OCR after these minutes are considered stuck @@ -76,11 +90,15 @@ class FileExplorer extends React.Component { components: [], order: "asc", orderBy: "name", + viewMode: localStorage.getItem('fileViewMode') || 'grid', // 'grid' or 'list' fetched: false, } + + this.handleViewModeChange = this.handleViewModeChange.bind(this); this.folderMenu = React.createRef(); + this.syncMenu = React.createRef(); this.ocrPopup = React.createRef(); this.deletePopup = React.createRef(); this.privateSpaceMenu = React.createRef(); @@ -104,11 +122,14 @@ class FileExplorer extends React.Component { this.deleteItem = this.deleteItem.bind(this); this.getOriginalFile = this.getOriginalFile.bind(this); this.getDocument = this.getDocument.bind(this); + this.getDocumentUncompressed = this.getDocumentUncompressed.bind(this); this.getEntities = this.getEntities.bind(this); this.requestEntities = this.requestEntities.bind(this); this.getImages = this.getImages.bind(this); this.editText = this.editText.bind(this); this.performOCR = this.performOCR.bind(this); + this.cancelOCR = this.cancelOCR.bind(this); + this.cancelFolderOCR = this.cancelFolderOCR.bind(this); this.configureOCR = this.configureOCR.bind(this); this.indexFile = this.indexFile.bind(this); this.removeIndexFile = this.removeIndexFile.bind(this); @@ -126,12 +147,52 @@ class FileExplorer extends React.Component { // functions for menus this.fetchFiles = this.fetchFiles.bind(this); + + // keyboard shortcuts + this.setupKeyboardShortcuts = this.setupKeyboardShortcuts.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + setupKeyboardShortcuts() { + // Setup keyboard event listeners + document.addEventListener('keydown', this.handleKeyDown); + } + + handleKeyDown(event) { + // Skip if in menu or typing in input field + if (this.props.ocrMenu || this.props.layoutMenu || this.props.editingMenu) return; + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return; + + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const modifier = isMac ? event.metaKey : event.ctrlKey; + + // Ctrl/Cmd + U: Upload file + if (modifier && event.key === 'u') { + event.preventDefault(); + if (this.props.current_folder !== "" || this.props._private) { + this.createFile(); + } + } + + // Ctrl/Cmd + N: New folder + if (modifier && event.key === 'n') { + event.preventDefault(); + this.createFolder(); + } + + // Escape: Close menus if any + if (event.key === 'Escape') { + // Will be handled by individual menu components + } } componentDidMount() { // Fetch the files and info from the server this.fetchFileSystem(); + // Setup keyboard shortcuts + this.setupKeyboardShortcuts(); + // Update the info every UPDATE_PERIOD_SECONDS seconds this.createFetchInfoInterval(); @@ -164,7 +225,9 @@ class FileExplorer extends React.Component { || this.state.files !== prevState.files) { // created/deleted document or folder this.displayFileSystem(); } else if (this.state.info !== prevState.info) { // fetched updated info - this.updateInfo(); + // Regenerate components for both views to show updated status + // Refs don't work because components are pre-created and stored in state + this.displayFileSystem(); } } @@ -234,12 +297,14 @@ class FileExplorer extends React.Component { * Call after actions that create or delete documents/folders. */ fetchFiles() { + const requestPath = this.props._private + ? this.props.spaceId + '/' + this.props.current_folder + : this.props.current_folder; + axios.get(API_URL + '/files', { params: { _private: this.props._private, - path: (this.props._private - ? this.props.spaceId + '/' + this.props.current_folder - : this.props.current_folder) + path: requestPath } }) .then(response => { @@ -248,9 +313,13 @@ class FileExplorer extends React.Component { } const files = response.data['files']; const info = response.data["info"]; - this.setState({files: files, info: info, updateCount: 0}); + this.setState({files: files, info: info, updateCount: 0}, () => { + // Ensure the file list is refreshed after state update + this.displayFileSystem(); + }); }) .catch(err => { + console.error('[fetchFiles] Error:', err); this.storageMenu.current.openWithMessage(err.message); }); } @@ -279,6 +348,70 @@ class FileExplorer extends React.Component { throw new Error("Não foi possível obter os dados do servidor."); } const info = response.data["info"]; + + // Check if any files are processing (uploading, preparing, or OCR) + // If so, start the monitoring interval if not already running + let anyProcessing = false; + for (const [path, fileInfo] of Object.entries(info)) { + if (fileInfo.type === "file") { + const isUploading = fileInfo.stored !== true; + const isOCRing = fileInfo.ocr && + fileInfo.pages && + fileInfo.ocr.progress < fileInfo.pages; + + if (isUploading || isOCRing) { + anyProcessing = true; + break; + } + } + } + + // Start monitoring interval if needed + if (anyProcessing && !this.uploadingCheckInterval) { + this.uploadingCheckInterval = setInterval(() => { + axios.get(API_URL + '/info', { + params: { + _private: this.props._private, + path: (this.props._private + ? this.props.spaceId + : this.props.current_folder) + } + }) + .then(response => { + if (response.status !== 200) { + throw new Error("Não foi possível obter os dados do servidor."); + } + const info = response.data["info"]; + + // Check if any files are still uploading, preparing, or performing OCR + let anyProcessing = false; + for (const [path, fileInfo] of Object.entries(info)) { + if (fileInfo.type === "file") { + // Check if uploading/preparing + const isUploading = fileInfo.stored !== true; + // Check if OCR is in progress + const isOCRing = fileInfo.ocr && + fileInfo.pages && + fileInfo.ocr.progress < fileInfo.pages; + + if (isUploading || isOCRing) { + anyProcessing = true; + break; + } + } + } + + // If no files are processing, clear the interval + if (!anyProcessing) { + clearInterval(this.uploadingCheckInterval); + this.uploadingCheckInterval = null; + } + + this.setState({info: info}); + }); + }, 1000 * UPLOAD_UPDATE_SECONDS); + } + this.setState({info: info, updateCount: 0}); }) .catch(err => { @@ -293,6 +426,7 @@ class FileExplorer extends React.Component { if (this.props.ocrMenu || this.props.layoutMenu || this.props.editingMenu) return; // update info of rows for current folder's contents this.rowRefs.forEach(ref => { + if (!ref.current) return; const filename = ref.current.props.name; const rowInfo = this.getInfo(filename); ref.current.updateInfo(rowInfo); @@ -306,15 +440,40 @@ class FileExplorer extends React.Component { clearInterval(this.stuckInterval); if (this.uploadingCheckInterval) clearInterval(this.uploadingCheckInterval); + // Remove keyboard event listener + document.removeEventListener('keydown', this.handleKeyDown); } /** * Open the folder menu */ createFolder() { + console.log("[FileSystem] createFolder called"); + let path = this.props.current_folder; if (this.props._private) { path = this.props.spaceId + '/' + path } - this.folderMenu.current.openMenu(path); + + console.log("[FileSystem] folderMenu ref:", this.folderMenu); + + if (this.folderMenu.current) { + console.log("[FileSystem] Calling openMenu() with path:", path); + this.folderMenu.current.openMenu(path); + } else { + console.warn("[FileSystem] FolderMenu ref is null!"); + } + } + + /** + * Open the sync menu to import external files + */ + openSyncMenu() { + console.log("[FileSystem] openSyncMenu called"); + let path = this.props.current_folder; + if (this.props._private) { path = this.props.spaceId + '/' + path } + + if (this.syncMenu.current) { + this.syncMenu.current.openMenu(path); + } } showStorageForm(errorMessage) { @@ -328,6 +487,63 @@ class FileExplorer extends React.Component { this.ocrPopup.current.openMenu(path, filename, ocrTargetIsFolder, alreadyOcr, customConfig); } + cancelOCR(filename) { + console.log('🚫 cancelOCR called with filename:', filename); + + let path = this.props.current_folder; + if (this.props._private) { path = this.props.spaceId + '/' + path } + + const files_path = path ? `${path}/${filename}` : filename; + + console.log('🚫 Sending cancel request with files_path:', files_path); + console.log('🚫 API URL:', API_URL); + + axios.post(`${API_URL}/cancel-ocr`, { files_path }) + .then(response => { + console.log('🚫 Cancel response:', response.data); + if (response.data.success) { + this.successNot.current.openNotif(this.props.t('ocr cancelled')); + this.fetchFiles(); + } else { + this.errorNot.current.openNotif(this.props.t('error') + ': ' + response.data.error); + } + }) + .catch(err => { + console.error('🚫 Error cancelling OCR:', err); + console.error('🚫 Error details:', err.response); + this.errorNot.current.openNotif(this.props.t('error canceling ocr')); + }); + } + + cancelFolderOCR(foldername) { + console.log('🚫 cancelFolderOCR called with foldername:', foldername); + + let path = this.props.current_folder; + if (this.props._private) { path = this.props.spaceId + '/' + path } + + const files_path = path ? `${path}/${foldername}` : foldername; + + console.log('🚫 Sending cancel folder request with files_path:', files_path); + console.log('🚫 API URL:', API_URL); + + axios.post(`${API_URL}/cancel-folder-ocr`, { files_path }) + .then(response => { + console.log('🚫 Cancel folder response:', response.data); + if (response.data.success) { + const message = `${this.props.t('ocr cancelled')} (${response.data.cancelled_documents} ${this.props.t('document')}(s))`; + this.successNot.current.openNotif(message); + this.fetchFiles(); + } else { + this.errorNot.current.openNotif(this.props.t('error') + ': ' + response.data.error); + } + }) + .catch(err => { + console.error('🚫 Error cancelling folder OCR:', err); + console.error('🚫 Error details:', err.response); + this.errorNot.current.openNotif(this.props.t('error canceling ocr')); + }); + } + sendChunk(i, chunk, fileName, _totalCount, _fileID, uniquePreventExit) { let path = this.props.current_folder; if (this.props._private) { path = this.props.spaceId + '/' + path } @@ -348,7 +564,7 @@ class FileExplorer extends React.Component { if (data['success']) { // Update upload status every UPLOAD_UPDATE_SECONDS seconds if (!this.uploadingCheckInterval) { - this.uploadingCheckInterval = setInterval((fileName) => { + this.uploadingCheckInterval = setInterval(() => { axios.get(API_URL + '/info', { params: { _private: this.props._private, @@ -362,19 +578,42 @@ class FileExplorer extends React.Component { throw new Error("Não foi possível obter os dados do servidor."); } const info = response.data["info"]; - const uploadInfo = this.getInfo(fileName) - // If upload is finished, end interval - if (uploadInfo["stored"]) { + + // Check if any files are still uploading, preparing, or performing OCR + let anyProcessing = false; + for (const [path, fileInfo] of Object.entries(info)) { + if (fileInfo.type === "file") { + // Check if uploading/preparing + const isUploading = fileInfo.stored !== true; + // Check if OCR is in progress + const isOCRing = fileInfo.ocr && + fileInfo.pages && + fileInfo.ocr.progress < fileInfo.pages; + + if (isUploading || isOCRing) { + anyProcessing = true; + break; + } + } + } + + // If no files are processing, clear the interval + if (!anyProcessing) { clearInterval(this.uploadingCheckInterval); + this.uploadingCheckInterval = null; } + this.setState({info: info}); - }); - }, 1000 * UPLOAD_UPDATE_SECONDS, fileName); + }, 1000 * UPLOAD_UPDATE_SECONDS); } if (i + 1 === _totalCount) { window.removeEventListener('beforeunload', uniquePreventExit); + // Refresh file list after last chunk is uploaded to ensure proper display + setTimeout(() => { + this.fetchFiles(); + }, 1000); } } else { this.storageMenu.current.openWithMessage(data.error); @@ -382,7 +621,7 @@ class FileExplorer extends React.Component { }) .catch(error => { // TODO: give feedback to user on communication error - this.sendChunk(i, chunk, fileName, _totalCount, _fileID); + this.sendChunk(i, chunk, fileName, _totalCount, _fileID, uniquePreventExit); }); } @@ -444,9 +683,6 @@ class FileExplorer extends React.Component { window.addEventListener('beforeunload', uniquePreventExit); fileName = data["filename"]; // update filename if server changed it due to name collisions - //// Update list of files on screen after upload of first chunk - this.fetchFiles(); - // Send chunks let startChunk = 0; let endChunk = chunkSize; @@ -456,6 +692,12 @@ class FileExplorer extends React.Component { endChunk = endChunk + chunkSize; this.sendChunk(i, chunk, fileName, _totalCount, _fileID, uniquePreventExit); } + + //// Update list of files on screen after first chunk is sent + // Add a delay to ensure backend has started processing the first chunk + setTimeout(() => { + this.fetchFiles(); + }, 500); } else { this.storageMenu.current.openWithMessage(data.error); } @@ -469,12 +711,33 @@ class FileExplorer extends React.Component { * Export the .txt or .pdf file */ getDocument(type, file, extension, suffix="") { - this.successNot.current.openNotif("A transferência do ficheiro começou, por favor aguarde"); + this.successNot.current.openNotif(this.props.t("file download started")); + + let path = this.props.current_folder + '/' + file; + if (this.props._private) { path = this.props.spaceId + '/' + path } + + fetch(API_URL + '/get_' + type + '?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { + method: 'GET' + }) + .then(response => {return response.blob()}) + .then(data => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(data); + + const basename = file.split('.').slice(0, -1).join('.'); + a.download = basename + '_ocr' + suffix + '.' + extension; + a.click(); + a.remove(); + }); + } + + getDocumentUncompressed(type, file, extension, suffix="") { + this.successNot.current.openNotif(this.props.t("file download started")); let path = this.props.current_folder + '/' + file; if (this.props._private) { path = this.props.spaceId + '/' + path } - fetch(API_URL + '/get_' + type + '?_private=' + this.props._private + '&path=' + path, { + fetch(API_URL + '/get_' + type + '?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { method: 'GET' }) .then(response => {return response.blob()}) @@ -490,12 +753,12 @@ class FileExplorer extends React.Component { } getEntities(file) { - this.successNot.current.openNotif("A transferência do ficheiro começou, por favor aguarde"); + this.successNot.current.openNotif(this.props.t("file download started")); let path = this.props.current_folder + '/' + file; if (this.props._private) { path = this.props.spaceId + '/' + path } - fetch(API_URL + '/get_entities?_private=' + this.props._private + '&path=' + path, { + fetch(API_URL + '/get_entities?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { method: 'GET' }) .then(response => {return response.blob()}) @@ -516,7 +779,7 @@ class FileExplorer extends React.Component { let path = this.props.current_folder + '/' + file; if (this.props._private) { path = this.props.spaceId + '/' + path } - fetch( API_URL + '/request_entities?_private=' + this.props._private + '&path=' + path, { + fetch( API_URL + '/request_entities?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { method: 'GET' }) .then(response => {return response.json()}) @@ -547,7 +810,7 @@ class FileExplorer extends React.Component { return response.json(); } - this.successNot.current.setMessage("O seu download vai começar em breves momentos.") + this.successNot.current.setMessage(this.props.t("download starting")) this.successNot.current.open(); return response.blob() }) @@ -569,12 +832,12 @@ class FileExplorer extends React.Component { */ getOriginalFile(file) { - this.successNot.current.openNotif("A transferência do ficheiro começou, por favor aguarde"); + this.successNot.current.openNotif(this.props.t("file download started")); let path = this.props.current_folder + '/' + file; if (this.props._private) { path = this.props.spaceId + '/' + path } - fetch(API_URL + '/get_original?_private=' + this.props._private + '&path=' + path, { + fetch(API_URL + '/get_original?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { method: 'GET' }) .then(response => {return response.blob()}) @@ -593,12 +856,12 @@ class FileExplorer extends React.Component { * Export the .zip file */ getImages(file) { - this.successNot.current.openNotif("A transferência do ficheiro começou, por favor aguarde"); + this.successNot.current.openNotif(this.props.t("file download started")); let path = this.props.current_folder + '/' + file; if (this.props._private) { path = this.props.spaceId + '/' + path } - fetch(API_URL + '/get_images?_private=' + this.props._private + '&path=' + path, { + fetch(API_URL + '/get_images?_private=' + this.props._private + '&path=' + encodeURIComponent(path), { method: 'GET' }) .then(response => {return response.blob()}) @@ -616,6 +879,16 @@ class FileExplorer extends React.Component { * Open the delete menu */ deleteItem(filename) { + console.log('deleteItem called with:', filename); + console.log('deletePopup ref:', this.deletePopup); + console.log('deletePopup.current:', this.deletePopup.current); + + if (!this.deletePopup || !this.deletePopup.current) { + console.error('DeletePopup ref is not available!'); + this.errorNot.current.openNotif('Error: Delete popup is not available. Please refresh the page.'); + return; + } + let path = this.props.current_folder; if (this.props._private) { path = this.props.spaceId + '/' + path } this.deletePopup.current.openMenu(path, filename); @@ -764,8 +1037,8 @@ class FileExplorer extends React.Component { sizeB = Number(sizeB[0]) * (sizeMap[sizeB[1]] || 1); return order * (sizeA - sizeB); } else { - let sizeA = a.props.info?.["total_size"]?.split(" ") ?? _zerobytes; - let sizeB = b.props.info?.["total_size"]?.split(" ") ?? _zerobytes; + let sizeA = a.props.info?.["size"]?.split(" ") ?? _zerobytes; + let sizeB = b.props.info?.["size"]?.split(" ") ?? _zerobytes; sizeA = Number(sizeA[0]) * (sizeMap[sizeA[1]] || 1); sizeB = Number(sizeB[0]) * (sizeMap[sizeB[1]] || 1); return order * (sizeA - sizeB); @@ -791,60 +1064,96 @@ class FileExplorer extends React.Component { } } + handleViewModeChange(newMode) { + this.setState({ viewMode: newMode }, () => { + this.displayFileSystem(); + }); + localStorage.setItem('fileViewMode', newMode); + } + displayFileSystem() { /** * Iterate the contents of the folder and build the components */ if (this.props.ocrMenu || this.props.layoutMenu || this.props.editingMenu) return; - + this.rowRefs = []; const items = []; + const viewMode = this.state.viewMode; for (let item of this.sortContents(this.getPathContents())) { let ref = React.createRef(); this.rowRefs.push(ref); - if (typeof item === 'string' || item instanceof String) { - items.push( - - ) + const isDocument = typeof item === 'string' || item instanceof String; + const itemName = isDocument ? item : Object.keys(item)[0]; + + // Common props for both view modes + const itemInfo = this.getInfo(itemName); + // Include stored status/progress and OCR progress in key to force re-render when they change + const ocrProgress = itemInfo?.ocr?.progress || 0; + const storedKey = typeof itemInfo?.stored === 'number' ? Math.floor(itemInfo.stored / 5) : itemInfo?.stored || 'unknown'; + const itemKey = this.props.current_folder + "/" + itemName + "/" + storedKey + "/" + ocrProgress; + const commonProps = { + ref: ref, + key: itemKey, + name: itemName, + info: itemInfo, + _private: this.props._private, + deleteItem: this.deleteItem, + performOCR: this.performOCR, + cancelOCR: this.cancelOCR, + configureOCR: this.configureOCR, + }; + + if (isDocument) { + // Construct thumbnail path correctly, avoiding double slashes + const pathParts = []; + if (this.props._private && this.props.spaceId) { + pathParts.push(this.props.spaceId); + } + if (this.props.current_folder) { + pathParts.push(this.props.current_folder); + } + pathParts.push(itemName); + const basePath = pathParts.join('/'); + + const docProps = { + ...commonProps, + thumbnails: { + small: `${basePath}/_thumbnails/${itemName}_128.thumbnail`, + large: `${basePath}/_thumbnails/${itemName}_600.thumbnail`, + }, + enterDocument: this.enterFolder, + getOriginalFile: this.getOriginalFile, + getDocument: this.getDocument, + getDocumentUncompressed: this.getDocumentUncompressed, + getEntities: this.getEntities, + requestEntities: this.requestEntities, + getImages: this.getImages, + editText: this.editText, + indexFile: this.props._private ? null : this.indexFile, + removeIndexFile: this.props._private ? null : this.removeIndexFile, + createLayout: this.createLayout, + }; + + if (viewMode === 'grid') { + items.push(); + } else { + items.push(); + } } else { - const key = Object.keys(item)[0]; - items.push( - - ) + const folderProps = { + ...commonProps, + cancelFolderOCR: this.cancelFolderOCR, + enterFolder: this.enterFolder, + }; + + if (viewMode === 'grid') { + items.push(); + } else { + items.push(); + } } } this.setState({components: items}); @@ -905,7 +1214,7 @@ class FileExplorer extends React.Component { width: "fit-content", }} > - Nome + {this.props.t("name")} {this.state.orderBy === "name" ? ( {this.state.order === 'desc' ? 'ordem descendente' : 'ordem ascendente'} @@ -925,7 +1234,7 @@ class FileExplorer extends React.Component { width: "fit-content", }} > - Detalhes + {this.props.t("details")} {this.state.orderBy === "details" ? ( {this.state.order === 'desc' ? 'ordem descendente' : 'ordem ascendente'} @@ -944,7 +1253,7 @@ class FileExplorer extends React.Component { flexWrap: "wrap", }} > - Tamanho + {this.props.t("size")} {this.state.orderBy === "size" ? ( {this.state.order === 'desc' ? 'ordem descendente' : 'ordem ascendente'} @@ -963,7 +1272,7 @@ class FileExplorer extends React.Component { flexWrap: "wrap", }} > - Data de criação + {this.props.t("date of creation")} {this.state.orderBy === "dateCreated" ? ( {this.state.order === 'desc' ? 'ordem descendente' : 'ordem ascendente'} @@ -973,7 +1282,7 @@ class FileExplorer extends React.Component { - Estado do Processo + {this.props.t("process state")} @@ -1123,133 +1432,189 @@ class FileExplorer extends React.Component { return ( <> + { this.props.ocrMenu - ? - : this.props.layoutMenu - ? - : this.props.editingMenu - ? - : - <> - - - - - + isFolder={this.props.ocrTargetIsFolder} + isSinglePage={this.props.ocrTargetIsSinglePage} + customConfig={this.props.customConfig} + setCurrentCustomConfig={this.props.setCurrentCustomConfig} + closeOCRMenu={this.closeOCRMenu} + showStorageForm={this.showStorageForm}/> + : this.props.layoutMenu + ? + : this.props.editingMenu + ? + : + <> + + + + + + + + + {MODEL !== STJ && ( + + )} + - - - - {this.props.spaceId - ? - : - } - - - - - - - - - - { - this.props._private && this.state.fetched - ? - : null - } + + + + {this.props.spaceId + ? + : + } + + + + + + + + + + + { + this.props._private && this.state.fetched + ? + : null + } + + {/* View Mode Toggle */} + + + + this.state.viewMode !== 'grid' && this.handleViewModeChange('grid')} + className={`view-toggle-button ${this.state.viewMode === 'grid' ? 'active' : ''}`} + > + + + + + this.state.viewMode !== 'list' && this.handleViewModeChange('list')} + className={`view-toggle-button ${this.state.viewMode === 'list' ? 'active' : ''}`} + > + + + + + - { - this.generateTable() - } - - + { + this.state.viewMode === 'grid' + ? + : this.generateTable() + } + + } ); @@ -1281,4 +1646,4 @@ FileExplorer.defaultProps = { exitMenus: null } -export default FileExplorer; +export default withTranslation()(FileExplorer); diff --git a/website/src/Components/FileSystem/FolderCard.js b/website/src/Components/FileSystem/FolderCard.js new file mode 100644 index 00000000..bd066f26 --- /dev/null +++ b/website/src/Components/FileSystem/FolderCard.js @@ -0,0 +1,345 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Tooltip from '@mui/material/Tooltip'; +import CircularProgress from '@mui/material/CircularProgress'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import SettingsIcon from '@mui/icons-material/Settings'; +import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; +import FolderIcon from '@mui/icons-material/Folder'; +import DescriptionIcon from '@mui/icons-material/Description'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import CancelIcon from '@mui/icons-material/Cancel'; + +import { withTranslation } from "react-i18next"; +import OcrIcon from 'Components/CustomIcons/OcrIcon'; + +class FolderCard extends React.Component { + constructor(props) { + super(props); + this.state = { + info: props.info, + contextMenu: null, + isHovered: false, + }; + } + + updateInfo(info) { + this.setState({ info: info }); + } + + componentDidUpdate(prevProps) { + if (prevProps.info !== this.props.info && this.props.info !== null) { + this.setState({ info: this.props.info }); + } + } + + handleOptionsClick(event) { + event.stopPropagation(); + this.setState({ + contextMenu: this.state.contextMenu === null + ? { anchorEl: event.currentTarget } + : null + }); + } + + handleContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ + contextMenu: this.state.contextMenu === null + ? { mouseX: event.clientX + 2, mouseY: event.clientY - 6 } + : null + }); + } + + handleCloseContextMenu() { + this.setState({ contextMenu: null }); + } + + folderClicked() { + this.props.enterFolder(this.props.name); + } + + performOCR(e, usingCustomConfig) { + e.stopPropagation(); + this.handleCloseContextMenu(); + const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; + this.props.performOCR(this.props.name, true, true, customConfig); + } + + configureOCR(e, usingCustomConfig) { + e.stopPropagation(); + this.handleCloseContextMenu(); + const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; + this.props.configureOCR(this.props.name, true, false, customConfig); + } + + cancelFolderOCR(e) { + e.stopPropagation(); + this.handleCloseContextMenu(); + this.props.cancelFolderOCR(this.props.name); + } + + delete(e) { + e.stopPropagation(); + this.handleCloseContextMenu(); + this.props.deleteItem(this.props.name); + } + + getQueueStatusBadge() { + const queueStatus = this.state.info?.["queue_status"]; + if (!queueStatus) return null; + + if (queueStatus.state === "active") { + return ( + + + {this.props.t("queue.processing")} + + ); + } + + if (queueStatus.state === "queued") { + const position = queueStatus.position || 0; + return ( + + {this.props.t("queue.pending")} (#{position}) + + ); + } + + if (queueStatus.state === "finished") { + return ( + + ✓ {this.props.t("queue.finished")} + + ); + } + + return null; + } + + render() { + if (!this.state.info) { + return ( + + ); + } + + const contents = this.state.info?.["contents"]; + const nDocs = Number(contents?.["documents"]); + const nSubfolders = Number(contents?.["subfolders"]); + const size = this.state.info?.["size"]; + const usingCustomConfig = this.state.info?.["config"] && this.state.info["config"] !== "default"; + + return ( + <> + this.folderClicked()} + onContextMenu={(e) => this.handleContextMenu(e)} + onMouseEnter={() => this.setState({ isHovered: true })} + onMouseLeave={() => this.setState({ isHovered: false })} + > + + {this.state.isHovered ? ( + + ) : ( + + )} + {this.getQueueStatusBadge()} + {usingCustomConfig && ( + + + + )} + + + + {this.props.name} + + + {nDocs > 0 && ( + + + {nDocs} {this.props.t(nDocs === 1 ? "document" : "documents")} + + )} + {nSubfolders > 0 && ( + + + {nSubfolders} {this.props.t(nSubfolders === 1 ? "folder" : "folders")} + + )} + + {size && ( + + {size} + + )} + + + + + this.handleOptionsClick(e)} + sx={{ + position: 'relative', + zIndex: 2, + '&:hover': { + backgroundColor: 'var(--accent-primary)', + color: 'white' + } + }} + > + + + + + + this.handleCloseContextMenu()} + anchorReference={this.state.contextMenu?.anchorEl ? "anchorEl" : "anchorPosition"} + anchorEl={this.state.contextMenu?.anchorEl} + anchorPosition={ + this.state.contextMenu?.mouseY + ? { top: this.state.contextMenu.mouseY, left: this.state.contextMenu.mouseX } + : undefined + } + > + this.folderClicked()}> + + {this.props.t("open folder")} + + + + + this.performOCR(e, usingCustomConfig)} + > + + {this.props.t("run ocr")} + + + + + this.configureOCR(e, usingCustomConfig)}> + {usingCustomConfig ? ( + + ) : ( + + )} + {this.props.t("config ocr")} + + + {this.state.info?.["queue_status"]?.state === "active" && ( + this.cancelFolderOCR(e)} sx={{ color: 'var(--red-600)' }}> + + {this.props.t("cancel ocr")} + + )} + + this.delete(e)} sx={{ color: 'var(--red-600)' }}> + + {this.props.t("delete")} + + + + ); + } +} + +FolderCard.defaultProps = { + name: "", + info: null, + enterFolder: null, + performOCR: null, + configureOCR: null, + cancelFolderOCR: null, + deleteItem: null, +}; + +export default withTranslation()(FolderCard); + + diff --git a/website/src/Components/FileSystem/FolderRow.js b/website/src/Components/FileSystem/FolderRow.js index 1b08928f..4028e48a 100644 --- a/website/src/Components/FileSystem/FolderRow.js +++ b/website/src/Components/FileSystem/FolderRow.js @@ -1,13 +1,18 @@ import React from 'react'; import Box from '@mui/material/Box'; +import {withTranslation} from "react-i18next"; + import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; +import CircularProgress from '@mui/material/CircularProgress'; import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import SettingsIcon from '@mui/icons-material/Settings'; import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; +import TuneIcon from '@mui/icons-material/Tune'; +import CancelIcon from '@mui/icons-material/Cancel'; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; @@ -30,6 +35,13 @@ class FolderRow extends React.Component { this.setState({info: info}); } + componentDidUpdate(prevProps) { + // Update state when props.info changes (e.g., after initial fetch) + if (prevProps.info !== this.props.info && this.props.info !== null) { + this.setState({ info: this.props.info }); + } + } + handleOptionsClick(event) { this.setState({ contextMenu: @@ -93,12 +105,28 @@ class FolderRow extends React.Component { this.props.configureOCR(this.props.name, true, false, customConfig); } + cancelFolderOCR(e) { + e.stopPropagation(); + this.props.cancelFolderOCR(this.props.name); + } + delete(e) { e.stopPropagation(); this.props.deleteItem(this.props.name); } render() { + // Show loading state while info is being fetched + if (!this.state.info) { + return ( + + + + + + ); + } + const contents = this.state.info?.["contents"]; const nDocs = Number(contents?.["documents"]); const nSubfolders = Number(contents?.["subfolders"]); @@ -118,7 +146,7 @@ class FolderRow extends React.Component { > -  Fazer OCR +  {this.props.t("run ocr")}
@@ -146,16 +174,27 @@ class FolderRow extends React.Component { > {usingCustomConfig ? : } -  {usingCustomConfig ? "Editar Configuração" : "Configurar OCR"} +  {usingCustomConfig ? this.props.t("alter existing config") : this.props.t("config ocr")} + {this.state.info?.["queue_status"]?.state === "active" && ( + this.cancelFolderOCR(e)} + > + + + +  {this.props.t("cancel ocr")} + + )} + this.delete(e)} > -  Apagar +  {this.props.t("delete")} @@ -165,7 +204,7 @@ class FolderRow extends React.Component { > this.handleOptionsClick(e)} > @@ -187,14 +226,25 @@ class FolderRow extends React.Component { }}> {this.props.name} + {usingCustomConfig && ( + + )} - {nDocs} documento(s) + {nDocs} {this.props.t("document")}(s) {'\n'} - {nSubfolders} sub-pasta(s) + {nSubfolders} {this.props.t("sub-folder")}(s) @@ -210,9 +260,68 @@ class FolderRow extends React.Component { - - — - + {(() => { + const queueStatus = this.state.info?.["queue_status"]; + + if (!queueStatus) { + return ( + + + + + + ); + } + + if (queueStatus.state === "active") { + const duration = queueStatus.duration_seconds || 0; + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + return ( + + + + + {this.props.t("queue.processing")} + + + + ); + } + + if (queueStatus.state === "queued") { + const position = queueStatus.position || 0; + return ( + + + + {this.props.t("queue.pending")} (#{position}) + + + + ); + } + + if (queueStatus.state === "finished") { + return ( + + + + ✓ {this.props.t("queue.finished")} + + + + ); + } + + return ( + + + + + + ); + })()} ) } @@ -225,7 +334,8 @@ FolderRow.defaultProps = { enterFolder: null, performOCR: null, configureOCR: null, + cancelFolderOCR: null, deleteItem: null } -export default FolderRow; +export default withTranslation()(FolderRow); diff --git a/website/src/Components/FileSystem/ReturnButton.js b/website/src/Components/FileSystem/ReturnButton.js index cd1d6a25..d92bb2f5 100644 --- a/website/src/Components/FileSystem/ReturnButton.js +++ b/website/src/Components/FileSystem/ReturnButton.js @@ -1,10 +1,13 @@ import React from 'react'; +import {useTranslation} from "react-i18next"; + import Button from "@mui/material/Button"; import UndoIcon from "@mui/icons-material/Undo"; const ReturnButton = ({ disabled = false, returnFunction = null, sx = {} }) => { + const { t } = useTranslation(); return ( ); } diff --git a/website/src/Components/FileSystem/StaticFileRow.js b/website/src/Components/FileSystem/StaticFileRow.js index 80f7e6d5..de53d4bd 100644 --- a/website/src/Components/FileSystem/StaticFileRow.js +++ b/website/src/Components/FileSystem/StaticFileRow.js @@ -1,6 +1,7 @@ import React from 'react'; import Box from '@mui/material/Box'; import Button from "@mui/material/Button"; +import { withTranslation } from "react-i18next"; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; @@ -46,7 +47,7 @@ class StaticFileRow extends React.Component { {this.props.info["pages"] - ? this.props.info["pages"] + " página(s)" + ? this.props.info["pages"] + " " + this.props.t("page count") : "—" } @@ -81,4 +82,4 @@ StaticFileRow.defaultProps = { downloadFile: null, } -export default StaticFileRow; +export default withTranslation()(StaticFileRow); diff --git a/website/src/Components/Form/CheckboxList.js b/website/src/Components/Form/CheckboxList.js index 01a41190..cbd71f5e 100644 --- a/website/src/Components/Form/CheckboxList.js +++ b/website/src/Components/Form/CheckboxList.js @@ -45,13 +45,14 @@ function CheckboxList( error={error} component="fieldset" variant="standard" + sx={{ overflow: 'visible' }} > {title} - + {options.map((option, index) => { const order = showOrder ? checked.indexOf(option.value)+1 : -1; return ( - + {order > 0 ? {order}º  diff --git a/website/src/Components/Form/ConfirmActionPopup.js b/website/src/Components/Form/ConfirmActionPopup.js index 0a3e1bf5..77882e47 100644 --- a/website/src/Components/Form/ConfirmActionPopup.js +++ b/website/src/Components/Form/ConfirmActionPopup.js @@ -7,6 +7,7 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import ClickAwayListener from "@mui/material/ClickAwayListener"; +import { useTranslation } from "react-i18next"; import Notification from 'Components/Notifications/Notification'; @@ -38,6 +39,7 @@ function ConfirmActionPopup( submitCallback = null, // required cancelCallback = null, // required }) { + const { t } = useTranslation(); const successNotif = useRef(null); const errorNotif = useRef(null); @@ -72,7 +74,7 @@ function ConfirmActionPopup( sx={{border: '1px solid black', mt: '0.5rem'}} onClick={() => submitCallback()} > - Confirmar + {t("confirm")} diff --git a/website/src/Components/Form/DeletePopup.js b/website/src/Components/Form/DeletePopup.js index 219081dd..e5d068ed 100644 --- a/website/src/Components/Form/DeletePopup.js +++ b/website/src/Components/Form/DeletePopup.js @@ -64,8 +64,19 @@ class DeletePopup extends React.Component { deleteItem() { this.setState({ buttonDisabled: true }); - const path = this.state.path + '/' + this.state.filename; - fetch(API_URL + '/delete-path', { + // Handle empty path (root folder) + const path = this.state.path ? + `${this.state.path}/${this.state.filename}` : + this.state.filename; + const url = API_URL + '/delete-path'; + console.log('Deleting with URL:', url); + console.log('Delete - state.path:', this.state.path); + console.log('Delete - state.filename:', this.state.filename); + console.log('Delete - combined path:', path); + console.log('Delete - _private:', this.props._private); + console.log('API_URL:', API_URL); + + fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -75,7 +86,15 @@ class DeletePopup extends React.Component { "_private": this.props._private }) }) - .then(response => {return response.json()}) + .then(async response => { + console.log('Response status:', response.status, response.statusText); + const text = await response.text(); + console.log('Response body:', text); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return JSON.parse(text); + }) .then(data => { this.setState({ buttonDisabled: false }); if (data.success) { @@ -85,6 +104,11 @@ class DeletePopup extends React.Component { } else { this.errorNot.current.openNotif(data.error); } + }) + .catch(error => { + this.setState({ buttonDisabled: false }); + console.error('Delete error:', error); + this.errorNot.current.openNotif('Failed to delete: ' + error.message); }); } @@ -101,7 +125,7 @@ class DeletePopup extends React.Component { > - Tem a certeza que quer apagar {this.state.filename}? + {this.props.t("confirm delete")} {this.state.filename}? this.deleteItem()} > - Apagar + {this.props.t("delete")} @@ -134,6 +158,8 @@ DeletePopup.defaultProps = { _private: false, // functions: submitCallback: null, + // translation function passed from parent + t: (key) => key, } export default DeletePopup; diff --git a/website/src/Components/Form/FolderMenu.js b/website/src/Components/Form/FolderMenu.js index e7606372..b9b9e254 100644 --- a/website/src/Components/Form/FolderMenu.js +++ b/website/src/Components/Form/FolderMenu.js @@ -8,6 +8,8 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import i18n from "i18next"; + import Notification from 'Components/Notifications/Notification'; const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; @@ -62,7 +64,7 @@ class FolderMenu extends React.Component { createFolder() { if (this.state.textFieldValue.length === 0) { - this.errorNot.current.openNotif("Deve atribuir um nome à pasta"); + this.errorNot.current.openNotif(i18n.t("folder name required")); return; } this.setState({ buttonDisabled: true }); @@ -77,17 +79,29 @@ class FolderMenu extends React.Component { "_private": this.props._private }) }) - .then(response => {return response.json()}) - .then(data => { - this.setState({ buttonDisabled: false }); - if (data.success) { - this.successNot.current.openNotif("Pasta criada com sucesso"); - - this.closeMenu(this.props.submitCallback); - } else { - this.errorNot.current.openNotif(data.error); - } - }); + .then(response => {return response.json()}) + .then(data => { + this.setState({ buttonDisabled: false }); + if (data.success) { + this.successNot.current.openNotif(i18n.t("folder created successfully")); + + this.closeMenu(this.props.submitCallback); + } else { + this.errorNot.current.openNotif(data.error); + } + }); + } + + componentDidMount() { + console.log("%c[FolderMenu] Mounted", "color: green; font-weight: bold;"); + } + + componentWillUnmount() { + console.log("%c[FolderMenu] Unmounted", "color: red; font-weight: bold;"); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + console.log("%c[FolderMenu] Updated", "color: red; font-weight: bold;"); } render() { @@ -98,14 +112,14 @@ class FolderMenu extends React.Component { - Crie uma nova pasta + {i18n.t("create new folder")} { if (e.key === 'Enter') { this.createFolder() }}} variant="outlined" @@ -117,7 +131,7 @@ class FolderMenu extends React.Component { sx={{border: '1px solid black', mt: '0.5rem', mr: '1rem'}} onClick={() => this.createFolder()} > - Criar + {i18n.t("create")} this.closeMenu()}> diff --git a/website/src/Components/Form/InfoTooltip.js b/website/src/Components/Form/InfoTooltip.js new file mode 100644 index 00000000..57ea9a4e --- /dev/null +++ b/website/src/Components/Form/InfoTooltip.js @@ -0,0 +1,45 @@ +import React from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import IconButton from '@mui/material/IconButton'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; + +function InfoTooltip({ title, placement = "right" }) { + return ( + + + + + + ); +} + +export default InfoTooltip; diff --git a/website/src/Components/Form/OcrPopup.js b/website/src/Components/Form/OcrPopup.js index 52b1fd92..3f96047a 100644 --- a/website/src/Components/Form/OcrPopup.js +++ b/website/src/Components/Form/OcrPopup.js @@ -7,8 +7,19 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import ClickAwayListener from "@mui/material/ClickAwayListener"; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import Tooltip from '@mui/material/Tooltip'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormLabel from '@mui/material/FormLabel'; +import FormHelperText from '@mui/material/FormHelperText'; import Notification from 'Components/Notifications/Notification'; +import i18next from 'i18next'; const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; const style = { @@ -41,6 +52,10 @@ class OcrPopup extends React.Component { isFolder: false, alreadyOcr: false, customConfig: null, + + presetsList: [], + selectedPreset: "default", + configStrategy: "hybrid", } this.successNot = React.createRef(); @@ -55,6 +70,36 @@ class OcrPopup extends React.Component { // handler to close menu on click outside box this.handleClickOutsideMenu = this.handleClickOutsideMenu.bind(this); + this.fetchPresetsList = this.fetchPresetsList.bind(this); + this.handlePresetChange = this.handlePresetChange.bind(this); + this.handleStrategyChange = this.handleStrategyChange.bind(this); + } + + componentDidMount() { + this.fetchPresetsList(); + } + + fetchPresetsList() { + fetch(API_URL + '/presets-list') + .then(response => response.json()) + .then(data => { + // Add 'default' to the beginning if not already present + const presets = ['default', ...data]; + this.setState({ presetsList: presets }); + }) + .catch(err => { + console.error("Failed to fetch presets list:", err); + // Fallback to just default + this.setState({ presetsList: ['default'] }); + }); + } + + handlePresetChange(event) { + this.setState({ selectedPreset: event.target.value }); + } + + handleStrategyChange(event) { + this.setState({ configStrategy: event.target.value }); } handleClickOutsideMenu() { @@ -71,6 +116,8 @@ class OcrPopup extends React.Component { isFolder: ocrTargetIsFolder, alreadyOcr: alreadyOcr, customConfig: customConfig, + selectedPreset: "default", // Reset to default when opening + configStrategy: "hybrid", // Reset to hybrid when opening }); } @@ -82,6 +129,8 @@ class OcrPopup extends React.Component { isFolder: false, alreadyOcr: false, customConfig: null, + selectedPreset: "default", + configStrategy: "hybrid", }, callback); } @@ -95,8 +144,19 @@ class OcrPopup extends React.Component { "multiple": this.state.isFolder, "_private": this.props._private } + + // Priority: customConfig > selectedPreset if (this.state.customConfig) { body["config"] = this.state.customConfig; + } else if (this.state.selectedPreset && this.state.selectedPreset !== "default") { + // Send preset name as string to backend + body["config"] = this.state.selectedPreset; + } + // If selectedPreset is "default" or null, don't send config (use system default) + + // Send config strategy if folder OCR + if (this.state.isFolder) { + body["config_strategy"] = this.state.configStrategy; } fetch(API_URL + '/request-ocr', { @@ -109,23 +169,25 @@ class OcrPopup extends React.Component { .then(response => response.json()) .then(data => { if (data.success) { - this.successNot.current.openNotif(data.message); + this.successNot.current.openNotif(i18next.t(data.message)); } else { if (data.error) { this.props.showStorageForm(data.error); } else { - this.errorNot.current.openNotif(data.message); + this.errorNot.current.openNotif(i18next.t(data.message)); } } this.closeMenu(this.props.submitCallback); }) .catch(err => { - this.errorNot.current.openNotif("Não foi possível realizar o pedido.") + this.errorNot.current.openNotif(i18next.t("admin.request_failed")) }); } render() { + const hasCustomConfig = this.state.customConfig !== null; + return ( @@ -138,17 +200,79 @@ class OcrPopup extends React.Component { onClickAway={this.handleClickOutsideMenu} > - - Realizar OCR {this.state.isFolder ? 'da pasta' : 'do ficheiro'} {this.state.filename} + + {i18next.t("run ocr")} {this.state.isFolder ? i18next.t('of folder') : i18next.t('of document')} {this.state.filename} {this.state.alreadyOcr - &&

Irá perder os resultados e alterações anteriores!

+ && {i18next.t("lose results")} } + {hasCustomConfig && ( + + {i18next.t("custom config")} + + )} + + {!hasCustomConfig && ( + + {i18next.t("select ocr preset")} + + + )} + + {this.state.isFolder && ( + + {i18next.t("config strategy")} + + } + label={i18next.t("config strategy override")} + /> + } + label={i18next.t("config strategy respect")} + /> + } + label={i18next.t("config strategy hybrid")} + /> + + {i18next.t("config strategy hint")} + + )} + diff --git a/website/src/Components/Form/SyncMenu.js b/website/src/Components/Form/SyncMenu.js new file mode 100644 index 00000000..52d38593 --- /dev/null +++ b/website/src/Components/Form/SyncMenu.js @@ -0,0 +1,180 @@ +import React from 'react'; + +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Modal from '@mui/material/Modal'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import CircularProgress from '@mui/material/CircularProgress'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import FolderIcon from '@mui/icons-material/Folder'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; + +import i18n from "i18next"; + +import Notification from 'Components/Notifications/Notification'; + +const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; + +const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 450, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + borderRadius: 2 +}; + +const crossStyle = { + position: 'absolute', + top: '0.5rem', + right: '0.5rem' +}; + +class SyncMenu extends React.Component { + constructor(props) { + super(props); + this.state = { + open: false, + path: "", + loading: false, + result: null, + }; + + this.successNot = React.createRef(); + this.errorNot = React.createRef(); + } + + openMenu(path) { + this.setState({ path: path, open: true, loading: false, result: null }); + } + + closeMenu(callback = null) { + this.setState({ open: false, result: null }, callback); + } + + sync(recursive) { + this.setState({ loading: true, result: null }); + + fetch(API_URL + '/sync-inputs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "path": this.state.path, + "recursive": recursive, + "_private": this.props._private + }) + }) + .then(response => response.json()) + .then(data => { + this.setState({ loading: false }); + if (data.success) { + const importedCount = data.imported ? data.imported.length : 0; + if (importedCount > 0) { + this.setState({ result: data }); + this.successNot.current.openNotif( + i18n.t("sync_success").replace("{count}", importedCount) + ); + // Refresh file list after a short delay + setTimeout(() => { + this.closeMenu(this.props.submitCallback); + }, 1500); + } else { + this.successNot.current.openNotif(i18n.t("sync_no_new")); + setTimeout(() => { + this.closeMenu(); + }, 1500); + } + } else { + this.errorNot.current.openNotif(data.error || i18n.t("sync_error")); + } + }) + .catch(err => { + this.setState({ loading: false }); + this.errorNot.current.openNotif(i18n.t("sync_error")); + }); + } + + render() { + return ( + <> + this.closeMenu()} + aria-labelledby="sync-modal-title" + > + + this.closeMenu()}> + + + + + {i18n.t("sync_title")} + + + + {i18n.t("sync_description")} + + + {this.state.loading ? ( + + + + ) : this.state.result ? ( + + + {i18n.t("sync_success").replace("{count}", this.state.result.imported.length)} + + {this.state.result.skipped > 0 && ( + + {i18n.t("sync_skipped").replace("{count}", this.state.result.skipped)} + + )} + + ) : ( + + + + + )} + + + + + + + ); + } +} + +SyncMenu.defaultProps = { + _private: false, + submitCallback: null, +}; + +export default SyncMenu; + + + diff --git a/website/src/Components/ImmediateOCR/ImmediateOCR.js b/website/src/Components/ImmediateOCR/ImmediateOCR.js new file mode 100644 index 00000000..182f2ec8 --- /dev/null +++ b/website/src/Components/ImmediateOCR/ImmediateOCR.js @@ -0,0 +1,1061 @@ +import React from 'react'; +import axios from 'axios'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router'; + +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import LinearProgress from '@mui/material/LinearProgress'; +import Paper from '@mui/material/Paper'; +import Alert from '@mui/material/Alert'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import TextField from '@mui/material/TextField'; +import Switch from '@mui/material/Switch'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; + +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import FlashOnIcon from '@mui/icons-material/FlashOn'; +import DownloadIcon from '@mui/icons-material/Download'; +import DeleteIcon from '@mui/icons-material/Delete'; + +import CheckboxList from 'Components/Form/CheckboxList'; +import { tesseractLangList } from 'defaultOcrConfigs'; +import Footer from 'Components/Footer/Footer'; +import { MODEL, STJ } from 'App'; +import logoApp from "static/logoApp.png"; +import logoUN from "static/Logo_of_the_United_Nations.svg"; + +// Construct API URL, removing any double slashes +const apiPath = process.env.REACT_APP_API_URL || 'api'; +const cleanApiPath = apiPath.replace(/^\/+|\/+$/g, ''); // Remove leading/trailing slashes +const API_URL = `${window.location.protocol}//${window.location.host}/${cleanApiPath}`; + +class ImmediateOCR extends React.Component { + constructor(props) { + super(props); + this.state = { + // File management + uploadedFile: null, + docId: null, + + // Configuration + selectedLanguages: ['por'], + selectedPreset: 'default', + presetsList: ['default'], + + // Output formats + outputFormats: { + txt: true, + pdf: false, + pdf_indexed: false + }, + + // Compression setting + enableCompression: true, + compressionTargetDpi: 100, + compressionBgQuality: 40, + compressionFgQuality: 80, + + // Processing state + status: 'idle', // idle, uploading, processing, complete, error + progress: 0, + statusMessage: '', + errorMessage: '', + + // Results + availableResults: { + txt: false, + pdf: false, + pdf_indexed: false + }, + resultSizes: { + txt: null, + pdf: { compressed: null, uncompressed: null }, + pdf_indexed: { compressed: null, uncompressed: null } + } + }; + + this.fileInputRef = React.createRef(); + this.pollInterval = null; + + this.handleFileSelect = this.handleFileSelect.bind(this); + this.handleDrop = this.handleDrop.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.processFile = this.processFile.bind(this); + this.pollStatus = this.pollStatus.bind(this); + this.downloadResult = this.downloadResult.bind(this); + this.cleanup = this.cleanup.bind(this); + this.resetForm = this.resetForm.bind(this); + } + + componentWillUnmount() { + // Cleanup on page leave + if (this.state.docId) { + this.cleanup(); + } + if (this.pollInterval) { + clearInterval(this.pollInterval); + } + } + + handleFileSelect(event) { + const file = event.target.files[0]; + if (file) { + this.setState({ uploadedFile: file, errorMessage: '' }); + } + } + + handleDrop(event) { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + if (file) { + this.setState({ uploadedFile: file, errorMessage: '' }); + } + } + + handleDragOver(event) { + event.preventDefault(); + } + + setLanguages(checked) { + this.setState({ selectedLanguages: checked }); + } + + toggleOutputFormat(format) { + this.setState(prevState => ({ + outputFormats: { + ...prevState.outputFormats, + [format]: !prevState.outputFormats[format] + } + })); + } + + async processFile() { + const { + uploadedFile, selectedLanguages, selectedPreset, outputFormats, + enableCompression, + compressionTargetDpi, compressionBgQuality, compressionFgQuality + } = this.state; + + if (!uploadedFile) { + this.setState({ errorMessage: this.props.t('no file uploaded') }); + return; + } + + if (selectedLanguages.length === 0) { + this.setState({ errorMessage: this.props.t('language required') }); + return; + } + + const hasOutputSelected = Object.values(outputFormats).some(v => v); + if (!hasOutputSelected) { + this.setState({ errorMessage: this.props.t('output required') }); + return; + } + + // Clean up previous results if any + if (this.state.docId) { + await this.cleanup(); + } + + this.setState({ + status: 'uploading', + statusMessage: this.props.t('uploading'), + errorMessage: '' + }); + + try { + // Prepare config + const outputs = []; + if (outputFormats.txt) outputs.push('txt'); + if (outputFormats.pdf) outputs.push('pdf'); + if (outputFormats.pdf_indexed) outputs.push('pdf_indexed'); + + const formData = new FormData(); + formData.append('file', uploadedFile); + + // Send preset name or full config + if (selectedPreset && selectedPreset !== 'default') { + // Send preset name as string, but include compression settings separately + formData.append('config', selectedPreset); + // Add compression settings that override preset + formData.append('compress', enableCompression); + } else { + // Send complete config with all required fields including compression settings + const config = { + engine: "tesserocr", + lang: selectedLanguages, + outputs: outputs, + engineMode: 3, + segmentMode: 3, + thresholdMethod: 0, + compress: enableCompression, + compressionTargetDpi: Number(compressionTargetDpi), + compressionBgQuality: Number(compressionBgQuality), + compressionFgQuality: Number(compressionFgQuality), + compressionFlattenToJpeg: true // Always flatten to JPEG + }; + formData.append('config', JSON.stringify(config)); + } + + const response = await axios.post(API_URL + '/perform-ocr', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + if (response.data.success) { + this.setState({ + docId: response.data.doc_id, + status: 'processing', + statusMessage: this.props.t('processing document') + }); + + // Start polling for status + this.pollInterval = setInterval(this.pollStatus, 2000); + } else { + this.setState({ + status: 'error', + errorMessage: response.data.error || this.props.t('upload failed') + }); + } + } catch (error) { + console.error('Upload error:', error); + this.setState({ + status: 'error', + errorMessage: this.props.t('upload failed') + }); + } + } + + async pollStatus() { + const { docId, enableCompression, outputFormats } = this.state; + if (!docId) return; + + try { + const response = await axios.get(API_URL + '/check-status', { + params: { doc_id: docId } + }); + + const data = response.data; + + // Translate backend status messages + let statusMessage = data.status?.message || this.props.t('processing document'); + + // Check if message is a translation key and use current/total fields if present + if (statusMessage === 'processing ocr page' && data.status?.current && data.status?.total) { + statusMessage = this.props.t('processing ocr page', { current: data.status.current, total: data.status.total }); + } + else if (statusMessage === 'compressing file page' && data.status?.current && data.status?.total) { + statusMessage = this.props.t('compressing file page', { current: data.status.current, total: data.status.total }); + } + else if (statusMessage === 'adding text page' && data.status?.current && data.status?.total) { + statusMessage = this.props.t('adding text page', { current: data.status.current, total: data.status.total }); + } + // Handle translation keys without parameters + else if (statusMessage === 'processing ocr') { + statusMessage = this.props.t('processing ocr'); + } + else if (statusMessage === 'compressing file') { + statusMessage = this.props.t('compressing file'); + } + else if (statusMessage === 'adding text layer') { + statusMessage = this.props.t('adding text layer'); + } + else if (statusMessage === 'completed') { + statusMessage = this.props.t('completed'); + } + // Legacy: Check for OCR processing messages in Portuguese and translate them + else if (statusMessage.includes('A processar OCR')) { + if (statusMessage.match(/Página \d+\/\d+/)) { + const match = statusMessage.match(/Página (\d+)\/(\d+)/); + if (match) { + statusMessage = this.props.t('processing ocr page', { current: match[1], total: match[2] }); + } else { + statusMessage = this.props.t('processing ocr'); + } + } else { + statusMessage = this.props.t('processing ocr'); + } + } + // Legacy: Check for compression messages in Portuguese and translate them + else if (statusMessage.includes('A comprimir')) { + if (statusMessage.match(/Página \d+\/\d+/)) { + const match = statusMessage.match(/Página (\d+)\/(\d+)/); + if (match) { + statusMessage = this.props.t('compressing file page', { current: match[1], total: match[2] }); + } else if (statusMessage.includes('ficheiro original')) { + statusMessage = this.props.t('compressing file'); + } else if (statusMessage.includes('concluída')) { + statusMessage = this.props.t('compression complete'); + } else { + statusMessage = this.props.t('compressing pdf'); + } + } else if (statusMessage.includes('ficheiro original')) { + statusMessage = this.props.t('compressing file'); + } else if (statusMessage.includes('concluída')) { + statusMessage = this.props.t('compression complete'); + } else { + statusMessage = this.props.t('compressing pdf'); + } + } + // Legacy: Check for text layer messages and translate them + else if (statusMessage.includes('A adicionar texto')) { + if (statusMessage.match(/Página \d+\/\d+/)) { + const match = statusMessage.match(/Página (\d+)\/(\d+)/); + if (match) { + statusMessage = this.props.t('adding text page', { current: match[1], total: match[2] }); + } else { + statusMessage = this.props.t('adding text layer'); + } + } else if (statusMessage.includes('OCR')) { + statusMessage = this.props.t('adding text layer'); + } + } + // Legacy: Check for completed status + else if (statusMessage.includes('Concluído')) { + statusMessage = this.props.t('completed'); + } + + // Calculate progress percentage based on backend percentage if available + let progressPercent = 0; + const stage = data.status?.stage; + + // Use backend percentage if provided (new progress system) + if (data.status?.percentage !== undefined) { + progressPercent = data.status.percentage; + } else { + // Fallback to old calculation for backwards compatibility + const hasPdfOutput = outputFormats.pdf || outputFormats.pdf_indexed; + const willCompress = enableCompression && hasPdfOutput; + + if (stage === 'ocr') { + const ocrProgress = data.ocr?.progress || 0; + const totalPages = data.pages || 1; + const ocrPercent = (ocrProgress / totalPages) * 100; + + if (willCompress) { + progressPercent = Math.min(50, ocrPercent * 0.5); + } else { + progressPercent = Math.min(100, ocrPercent); + } + } else if (stage === 'compressing') { + const compressionProgress = data.status?.progress || 0; + progressPercent = 50 + (compressionProgress * 0.5); + } else if (stage === 'exporting') { + progressPercent = willCompress ? 95 : 90; + } else if (stage === 'post-ocr') { + progressPercent = 100; + } else { + progressPercent = data.ocr?.progress || 0; + } + } + + this.setState({ + progress: Math.round(progressPercent), + statusMessage: statusMessage + }); + + // Check if complete + const resultsComplete = { + txt: data.txt?.complete || false, + pdf: data.pdf?.complete || false, + pdf_indexed: data.pdf_indexed?.complete || false + }; + + // Get file sizes from response + const resultSizes = { + txt: data.txt?.size || null, + pdf: { + compressed: data.pdf?.compressed_size || data.pdf?.size || null, + uncompressed: data.pdf?.uncompressed_size || null + }, + pdf_indexed: { + compressed: data.pdf_indexed?.compressed_size || data.pdf_indexed?.size || null, + uncompressed: data.pdf_indexed?.uncompressed_size || null + } + }; + + const allComplete = Object.entries(this.state.outputFormats) + .filter(([_, selected]) => selected) + .every(([format, _]) => resultsComplete[format]); + + if (allComplete && stage !== 'ocr' && stage !== 'compressing') { + // Processing complete + clearInterval(this.pollInterval); + this.pollInterval = null; + this.setState({ + status: 'complete', + progress: 100, + availableResults: resultsComplete, + resultSizes: resultSizes, + statusMessage: this.props.t('ocr complete') + }); + } else if (stage === 'error') { + // Error occurred + clearInterval(this.pollInterval); + this.pollInterval = null; + + // Show detailed error if available + let errorMsg = data.status?.message || this.props.t('processing failed'); + if (data.ocr?.exceptions) { + errorMsg += ` - ${data.ocr.exceptions}`; + } + console.error('OCR Error:', errorMsg); + + this.setState({ + status: 'error', + errorMessage: errorMsg + }); + } + } catch (error) { + console.error('Status check error:', error); + clearInterval(this.pollInterval); + this.pollInterval = null; + this.setState({ + status: 'error', + errorMessage: this.props.t('processing failed') + }); + } + } + + downloadResult(type) { + const { docId } = this.state; + if (!docId) return; + + const url = `${API_URL}/get-result?doc_id=${docId}&type=${type}`; + window.open(url, '_blank'); + } + + async cleanup() { + const { docId } = this.state; + if (!docId) return; + + try { + await axios.post(API_URL + '/delete-results', + { doc_id: docId }, + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('Cleanup error:', error); + } + } + + resetForm() { + // Cleanup first + if (this.state.docId) { + this.cleanup(); + } + + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + + this.setState({ + uploadedFile: null, + docId: null, + status: 'idle', + progress: 0, + statusMessage: '', + errorMessage: '', + availableResults: { + txt: false, + pdf: false, + pdf_indexed: false + }, + resultSizes: { + txt: null, + pdf: { compressed: null, uncompressed: null }, + pdf_indexed: { compressed: null, uncompressed: null } + } + }); + + if (this.fileInputRef.current) { + this.fileInputRef.current.value = ''; + } + } + + rerunOCR() { + // Cleanup previous results but keep the file + if (this.state.docId) { + this.cleanup(); + } + + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + + // Reset processing state but keep the uploaded file and settings + this.setState({ + docId: null, + status: 'idle', + progress: 0, + statusMessage: '', + errorMessage: '', + availableResults: { + txt: false, + pdf: false, + pdf_indexed: false + }, + resultSizes: { + txt: null, + pdf: { compressed: null, uncompressed: null }, + pdf_indexed: { compressed: null, uncompressed: null } + } + }); + } + + componentDidMount() { + // Fetch presets list + axios.get(API_URL + '/presets-list') + .then(({ data }) => { + this.setState({ presetsList: ['default', ...data] }); + }) + .catch(err => { + console.error('Failed to fetch presets:', err); + }); + } + + render() { + const { t } = this.props; + const { + uploadedFile, + status, + progress, + statusMessage, + errorMessage, + selectedLanguages, + selectedPreset, + presetsList, + outputFormats, + enableCompression, + availableResults, + resultSizes + } = this.state; + + const isProcessing = status === 'processing' || status === 'uploading'; + const isComplete = status === 'complete'; + const hasError = status === 'error'; + + return ( + + {/* Header */} + + + + {MODEL + + + {t('immediate ocr')} + + + + + + + + {/* Main Content */} + + {/* Info Alert */} + + {t('temporary processing note')} + + + {/* Error Alert */} + {hasError && errorMessage && ( + this.setState({ + status: 'idle', + errorMessage: '', + docId: null, + progress: 0 + })} + > + {t('retry')} + + } + > + {errorMessage} + + )} + + {/* Three Column Layout */} + + {/* Left Column - Upload & Languages */} + + {/* Upload Section */} + + + + {t('upload for immediate ocr')} + + + this.fileInputRef.current?.click()} + > + + + {uploadedFile ? t('file uploaded') : t('drag drop files')} + + {uploadedFile && ( + <> + + {uploadedFile.name} + + + {(uploadedFile.size / 1024 / 1024).toFixed(2)} MB + + + )} + {!uploadedFile && ( + + {t('supported formats')}: PDF, JPG, PNG, TIFF, ZIP + + )} + + + + + {/* Languages Section */} + + + {t('select languages')} + + + ({ + value: lang.value, + description: t(lang.translationKey), + disabled: lang.disabled + }))} + checked={selectedLanguages} + onChangeCallback={(checked) => this.setState({ selectedLanguages: checked })} + required + showOrder + helperText={t('language hint')} + errorText={t('language required')} + /> + + + {/* Middle Column - Preset & Process Button */} + + + + {t('select preset')} + + + + {t('ocr preset')} + + + + {/* Process Button */} + {!isComplete && ( + + )} + + + {/* Right Column - Output Formats & Compression */} + + + + {t('output formats')} + + + + this.toggleOutputFormat('txt')} + /> + } + label={t('plain text')} + /> + this.toggleOutputFormat('pdf')} + /> + } + label={t('pdf simple')} + /> + this.toggleOutputFormat('pdf_indexed')} + /> + } + label={t('pdf searchable')} + /> + + + {/* Compression Settings - Only show if PDF output is selected */} + {(outputFormats.pdf || outputFormats.pdf_indexed) && ( + + + this.setState({ enableCompression: e.target.checked })} + color="primary" + /> + } + label={t('compress pdf')} + /> + + + {t('compression info')} + + + {enableCompression && ( + + + {t('compression settings')} + + this.setState({ compressionTargetDpi: e.target.value })} + inputProps={{ min: 50, max: 300 }} + sx={{ mb: 1.5 }} + /> + this.setState({ compressionBgQuality: e.target.value })} + inputProps={{ min: 1, max: 100 }} + sx={{ mb: 1.5 }} + /> + this.setState({ compressionFgQuality: e.target.value })} + inputProps={{ min: 1, max: 100 }} + /> + + )} + + )} + + + + {/* Progress Section - Below columns */} + {isProcessing && ( + + + + {t('processing document')} + + + + {progress}% - {statusMessage} + + + + )} + + {/* Results Section - Below columns */} + {isComplete && ( + + + + {t('results ready')} + + + + {availableResults.txt && ( + + + + )} + {availableResults.pdf && ( + <> + + + + {enableCompression && resultSizes.pdf.uncompressed && ( + + + + )} + + )} + {availableResults.pdf_indexed && ( + <> + + + + {enableCompression && resultSizes.pdf_indexed.uncompressed && ( + + + + )} + + )} + + + + + + + + + + + )} + + + ); + } +} + +// Wrap with translation HOC +const ImmediateOCRWithTranslation = (props) => { + const { t } = useTranslation(); + return ; +}; + +export default ImmediateOCRWithTranslation; diff --git a/website/src/Components/LayoutMenu/LayoutImage.js b/website/src/Components/LayoutMenu/LayoutImage.js index 6e8fc310..0ec3c2c7 100644 --- a/website/src/Components/LayoutMenu/LayoutImage.js +++ b/website/src/Components/LayoutMenu/LayoutImage.js @@ -145,6 +145,15 @@ class LayoutBox extends React.Component { mouseX = coords.x; mouseY = coords.y; + // Get image dimensions to constrain box within boundaries + const image = this.imageRef.current; + const maxX = image.naturalWidth; + const maxY = image.naturalHeight; + + // Clamp coordinates to image boundaries + mouseX = Math.max(0, Math.min(mouseX, maxX)); + mouseY = Math.max(0, Math.min(mouseY, maxY)); + switch (corner) { case 0: this.setState({ top: mouseY, left: mouseX }); @@ -528,9 +537,23 @@ class LayoutImage extends React.Component { const initialCoords = this.screenToImageCoordinates(this.state.initialCoords.x, this.state.initialCoords.y); const finalCoords = this.screenToImageCoordinates(e.clientX - this.viewRef.current.offsetLeft + this.viewRef.current.scrollLeft + window.scrollX, e.clientY - this.viewRef.current.offsetTop + this.viewRef.current.scrollTop + window.scrollY); + // Get image dimensions to constrain box within boundaries + const image = this.imageRef.current; + const maxX = image.naturalWidth; + const maxY = image.naturalHeight; + + // Clamp initial and final coordinates to image boundaries + initialCoords.x = Math.max(0, Math.min(initialCoords.x, maxX)); + initialCoords.y = Math.max(0, Math.min(initialCoords.y, maxY)); + finalCoords.x = Math.max(0, Math.min(finalCoords.x, maxX)); + finalCoords.y = Math.max(0, Math.min(finalCoords.y, maxY)); + if (finalCoords.x - initialCoords.x < 150 && finalCoords.y - initialCoords.y < 150) { finalCoords.x = Math.max(finalCoords.x, initialCoords.x + 150); finalCoords.y = Math.max(finalCoords.y, initialCoords.y + 150); + // Re-clamp after minimum size adjustment + finalCoords.x = Math.min(finalCoords.x, maxX); + finalCoords.y = Math.min(finalCoords.y, maxY); } const newGroupData = { diff --git a/website/src/Components/LayoutMenu/LayoutMenu.js b/website/src/Components/LayoutMenu/LayoutMenu.js index 7be24929..34c05f57 100644 --- a/website/src/Components/LayoutMenu/LayoutMenu.js +++ b/website/src/Components/LayoutMenu/LayoutMenu.js @@ -12,6 +12,9 @@ import Switch from '@mui/material/Switch'; import CircularProgress from '@mui/material/CircularProgress'; import { NumberField } from '@base-ui-components/react/number-field'; import Tooltip from "@mui/material/Tooltip"; +import SettingsIcon from '@mui/icons-material/Settings'; + +import { withTranslation } from "react-i18next"; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; @@ -51,6 +54,8 @@ class LayoutMenu extends React.Component { contents: [], currentPage: 1, + info: props.info, + boxes: [], uncommittedChanges: false, @@ -87,8 +92,21 @@ class LayoutMenu extends React.Component { event.returnValue = ''; } + constructPath(includeSpaceId = false) { + // Build path correctly, avoiding double slashes + let parts = []; + if (includeSpaceId && this.props.spaceId) { + parts.push(this.props.spaceId); + } + if (this.props.current_folder) { + parts.push(this.props.current_folder); + } + parts.push(this.props.filename); + return parts.join('/'); + } + getLayouts() { - const path = (this.props.spaceId + '/' + this.props.current_folder + '/' + this.props.filename).replace(/^\//, ''); + const path = this.constructPath(true); const is_private = this.props._private ? '_private=true&' : ''; fetch(API_URL + '/get-layouts?' + is_private + 'path=' + path, { method: 'GET' @@ -378,7 +396,7 @@ class LayoutMenu extends React.Component { } saveLayout(closeWindow = false) { - const path = (this.props.current_folder + '/' + this.props.filename).replace(/^\//, ''); + const path = this.constructPath(false); axios.post(API_URL + '/save-layouts', { _private: this.props._private, @@ -413,9 +431,9 @@ class LayoutMenu extends React.Component { GenerateLayoutAutomatically() { this.setState({ segmentLoading: true }); - this.successNotifRef.current.openNotif("A segmentar automaticamente... Por favor aguarde"); + this.successNotifRef.current.openNotif(this.props.t("auto layout popup")); - const path = (this.props.current_folder + '/' + this.props.filename).replace(/^\//, ''); + const path = this.constructPath(false); axios.get(API_URL + '/generate-automatic-layouts', { params: { _private: this.props._private, @@ -734,7 +752,15 @@ class LayoutMenu extends React.Component { this.setState({ textModeState: mode }); } + configureOCR(e, usingCustomConfig) { + e.stopPropagation(); + const customConfig = usingCustomConfig ? this.state.info?.["config"] : null; + this.props.configureOCR(this.props.filename, false, false, customConfig); + } + render() { + const info = this.state.info; + const usingCustomConfig = info?.["config"] && info["config"] !== "default"; const loaded = this.state.contents.length !== 0; let tableData = []; @@ -808,16 +834,25 @@ class LayoutMenu extends React.Component { component="h2" className="toolbarTitle" > - Segmentar o documento + {this.props.t("layout create")}
+ this.cleanAllBoxes()} startIcon={} > - Limpar Tudo + {this.props.t("clean all")} @@ -839,7 +874,7 @@ class LayoutMenu extends React.Component { placement="top" title={ this.state.segmentLoading - ? "O documento está a ser segmentado pelo servidor" + ? this.props.t("layout loading") : cannotAutoSegmentFile ? "Não é possível segmentar automaticamente este formato de ficheiro" : "A obter informação do servidor" @@ -855,7 +890,7 @@ class LayoutMenu extends React.Component { style={{pointerEvents: "auto"} /* ensures disabled button can show title */} onClick={() => this.GenerateLayoutAutomatically()} > - Segmentar automaticamente + {this.props.t("auto layout")} { this.state.segmentLoading ? @@ -868,7 +903,7 @@ class LayoutMenu extends React.Component { placement="top" title={ this.state.segmentLoading - ? "O documento está a ser segmentado pelo servidor" + ? this.props.t("layout loading") : !this.state.uncommittedChanges ? "Não há alterações" : "A obter informação do servidor" @@ -880,37 +915,12 @@ class LayoutMenu extends React.Component { - - - - @@ -1034,7 +1044,7 @@ class LayoutMenu extends React.Component { textTransform: 'none', }} > - Replicar + {this.props.t("replicate")} @@ -1060,7 +1070,7 @@ class LayoutMenu extends React.Component { textTransform: 'none', }} > - Agrupar + {this.props.t("join")} @@ -1084,7 +1094,7 @@ class LayoutMenu extends React.Component { textTransform: 'none', }} > - Desagrupar + {this.props.t("separate")} @@ -1106,7 +1116,7 @@ class LayoutMenu extends React.Component { textTransform: 'none', }} > - Apagar + {this.props.t("delete")} @@ -1123,7 +1133,7 @@ class LayoutMenu extends React.Component { }} size='small' /> - Ignorar/Extrair + {this.props.t("ignore extract")}
{ + return ( + + + + + + + + + ); +}; + +export default SkeletonCard; + + diff --git a/website/src/Components/Notifications/ConfirmLeave.js b/website/src/Components/Notifications/ConfirmLeave.js index a7564fb9..9f3876dc 100644 --- a/website/src/Components/Notifications/ConfirmLeave.js +++ b/website/src/Components/Notifications/ConfirmLeave.js @@ -1,10 +1,12 @@ -import React from 'react'; +import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Modal from '@mui/material/Modal'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; +import CircularProgress from '@mui/material/CircularProgress'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import { useTranslation } from 'react-i18next'; import Notification from 'Components/Notifications/Notification'; @@ -27,69 +29,112 @@ const crossStyle = { right: '0.5rem' } -class ConfirmLeave extends React.Component { - constructor(props) { - super(props); - this.state = { - open: false, - } +const ConfirmLeave = forwardRef((props, ref) => { + const { leaveFunc, saveAndLeaveFunc } = props; + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [saving, setSaving] = useState(false); + + const successNot = useRef(); + const errorNot = useRef(); - this.textField = React.createRef(); - this.successNot = React.createRef(); - this.errorNot = React.createRef(); - } + // Expose toggleOpen method to parent via ref + useImperativeHandle(ref, () => ({ + toggleOpen() { + setOpen(!open); + setSaving(false); // Reset saving state when reopening + } + })); - toggleOpen() { - this.setState({ open: !this.state.open }); - } + const confirm = () => { + if (leaveFunc) { + leaveFunc(); + } + }; - confirm() { - this.props.leaveFunc(); - } + const saveAndConfirm = () => { + if (saveAndLeaveFunc) { + setSaving(true); + saveAndLeaveFunc(); + } + }; - render() { - return ( - - - - - - - Tem a certeza que quer sair? - + const toggleModal = () => { + setOpen(!open); + setSaving(false); // Reset saving state when closing + }; -

Se sair sem gravar, irá perder qualquer alteração que tenha feito!

+ return ( + + + + + + + {t("confirm leave title")} + - - - +

{t("confirm leave warning")}

- this.toggleOpen()}> - - + + + -
-
- ) - } -} + + + + +
+
+
+ ); +}); ConfirmLeave.defaultProps = { - // functions: - leaveFunc: null -} + leaveFunc: null, + saveAndLeaveFunc: null +}; export default ConfirmLeave; diff --git a/website/src/Components/Notifications/ToastNotification.js b/website/src/Components/Notifications/ToastNotification.js new file mode 100644 index 00000000..3409beb9 --- /dev/null +++ b/website/src/Components/Notifications/ToastNotification.js @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import Slide from '@mui/material/Slide'; + +function SlideTransition(props) { + return ; +} + +const ToastNotification = React.forwardRef((props, ref) => { + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(''); + const [severity, setSeverity] = useState('success'); // 'success' | 'error' | 'warning' | 'info' + const [duration, setDuration] = useState(4000); + + React.useImperativeHandle(ref, () => ({ + showToast(msg, sev = 'success', dur = 4000) { + setMessage(msg); + setSeverity(sev); + setDuration(dur); + setOpen(true); + }, + showSuccess(msg) { + this.showToast(msg, 'success'); + }, + showError(msg) { + this.showToast(msg, 'error', 6000); + }, + showWarning(msg) { + this.showToast(msg, 'warning', 5000); + }, + showInfo(msg) { + this.showToast(msg, 'info'); + }, + })); + + const handleClose = (event, reason) => { + if (reason === 'clickaway') { + return; + } + setOpen(false); + }; + + return ( + + + {message} + + + ); +}); + +export default ToastNotification; + + diff --git a/website/src/Components/OcrMenu/OcrMenu.js b/website/src/Components/OcrMenu/OcrMenu.js index a4b229db..0e22dd5a 100644 --- a/website/src/Components/OcrMenu/OcrMenu.js +++ b/website/src/Components/OcrMenu/OcrMenu.js @@ -1,6 +1,8 @@ import React from 'react'; import axios from "axios"; +import {withTranslation} from "react-i18next"; + import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; @@ -15,14 +17,27 @@ import FormLabel from "@mui/material/FormLabel"; import RadioGroup from "@mui/material/RadioGroup"; import Radio from "@mui/material/Radio"; import FormControl from "@mui/material/FormControl"; +import Switch from "@mui/material/Switch"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import InputLabel from "@mui/material/InputLabel"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import SettingsIcon from "@mui/icons-material/Settings"; import { defaultConfig, emptyConfig, engineList, tesseractLangList, + tesseractLanguagesList, + tesseractModulesList, tesseractModeList, tesseractOutputsList, + tesseractMainOutputsList, + tesseractAdvancedOutputsList, tesseractSegmentList, tesseractThreshList, } from "defaultOcrConfigs"; @@ -30,8 +45,8 @@ import { import ReturnButton from 'Components/FileSystem/ReturnButton'; import ConfirmLeave from 'Components/Notifications/ConfirmLeave'; import Notification from 'Components/Notifications/Notification'; -//const AlgoDropdown = loadComponent('Dropdown', 'AlgoDropdown'); import CheckboxList from 'Components/Form/CheckboxList'; +import InfoTooltip from 'Components/Form/InfoTooltip'; const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; @@ -40,26 +55,41 @@ class OcrMenu extends React.Component { constructor(props) { super(props); const usingDefault = this.props.customConfig == null; // null or undefined + const engines = engineList(); + const modes = tesseractModeList(); + const segments = tesseractSegmentList(); + const thresholds = tesseractThreshList(); + const outputs = tesseractOutputsList(); + // hOCR and ALTO are now supported for multi-page documents + // outputs[outputs.length - 2].disabled = !this.props.isSinglePage && !this.props.isFolder; + // outputs[outputs.length - 1].disabled = !this.props.isSinglePage && !this.props.isFolder; this.state = { ...emptyConfig, + compress: true, // Ensure compress is always initialized + compressionTargetDpi: 100, + compressionBgQuality: 40, + compressionFgQuality: 80, presetsList: [], presetName: "", defaultConfig: defaultConfig, // lists of options in state, to allow changing them dynamically depending on other choices // e.g. when choosing an OCR engine that has different parameter values - engineOptions: engineList, - engineModeOptions: tesseractModeList, - segmentModeOptions: tesseractSegmentList, - thresholdMethodOptions: tesseractThreshList, + engineOptions: engines, + engineModeOptions: modes, + segmentModeOptions: segments, + thresholdMethodOptions: thresholds, + outputOptions: outputs, usingDefault: usingDefault, uncommittedChanges: false, loaded: false, // true if default configuration has been fetched and page is ready fetchingPreset: false, // true if selected preset has been fetched + advancedDialogOpen: false, // for advanced settings dialog + moreOutputsDialogOpen: false, // for more outputs dialog } // Disable options restricted to single-page if configuring for multi-page documents - tesseractOutputsList[tesseractOutputsList.length-2]["disabled"] = !this.props.isSinglePage && !this.props.isFolder; // hOCR output - tesseractOutputsList[tesseractOutputsList.length-1]["disabled"] = !this.props.isSinglePage && !this.props.isFolder; // ALTO output + //tesseractOutputsList[tesseractOutputsList.length-2]["disabled"] = !this.props.isSinglePage && !this.props.isFolder; // hOCR output + //tesseractOutputsList[tesseractOutputsList.length-1]["disabled"] = !this.props.isSinglePage && !this.props.isFolder; // ALTO output this.confirmLeave = React.createRef(); this.successNot = React.createRef(); @@ -83,27 +113,80 @@ class OcrMenu extends React.Component { event.returnValue = ''; } - fetchDefaultConfig() { + fetchDefaultConfig(savedConfig = null) { axios.get(API_URL + '/default-config') .then(({ data }) => { if (!this.state.loaded) { // entering config menu, set initial config - const initialConfig = Object.assign({...data}, this.props.customConfig); - this.setState({...initialConfig, defaultConfig: data, loaded: true}); + // Priority: savedConfig (from backend) > props.customConfig > default + const configToApply = savedConfig || this.props.customConfig; + const usingDefault = !configToApply || configToApply === "default"; + const initialConfig = Object.assign({...data}, configToApply); + // Ensure compress is always set (default to true if missing) + if (initialConfig.compress === undefined || initialConfig.compress === null) { + initialConfig.compress = true; + } + this.setState({...initialConfig, defaultConfig: data, loaded: true, usingDefault: usingDefault}); } else { this.setState({defaultConfig: data}); } }) .catch(err => { - this.errorNot.current.openNotif("Não foi possível obter a configuração por defeito mais atual"); + this.errorNot.current.openNotif(this.props.t("error fetch default config")); if (!this.state.loaded) { // entering config, use hardcoded default for initial config - const initialConfig = Object.assign({...defaultConfig}, this.props.customConfig); - this.setState({...initialConfig, loaded: true}); + const configToApply = savedConfig || this.props.customConfig; + const usingDefault = !configToApply || configToApply === "default"; + const initialConfig = Object.assign({...defaultConfig}, configToApply); + // Ensure compress is always set (default to true if missing) + if (initialConfig.compress === undefined || initialConfig.compress === null) { + initialConfig.compress = true; + } + this.setState({...initialConfig, loaded: true, usingDefault: usingDefault}); } }); } + constructPath() { + // Build path correctly, avoiding double slashes + let parts = []; + if (this.props.spaceId) { + parts.push(this.props.spaceId); + } + if (this.props.current_folder) { + parts.push(this.props.current_folder); + } + parts.push(this.props.filename); + return parts.join('/'); + } + + /** + * Fetch the document's saved OCR config from the backend. + * This ensures we always get the latest saved config. + */ + fetchDocumentConfig() { + const path = this.constructPath(); + axios.get(API_URL + '/get-config', { + params: { + _private: this.props._private, + path: path + } + }) + .then(({ data }) => { + if (data.success && data.config) { + // Document has a saved custom config + this.fetchDefaultConfig(data.config); + } else { + // No saved config - use default + this.fetchDefaultConfig(null); + } + }) + .catch(err => { + // Fallback to using prop or default + this.fetchDefaultConfig(this.props.customConfig); + }); + } + fetchConfigPreset(name) { this.setState({fetchingPreset: true}); axios.get(API_URL + '/config-preset', { @@ -121,7 +204,7 @@ class OcrMenu extends React.Component { }); }) .catch(err => { - this.errorNot.current.openNotif("Não foi possível obter a configuração predefinida"); + this.errorNot.current.openNotif(this.props.t("error fetch preset")); this.setState({presetName: null, fetchingPreset: false}); }); } @@ -132,12 +215,13 @@ class OcrMenu extends React.Component { this.setState({presetsList: data}); }) .catch(err => { - this.errorNot.current.openNotif("Não foi possível atualizar a lista de configurações predefinidas"); + this.errorNot.current.openNotif(this.props.t("error fetch presets list")); }); } componentDidMount() { - this.fetchDefaultConfig(); + // Fetch the document's saved config first, which then fetches default config + this.fetchDocumentConfig(); this.fetchPresetsList(); this.interval = setInterval(() => { this.fetchDefaultConfig(); @@ -166,6 +250,12 @@ class OcrMenu extends React.Component { engineMode: this.state.engineMode, segmentMode: this.state.segmentMode, thresholdMethod: this.state.thresholdMethod, + compress: this.state.compress !== undefined ? this.state.compress : true, + compressionTargetDpi: Number(this.state.compressionTargetDpi), + compressionBgQuality: Number(this.state.compressionBgQuality), + compressionFgQuality: Number(this.state.compressionFgQuality), + compressionFlattenToJpeg: true, // Always flatten to JPEG + preprocessing: this.state.preprocessing, } if (this.state.dpiVal !== null && this.state.dpiVal !== "") { config.dpi = this.state.dpiVal; @@ -186,12 +276,17 @@ class OcrMenu extends React.Component { restoreDefault() { if (this.state.usingDefault) return; - this.setState({ + const restoredConfig = { ...this.state.defaultConfig, presetName: null, usingDefault: true, uncommittedChanges: this.props.customConfig != null, // no changes if was already default - }); + }; + // Reset compression settings to defaults + restoredConfig.compressionTargetDpi = 100; + restoredConfig.compressionBgQuality = 40; + restoredConfig.compressionFgQuality = 80; + this.setState(restoredConfig); } setLangList(checked) { @@ -205,7 +300,7 @@ class OcrMenu extends React.Component { changeDpi(value) { value = value.trim() if (!(/^[1-9][0-9]*$/.test(value))) { - this.errorNot.current.openNotif("O valor de DPI deve ser um número inteiro!"); + this.errorNot.current.openNotif(this.props.t("error dpi must be integer")); } this.setState({ dpiVal: value, usingDefault: false, uncommittedChanges: true }); } @@ -222,6 +317,10 @@ class OcrMenu extends React.Component { this.setState({ segmentMode: Number(value), usingDefault: false, uncommittedChanges: true }); } + changeCompress(value) { + this.setState({ compress: value, usingDefault: false, uncommittedChanges: true }); + } + changeThresholdingMethod(value) { this.setState({ thresholdMethod: Number(value), usingDefault: false, uncommittedChanges: true }); } @@ -246,7 +345,7 @@ class OcrMenu extends React.Component { } saveConfig(exit = false) { - const path = (this.props.spaceId + '/' + this.props.current_folder + '/' + this.props.filename).replace(/^\//, ''); + const path = this.constructPath(); const config = this.state.usingDefault ? "default" : this.getConfig(); axios.post(API_URL + '/save-config', { @@ -263,33 +362,34 @@ class OcrMenu extends React.Component { if (data["success"]) { this.setState({ uncommittedChanges: false }); - this.successNot.current.openNotif("Configuração de OCR guardada com sucesso."); + this.successNot.current.openNotif(this.props.t("success config saved")); if (exit) { this.leave(); } else { this.props.setCurrentCustomConfig(config); + // Reload the configuration from server to ensure it's properly saved + this.fetchDocumentConfig(); } } else { - this.errorNot.current.openNotif("Erro inesperado ao guardar a configuração de OCR.") + this.errorNot.current.openNotif(this.props.t("error config save unexpected")) } }) .catch(err => { - this.errorNot.current.openNotif("Não foi possível guardar a configuração de OCR."); + this.errorNot.current.openNotif(this.props.t("error config save failed")); }); } render() { const valid = ( (this.state.dpiVal === null || this.state.dpiVal === "" || (/^[1-9][0-9]*$/.test(this.state.dpiVal))) - && this.state.lang.length !== 0 && this.state.outputs.length !== 0 ); return ( <> - + this.saveConfig(true)} ref={this.confirmLeave} /> @@ -303,7 +403,7 @@ class OcrMenu extends React.Component { component="h2" className="toolbarTitle" > - Configurar OCR {this.props.isFolder ? 'da pasta' : 'do documento'} + {this.props.t("configure ocr")} {this.props.isFolder ? this.props.t("of folder") : this.props.t("of document")} @@ -311,14 +411,20 @@ class OcrMenu extends React.Component { option} + getOptionLabel={(option) => { + // Try to get translation for presets, fallback to raw name + const translationKey = `presets.${option}`; + const translated = this.props.t(translationKey); + // If translation key not found, i18next returns the key itself + return translated !== translationKey ? translated : option; + }} autoHighlight onChange={(e, newValue) => this.selectPreset(newValue)} renderInput={(params) => ( } onClick={() => this.restoreDefault()} > - Valores Por Defeito + {this.props.t("default values")} -
@@ -395,36 +491,70 @@ class OcrMenu extends React.Component { - - - - - + + {this.props.t("languages_section")} + + + + {/* #region agent log */} + {(() => { + const rawList = tesseractLanguagesList(); + const mappedList = rawList.map(opt => ({ value: opt.value, description: this.props.t(opt.translationKey) })); + fetch('http://127.0.0.1:7326/ingest/46879f29-c4a4-4a72-82b8-c1d87f64db28',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'00413b'},body:JSON.stringify({sessionId:'00413b',location:'OcrMenu.js:507',message:'Language list transformation',data:{rawSample:rawList[0],mappedSample:mappedList[0],tFunction:typeof this.props.t,tResult:this.props.t('languages.arabic')},timestamp:Date.now(),hypothesisId:'BCD'})}).catch(()=>{}); + return null; + })()} + {/* #endregion */} + ({ + value: opt.value, + description: this.props.t(opt.translationKey) + }))} checked={this.state.lang} onChangeCallback={this.setLangList} - required showOrder - helperText="Para melhores resultados, selecione por ordem de relevância" - errorText="Deve selecionar pelo menos uma língua"/> + helperText={this.props.t("helper text language order")} + errorText={this.props.t("error must select language")}/> + + + + {this.props.t("special modules")} + + + + ({ + value: opt.value, + description: this.props.t(opt.translationKey) + }))} + checked={this.state.lang} + onChangeCallback={this.setLangList} + showOrder={false}/>
- + + "& input:focus:invalid + fieldset": {borderColor: "red", borderWidth: 2}, + flexGrow: 1, + }} + /> + + - - Motor de OCR - this.changeEngine(e.target.value)}> - { - this.state.engineOptions.map((option) => - } label={option.description}/> - ) - } - - - - - Modo do motor - this.changeEngineMode(e.target.value)}> - { - this.state.engineModeOptions.map((option) => - } label={option.description}/> - ) - } - - - - - Segmentação - this.changeSegmentationMode(e.target.value)}> - { - this.state.segmentModeOptions.map((option) => - } label={option.description}/> - ) - } - - - - - Thresholding - this.changeThresholdingMethod(e.target.value)}> - { - this.state.thresholdMethodOptions.map((option) => - } label={option.description}/> - ) + {/* Preprocessing Settings */} + + + + {this.props.t("preprocessing.title")} + + + + + this.setState({ + preprocessing: { ...this.state.preprocessing, enabled: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + /> } - - + label={this.props.t("preprocessing.enabled")} + /> - this.changeAdditionalParams(e.target.value)} - variant='outlined' - className="simpleInput borderTop" - size="small" - slotProps={{inputLabel: {sx: {top: "0.5rem"}}}} - /> + + {this.props.t("preprocessing.threshold_method")} + + +
+ + +
+ + + + + {this.props.t("output formats")} + + + + ({ + value: opt.value, + description: this.props.t(opt.translationKey) + }))} + checked={this.state.outputs} + onChangeCallback={this.setOutputList} + errorText={this.props.t("error must select output")}/> + + + + {/* Compression Settings - Only show if PDF output is selected */} + {(this.state.outputs.includes('pdf') || this.state.outputs.includes('pdf_indexed')) && ( + + + this.changeCompress(e.target.checked)} + color="primary" + /> + } + label={this.props.t("compress pdf")} + /> + + + + {this.props.t("compress pdf description")} + + + {/* Compression Settings */} + {(this.state.compress !== undefined ? this.state.compress : true) && ( + + + {this.props.t('compression settings')} + + + {/* Target DPI */} + this.setState({ compressionTargetDpi: e.target.value, usingDefault: false, uncommittedChanges: true })} + inputProps={{ min: 50, max: 300 }} + sx={{ mb: 1.5 }} + /> + + {/* Background Quality */} + this.setState({ compressionBgQuality: e.target.value, usingDefault: false, uncommittedChanges: true })} + inputProps={{ min: 1, max: 100 }} + sx={{ mb: 1.5 }} + /> + + {/* Foreground Quality */} + this.setState({ compressionFgQuality: e.target.value, usingDefault: false, uncommittedChanges: true })} + inputProps={{ min: 1, max: 100 }} + /> + + )} + + )} {/* @@ -526,6 +747,349 @@ class OcrMenu extends React.Component {
} + + {/* Advanced Settings Dialog */} + this.setState({ advancedDialogOpen: false })} + maxWidth="md" + fullWidth + > + {this.props.t("advanced ocr settings")} + + + + + {this.props.t("engine mode")} + + + this.changeEngineMode(e.target.value)}> + { + this.state.engineModeOptions.map((option) => + } label={option.description}/> + ) + } + + + + + + {this.props.t("segmentation")} + + + this.changeSegmentationMode(e.target.value)}> + { + this.state.segmentModeOptions.map((option) => + } label={option.description}/> + ) + } + + + + + this.changeAdditionalParams(e.target.value)} + variant='outlined' + size="small" + fullWidth + sx={{ flexGrow: 1 }} + /> + + + + {/* Tesseract Thresholding Method */} + + + {this.props.t("tesseract thresholding")} + + + this.changeThresholdingMethod(e.target.value)}> + { + this.state.thresholdMethodOptions.map((option) => + } label={option.description}/> + ) + } + + + + {/* Preprocessing Pipeline Section */} + + {this.props.t("preprocessing.title")} + + this.setState({ + preprocessing: { ...this.state.preprocessing, enabled: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + /> + } + label={this.props.t("preprocessing.enabled")} + /> + + + this.setState({ + preprocessing: { ...this.state.preprocessing, grayscale: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.grayscale")} + /> + + + this.setState({ + preprocessing: { ...this.state.preprocessing, clahe: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.clahe")} + /> + {this.state.preprocessing?.clahe && ( + + this.setState({ + preprocessing: { ...this.state.preprocessing, clahe_clip_limit: parseFloat(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + sx={{ minWidth: '200px' }} + inputProps={{ min: 1, max: 10, step: 0.1 }} + disabled={!this.state.preprocessing?.enabled} + /> + this.setState({ + preprocessing: { ...this.state.preprocessing, clahe_tile_size: parseInt(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + sx={{ minWidth: '200px' }} + inputProps={{ min: 4, max: 32, step: 1 }} + disabled={!this.state.preprocessing?.enabled} + /> + + )} + + + + this.setState({ + preprocessing: { ...this.state.preprocessing, median_blur: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.median_blur")} + /> + {this.state.preprocessing?.median_blur && ( + this.setState({ + preprocessing: { ...this.state.preprocessing, median_blur_kernel: parseInt(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + inputProps={{ min: 3, max: 9, step: 2 }} + disabled={!this.state.preprocessing?.enabled} + /> + )} + + + this.setState({ + preprocessing: { ...this.state.preprocessing, deskew: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.deskew")} + /> + + + + {this.props.t("preprocessing.threshold_method")} + + + {this.state.preprocessing?.threshold_method === "adaptive_gaussian" && ( + + this.setState({ + preprocessing: { ...this.state.preprocessing, adaptive_block_size: parseInt(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + sx={{ minWidth: '200px' }} + inputProps={{ min: 3, max: 99, step: 2 }} + disabled={!this.state.preprocessing?.enabled} + /> + this.setState({ + preprocessing: { ...this.state.preprocessing, adaptive_c: parseInt(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + sx={{ minWidth: '200px' }} + inputProps={{ min: 0, max: 20, step: 1 }} + disabled={!this.state.preprocessing?.enabled} + /> + + )} + + + this.setState({ + preprocessing: { ...this.state.preprocessing, morphological_opening: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.morphological_opening")} + /> + + + this.setState({ + preprocessing: { ...this.state.preprocessing, morphological_closing: e.target.checked }, + usingDefault: false, + uncommittedChanges: true + })} + disabled={!this.state.preprocessing?.enabled} + /> + } + label={this.props.t("preprocessing.morphological_closing")} + /> + {(this.state.preprocessing?.morphological_opening || this.state.preprocessing?.morphological_closing) && ( + this.setState({ + preprocessing: { ...this.state.preprocessing, morph_kernel_size: parseInt(e.target.value) }, + usingDefault: false, + uncommittedChanges: true + })} + size="small" + inputProps={{ min: 3, max: 9, step: 2 }} + disabled={!this.state.preprocessing?.enabled} + /> + )} + + + + + + + + + + + {/* More Outputs Dialog */} + this.setState({ moreOutputsDialogOpen: false })} + maxWidth="sm" + fullWidth + > + {this.props.t("more outputs")} + + + ({ + value: opt.value, + description: this.props.t(opt.translationKey) + }))} + checked={this.state.outputs} + onChangeCallback={this.setOutputList}/> + + + + + + ); } @@ -545,4 +1109,4 @@ OcrMenu.defaultProps = { showStorageForm: null, } -export default OcrMenu; +export default withTranslation()(OcrMenu); diff --git a/website/src/Components/QueueMonitor/QueueMonitor.js b/website/src/Components/QueueMonitor/QueueMonitor.js new file mode 100644 index 00000000..b571d357 --- /dev/null +++ b/website/src/Components/QueueMonitor/QueueMonitor.js @@ -0,0 +1,625 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { useNavigate } from 'react-router'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import Chip from '@mui/material/Chip'; +import CircularProgress from '@mui/material/CircularProgress'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import PendingActionsIcon from '@mui/icons-material/PendingActions'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import ScheduleIcon from '@mui/icons-material/Schedule'; +import WorkIcon from '@mui/icons-material/Work'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DeleteIcon from '@mui/icons-material/Delete'; +import Button from '@mui/material/Button'; +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import { useTranslation } from 'react-i18next'; + +const API_URL = `${window.location.protocol}//${window.location.host}/${process.env.REACT_APP_API_URL}`; + +const QueueMonitor = ({ compact = false, autoRefresh = true, refreshInterval = 5000 }) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [queueData, setQueueData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' }); + + const fetchQueueStatus = async () => { + try { + const response = await axios.get(`${API_URL}/queue-status`); + if (response.data.success) { + setQueueData(response.data); + setError(null); + } else { + setError(response.data.error || 'Failed to fetch queue status'); + } + } catch (err) { + console.error('Error fetching queue status:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleCancelOCR = async (filePath) => { + if (!window.confirm(t('queue.cancel_confirm'))) { + return; + } + + try { + const response = await axios.post(`${API_URL}/cancel-ocr`, { + files_path: filePath + }); + + if (response.data.success) { + setSnackbar({ + open: true, + message: t('queue.cancel_success'), + severity: 'success' + }); + fetchQueueStatus(); + } else { + setSnackbar({ + open: true, + message: t('queue.cancel_error') + ': ' + response.data.error, + severity: 'error' + }); + } + } catch (err) { + console.error('Error cancelling OCR:', err); + setSnackbar({ + open: true, + message: t('queue.cancel_error') + ': ' + err.message, + severity: 'error' + }); + } + }; + + const handleRemoveFromQueue = async (filePath) => { + if (!window.confirm(t('queue.remove_confirm'))) { + return; + } + + try { + const response = await axios.post(`${API_URL}/remove-from-queue`, { + files_path: filePath + }); + + if (response.data.success) { + setSnackbar({ + open: true, + message: t('queue.remove_success'), + severity: 'success' + }); + fetchQueueStatus(); + } else { + setSnackbar({ + open: true, + message: t('queue.remove_error') + ': ' + response.data.error, + severity: 'error' + }); + } + } catch (err) { + console.error('Error removing from queue:', err); + setSnackbar({ + open: true, + message: t('queue.remove_error') + ': ' + err.message, + severity: 'error' + }); + } + }; + + const handleCloseSnackbar = () => { + setSnackbar({ ...snackbar, open: false }); + }; + + useEffect(() => { + fetchQueueStatus(); + + if (autoRefresh) { + const interval = setInterval(fetchQueueStatus, refreshInterval); + return () => clearInterval(interval); + } + }, [autoRefresh, refreshInterval]); + + const handleRefresh = () => { + setLoading(true); + fetchQueueStatus(); + }; + + // Task name beautification + const beautifyTaskName = (taskName) => { + const taskNames = { + 'file_ocr': t('queue.task.file_ocr') || 'File OCR', + 'ocr_from_api': t('queue.task.ocr_from_api') || 'API OCR', + 'page_ocr': t('queue.task.page_ocr') || 'Page OCR', + 'export_file': t('queue.task.export_file') || 'Export File', + 'prepare_file': t('queue.task.prepare_file') || 'Prepare File', + 'auto_segment': t('queue.task.auto_segment') || 'Auto Segment', + }; + return taskNames[taskName] || taskName; + }; + + if (loading && !queueData) { + return ( + + + + ); + } + + if (error) { + return ( + + + {t('queue.error') || 'Error loading queue status'}: {error} + + + ); + } + + if (!queueData) return null; + + const { + queue, + workers, + total_pending, + queued_files = [], + processing_files = [], + folder_queue = { active_folders: [], queued_folders: [] } + } = queueData; + + const { active_folders = [], queued_folders = [] } = folder_queue; + + // Compact view for header/navbar + if (compact) { + const hasActivity = queue.active.total > 0 || total_pending > 0; + + return ( + + navigate('/queue-status')} + > + + + {queue.active.total > 0 && `${queue.active.total} ${t('queue.active') || 'active'}`} + {queue.active.total > 0 && total_pending > 0 && ' • '} + {total_pending > 0 && `${total_pending} ${t('queue.pending') || 'queued'}`} + {!hasActivity && (t('queue.idle') || 'Idle')} + + {loading && } + + + ); + } + + // Full view for dedicated queue page/section + return ( + + + + {t('queue.title') || 'Task Queue Status'} + + + + + + + {/* Summary Cards */} + + + + + + + {t('queue.active') || 'Active Tasks'} + + + + {queue.active.total} + + + + + + + + + + {t('queue.queued') || 'Queued Tasks'} + + + + {queue.reserved.total} + + + + + + + + + + {t('queue.scheduled') || 'Scheduled Tasks'} + + + + {queue.scheduled.total} + + + + + + + + + + {t('queue.workers') || 'Workers'} + + + + {workers.length} + + + + + + {/* Folder Queue Status - NEW SECTION */} + {(active_folders.length > 0 || queued_folders.length > 0) && ( + + + + {t('queue.folder_queue_status') || 'Folder Queue Status'} + + + {/* Active Folders */} + {active_folders.length > 0 && ( + + + {t('queue.active_folders') || 'Currently Processing Folders'} + + {active_folders.map((folder, index) => ( + + + + + + {folder.name} + + + {folder.path} + + + + + + ))} + + )} + + {/* Queued Folders */} + {queued_folders.length > 0 && ( + + + {t('queue.queued_folders_display') || 'Folders Waiting in Queue'} + + {queued_folders.map((folder) => ( + + + + + + {folder.name} + + + {folder.path} • {folder.files_count} {t('queue.files') || 'files'} + + + + + + + + + ))} + + )} + + + )} + + {/* Task Details */} + {(queue.active.total > 0 || queue.reserved.total > 0 || queue.scheduled.total > 0) && ( + + + + {t('queue.task_breakdown') || 'Task Breakdown'} + + + {queue.active.total > 0 && ( + + + {t('queue.currently_processing') || 'Currently Processing'} + + + {Object.entries(queue.active.by_task).map(([taskName, count]) => ( + + ))} + + + )} + + {queue.reserved.total > 0 && ( + + + {t('queue.waiting_in_queue') || 'Waiting in Queue'} + + + {Object.entries(queue.reserved.by_task).map(([taskName, count]) => ( + + ))} + + + )} + + {queue.scheduled.total > 0 && ( + + + {t('queue.scheduled_for_later') || 'Scheduled for Later'} + + + {Object.entries(queue.scheduled.by_task).map(([taskName, count]) => ( + + ))} + + + )} + + + )} + + {/* Currently Processing Files */} + {processing_files.length > 0 && ( + + + + {t('queue.currently_processing_files') || 'Currently Processing Files'} + + {processing_files.map((file, index) => ( + + + + + + {file.name} + + + {file.path} + + + {file.message} + + + + + + + handleCancelOCR(file.path)} + sx={{ + '&:hover': { + backgroundColor: 'rgba(211, 47, 47, 0.1)' + } + }} + > + + + + + + ))} + + + )} + + {/* Queued Files (Sequential Processing) */} + {queued_files.length > 0 && ( + + + + {t('queue.queued_files') || 'Files Waiting in Sequential Queue'} + + {queued_files.map((file, index) => ( + + + + + + {file.name} + + + {file.path} + + + + + {file.position && ( + + )} + + handleRemoveFromQueue(file.path)} + sx={{ + '&:hover': { + backgroundColor: 'rgba(211, 47, 47, 0.1)' + } + }} + > + + + + + + ))} + + + )} + + {/* Workers Info */} + {workers.length > 0 && ( + + + + {t('queue.worker_details') || 'Worker Details'} + + {workers.map((worker, index) => ( + + + {worker.name} • Pool: {worker.pool} • Max Concurrency: {worker.max_concurrency} + + + ))} + + + )} + + + + + + {snackbar.message} + + + + ); +}; + +export default QueueMonitor; diff --git a/website/src/Components/QueueMonitor/QueueStatusPage.js b/website/src/Components/QueueMonitor/QueueStatusPage.js new file mode 100644 index 00000000..5c3bff0c --- /dev/null +++ b/website/src/Components/QueueMonitor/QueueStatusPage.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { Link } from 'react-router'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { useTranslation } from 'react-i18next'; +import QueueMonitor from '../QueueMonitor/QueueMonitor'; +import { MODEL, STJ } from '../../App'; +import logoApp from "static/logoApp.png"; +import logoUN from "static/Logo_of_the_United_Nations.svg"; + +const QueueStatusPage = () => { + const { t } = useTranslation(); + + return ( + + {/* Header */} + + + + {MODEL + + + {t("queue.page_title") || "Queue Status"} + + + + + + + + {/* Main Content */} + + + + + ); +}; + +export default QueueStatusPage; diff --git a/website/src/Components/Search/SearchBar.js b/website/src/Components/Search/SearchBar.js new file mode 100644 index 00000000..93ecfa35 --- /dev/null +++ b/website/src/Components/Search/SearchBar.js @@ -0,0 +1,271 @@ +import React, { useState, useRef, useEffect } from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; +import SearchIcon from '@mui/icons-material/Search'; +import ClearIcon from '@mui/icons-material/Clear'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import Chip from '@mui/material/Chip'; +import Collapse from '@mui/material/Collapse'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Checkbox from '@mui/material/Checkbox'; +import ListItemText from '@mui/material/ListItemText'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; + +const SearchBar = ({ onSearchChange, onFiltersChange, showFilters = true }) => { + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = useState(''); + const [showFilterPanel, setShowFilterPanel] = useState(false); + const [filters, setFilters] = useState({ + fileTypes: [], + ocrStatus: [], + dateRange: 'all', + }); + const searchInputRef = useRef(null); + + const fileTypeOptions = [ + { value: 'pdf', label: 'PDF' }, + { value: 'image', label: 'Images' }, + { value: 'zip', label: 'ZIP' }, + ]; + + const ocrStatusOptions = [ + { value: 'complete', label: t('ocr complete') }, + { value: 'processing', label: t('uploading stage') }, + { value: 'pending', label: 'Pending' }, + ]; + + const dateRangeOptions = [ + { value: 'all', label: 'All Time' }, + { value: 'today', label: 'Today' }, + { value: 'week', label: 'This Week' }, + { value: 'month', label: 'This Month' }, + ]; + + useEffect(() => { + // Focus search when Cmd/Ctrl+K is pressed + const handleKeyDown = (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + searchInputRef.current?.focus(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const handleSearchChange = (e) => { + const value = e.target.value; + setSearchQuery(value); + if (onSearchChange) { + onSearchChange(value); + } + }; + + const handleClearSearch = () => { + setSearchQuery(''); + if (onSearchChange) { + onSearchChange(''); + } + }; + + const handleFilterChange = (filterType, value) => { + const newFilters = { ...filters, [filterType]: value }; + setFilters(newFilters); + if (onFiltersChange) { + onFiltersChange(newFilters); + } + }; + + const handleClearFilters = () => { + const clearedFilters = { + fileTypes: [], + ocrStatus: [], + dateRange: 'all', + }; + setFilters(clearedFilters); + if (onFiltersChange) { + onFiltersChange(clearedFilters); + } + }; + + const hasActiveFilters = + filters.fileTypes.length > 0 || + filters.ocrStatus.length > 0 || + filters.dateRange !== 'all'; + + return ( + + + + + + ), + endAdornment: searchQuery && ( + + + + + + ), + }} + /> + {showFilters && ( + setShowFilterPanel(!showFilterPanel)} + sx={{ + backgroundColor: showFilterPanel || hasActiveFilters ? 'var(--accent-primary)' : 'var(--card-bg)', + color: showFilterPanel || hasActiveFilters ? 'white' : 'var(--text-primary)', + borderRadius: 'var(--radius-md)', + transition: 'all var(--transition-fast)', + '&:hover': { + backgroundColor: showFilterPanel || hasActiveFilters ? 'var(--accent-primary)' : 'var(--card-hover-bg)', + transform: 'translateY(-1px)', + }, + }} + > + + + )} + + + {showFilters && ( + + + + + File Type + + + + + OCR Status + + + + + Date Range + + + + + {hasActiveFilters && ( + + + + )} + + + )} + + ); +}; + +export default SearchBar; + + diff --git a/website/src/Languages/Arabic/translation.json b/website/src/Languages/Arabic/translation.json new file mode 100644 index 00000000..7112d5fd --- /dev/null +++ b/website/src/Languages/Arabic/translation.json @@ -0,0 +1,429 @@ +{ + "welcome": "مرحباً", + "login": "تسجيل الدخول", + "private space": "مساحة خاصة", + "new folder": "مجلد جديد", + "new document": "تحميل مستند", + "leave space": "مغادرة المساحة", + "back": "رجوع", + "start": "البداية", + "grid view": "عرض الشبكة", + "list view": "عرض القائمة", + "empty folder title": "هذا المجلد فارغ", + "empty folder description": "أضف مستنداً أو أنشئ مجلداً فرعياً للبدء.", + "uploading": "جارٍ الرفع", + "ocr complete": "اكتمل التعرف الضوئي", + "pages": "صفحات", + "document": "مستند", + "documents": "مستندات", + "folder": "مجلد", + "folders": "مجلدات", + "see document": "عرض المستند", + "edit text": "تعديل النص", + "repeat ocr": "إعادة التعرف الضوئي", + "run ocr": "تشغيل التعرف الضوئي", + "download txt": "تنزيل TXT", + "download pdf": "تنزيل PDF", + "download images": "تنزيل الصور", + "download original": "تنزيل الأصلي", + "open folder": "فتح المجلد", + "custom config": "تكوين التعرف الضوئي المخصص", + "search": "بحث", + "sync": "مزامنة", + "finish": "إنهاء", + "name": "الاسم", + "details": "التفاصيل", + "size": "الحجم", + "date of creation": "تاريخ الإنشاء", + "process state": "حالة العملية", + "version": "الإصدار", + "user manual": "دليل المستخدم", + "title": "أداة التعرف الضوئي على الأحرف", + "folder without files": "هذا المجلد فارغ", + "sub-folder": "مجلد فرعي", + "page": "صفحة", + "words": "كلمات", + "config ocr": "تكوين التعرف الضوئي", + "cancel ocr": "إلغاء التعرف الضوئي", + "config strategy": "استراتيجية التكوين", + "config strategy override": "استخدام تكوين المجلد لجميع الملفات", + "config strategy respect": "احترام التكوين الفردي لكل ملف", + "config strategy hybrid": "استخدام تكوين الملف إن وجد، وإلا استخدام تكوين المجلد", + "config strategy hint": "اختر كيفية تطبيق التكوين على الملفات في هذا المجلد", + "clean all": "مسح الكل", + "auto layout": "تخطيط تلقائي", + "edit results": "تعديل النتائج", + "delete": "حذف", + "index": "فهرسة", + "deindex": "إلغاء الفهرسة", + "save": "حفظ", + "close": "إغلاق", + "retry": "إعادة المحاولة", + "advanced": "متقدم", + "advanced ocr settings": "إعدادات OCR المتقدمة", + "upload error": "خطأ في رفع الملف", + "file download started": "بدأ تنزيل الملف، يرجى الانتظار", + "folder created successfully": "تم إنشاء المجلد بنجاح", + "folder name required": "يجب عليك تعيين اسم للمجلد", + "ocr started": "بدأ التعرف الضوئي، يرجى الانتظار", + "folder queued": "في انتظار قائمة انتظار المجلدات", + "edited results files to recreate": "نتائج معدلة، ملفات لإعادة إنشائها", + "queued file": "في الطابور (ملف {{current}}/{{total}})", + "processing ocr page": "معالجة التعرف الضوئي - الصفحة {{current}}/{{total}}", + "generating results": "إنشاء النتائج", + "generating text": "إنشاء النص", + "generating delimited text": "إنشاء النص المحدد", + "generating pdf with index": "إنشاء PDF مع الفهرس", + "generating pdf": "إنشاء PDF", + "generating csv": "إنشاء CSV", + "generating images": "إنشاء الصور", + "generating hocr": "إنشاء hOCR", + "generating alto xml": "إنشاء ALTO XML", + "generating index": "إنشاء الفهرس", + "generating pdf with index page": "إنشاء PDF مع الفهرس {{current}}/{{total}}", + "generating pdf page": "إنشاء PDF {{current}}/{{total}}", + "compression complete": "اكتمل الضغط", + "compressing pdf finalizing": "ضغط PDF - الانتهاء...", + "compressing pdf": "ضغط PDF", + "compressing file": "ضغط الملف الأصلي...", + "error preparing document": "خطأ في تحضير المستند", + "error during ocr": "خطأ أثناء التعرف الضوئي", + "error during ocr page": "خطأ أثناء التعرف الضوئي للصفحة {{page}}", + "error generating results": "خطأ في إنشاء النتائج", + "ocr interrupted retry": "تم إيقاف التعرف الضوئي - يمكنك المحاولة مرة أخرى", + "invalid parameters": "معاملات غير صالحة", + "ocr cancelled": "تم إلغاء OCR", + "error canceling ocr": "خطأ في إلغاء OCR", + "uploading stage": "جارٍ الرفع، يرجى الانتظار...", + "preparing stage": "جارٍ تحضير المستند...", + "preparing ocr": "جارٍ تحضير OCR...", + "confirm leave title": "هل أنت متأكد أنك تريد المغادرة؟", + "confirm leave warning": "إذا غادرت دون حفظ، ستفقد أي تغييرات قمت بها!", + "confirm leave button": "المغادرة دون حفظ", + "save and leave button": "حفظ والمغادرة", + "saving": "جارٍ الحفظ", + "compressing": "جارٍ الضغط", + "exporting": "جارٍ التصدير", + "ignore extract": "تجاهل/استخراج", + "join": "دمج", + "separate": "فصل", + "replicate": "نسخ", + "create new folder": "إنشاء مجلد جديد", + "folder name": "اسم المجلد*", + "folder name extra": "لا يمكن أن يبدأ الاسم بـ '_' أو يحتوي على '/' أو '\\'", + "create": "إنشاء", + "auto layout popup": "جارٍ تحضير التخطيط، يرجى الانتظار...", + "layout loading": "جارٍ إنشاء التخطيط...", + "layout create": "إنشاء تخطيط", + "configure ocr": "تكوين التعرف الضوئي", + "of document": "للمستند", + "of folder": "للمجلد", + "languages": { + "arabic": "العربية", + "chinese_simplified": "الصينية (المبسطة)", + "chinese_traditional": "الصينية (التقليدية)", + "german": "الألمانية", + "english": "الإنجليزية", + "french": "الفرنسية", + "hindi": "الهندية", + "indonesian": "الإندونيسية (Bahasa Indonesia)", + "italian": "الإيطالية", + "portuguese": "البرتغالية", + "russian": "الروسية", + "spanish": "الإسبانية", + "math module": "وحدة الكشف عن الرياضيات", + "osd module": "وحدة الكشف عن الاتجاه والنص" + }, + "output": { + "pdf indexed": "PDF مع النص وفهرس الكلمات", + "pdf": "PDF مع النص (افتراضي)", + "txt": "نص عادي", + "txt delimited": "نص محدد بالصفحات", + "csv": "فهرس الكلمات بتنسيق CSV", + "ner": "الكيانات المسماة (NER)", + "hocr": "hOCR (مستندات أحادية الصفحة فقط)", + "xml": "ALTO (مستندات أحادية الصفحة فقط)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract الأصلي", + "lstm": "Tesseract LSTM", + "combined": "LSTM + الأصلي مدمج", + "default": "الوضع الافتراضي" + }, + "segmentation mode": { + "auto with osd": "تقسيم تلقائي للصفحة مع OSD", + "auto no osd": "تقسيم تلقائي بدون OSD أو OCR", + "default": "(افتراضي) تقسيم تلقائي، بدون OSD", + "column variable lines": "عمود نص بأحجام أسطر متغيرة", + "block vertical": "كتلة نص عمودية موحدة", + "block uniform": "كتلة نص موحدة", + "single line": "صورة بسطر نص واحد", + "single word": "صورة بكلمة واحدة", + "single circle word": "صورة بكلمة واحدة في دائرة", + "single char": "صورة بحرف واحد", + "sparse text": "نص متفرق؛ ابحث عن أكبر قدر ممكن بدون ترتيب", + "sparse text osd": "نص متفرق مع OSD", + "single line hack": "حيلة التجاوز: تعامل مع الصورة كسطر نص واحد" + }, + "threshold": { + "otsu": "Otsu (افتراضي)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola" + }, + "default values": "القيم الافتراضية", + "output formats": "تنسيقات الإخراج", + "language": "اللغة", + "languages_section": "اللغات", + "special modules": "الوحدات الخاصة", + "more outputs": "المزيد من التنسيقات", + "tesseract thresholding": "عتبة Tesseract", + "language hint": "للحصول على أفضل النتائج، حدد حسب الأهمية", + "language required": "يجب تحديد لغة واحدة على الأقل", + "output required": "يجب تحديد تنسيق إخراج واحد على الأقل", + "dpi": "DPI (نقطة في البوصة)", + "ocr engine": "محرك التعرف الضوئي", + "engine mode": "وضع المحرك", + "segmentation": "التقسيم", + "thresholding": "العتبة", + "additional parameters": "معاملات إضافية", + "choose preset": "اختر تكويناً مسبقاً", + "select ocr preset": "اختر إعداد مسبق لتقنية التعرف الضوئي", + "use default": "استخدم الافتراضي", + "presets": { + "default": "افتراضي", + "fast": "سريع", + "balanced": "متوازن", + "high-quality": "جودة عالية", + "degraded-documents": "مستندات متدهورة", + "multi-column": "متعدد الأعمدة", + "tables-forms": "جداول/نماذج", + "default_desc": "التكوين الافتراضي للنظام", + "fast_desc": "معالجة سريعة - مثالي للمستندات البسيطة والنظيفة", + "balanced_desc": "توازن بين السرعة والجودة - خيار عام جيد", + "high_quality_desc": "دقة قصوى - للمستندات المهمة", + "degraded_documents_desc": "محسّن للمستندات القديمة أو ذات الجودة المنخفضة", + "multi_column_desc": "للصحف والمجلات والتخطيطات متعددة الأعمدة", + "tables_forms_desc": "محسّن للجداول والنماذج والبيانات المنظمة" + }, + "lose results": "ستفقد نتائجك الأخيرة والتغييرات السابقة!", + "begin": "بدء", + "clear all": "مسح الكل", + "alter existing config": "تعديل التكوين الحالي", + "sync_title": "مزامنة الملفات الخارجية", + "sync_description": "استيراد الملفات التي تمت إضافتها مباشرة إلى مجلد التخزين. اختر نطاق المزامنة:", + "sync_current": "المجلد الحالي فقط", + "sync_recursive": "جميع المجلدات الفرعية", + "sync_success": "تم استيراد {count} ملف(ات)", + "sync_skipped": "تم تخطي {count} ملف(ات) موجود(ة)", + "sync_no_new": "لم يتم العثور على ملفات جديدة", + "sync_error": "خطأ في مزامنة الملفات", + "email": "البريد الإلكتروني", + "password": "كلمة المرور", + "submit": "إرسال", + "logout": "تسجيل الخروج", + "confirm": "تأكيد", + "refresh": "تحديث", + "never": "أبداً", + "days": "يوم (أيام)", + "hours": "ساعات", + "every": "كل", + "hour": "ساعة", + "day": "يوم", + "admin": { + "login_title": "تسجيل دخول مسؤول OCR", + "email_password_incorrect": "البريد الإلكتروني أو كلمة المرور غير صحيحة", + "free_storage": "مساحة التخزين المتاحة", + "manage_storage": "إدارة التخزين", + "configure_ocr_defaults": "تكوين إعدادات OCR الافتراضية", + "view_workers_processes": "عرض العمليات والمعالجات", + "last_cleanup": "آخر تنظيف", + "last_update": "آخر تحديث", + "remove_private_spaces_older": "إزالة المساحات الخاصة الأقدم من", + "change_max_age": "تغيير الحد الأقصى للعمر", + "api_documents": "مستندات API", + "private_spaces": "المساحات الخاصة", + "confirm_delete_space": "هل أنت متأكد من رغبتك في حذف المساحة", + "confirm_delete_document": "هل أنت متأكد من رغبتك في حذف المستند بالمعرف", + "confirm_remove_sessions": "هل أنت متأكد من رغبتك في إزالة الجلسات الأقدم من", + "set_cleanup_schedule": "تعيين جدول التنظيف التلقائي", + "by_interval": "بفترات زمنية", + "weekly": "أسبوعياً", + "week_days": "أيام الأسبوع", + "select_at_least_one_day": "يجب تحديد يوم واحد على الأقل", + "monthly": "شهرياً", + "dpi_integer_error": "يجب أن تكون قيمة DPI عدداً صحيحاً!", + "manage_ocr_configurations": "إدارة تكوينات OCR", + "editing_configuration": "تعديل التكوين", + "default_config_cannot_delete": "لا يمكن حذف التكوين الافتراضي", + "creating_new_configuration": "إنشاء تكوين جديد:", + "default_config_must_define": "يجب على التكوين الافتراضي تحديد المعاملات الإلزامية", + "select_at_least_one_output": "يجب تحديد تنسيق إخراج واحد على الأقل", + "select_at_least_one_language": "يجب تحديد لغة واحدة على الأقل", + "save_configuration": "حفظ التكوين", + "confirm_delete_configuration": "هل أنت متأكد من رغبتك في حذف التكوين", + "request_failed": "تعذر إكمال الطلب.", + "hours_positive_integer": "يجب أن يكون عدد الساعات عدداً صحيحاً موجباً!", + "day_between_1_31": "يجب أن يكون اليوم رقماً بين 1 و 31!", + "folder_concurrency_title": "التحكم في التزامن لمعالجة المجلدات OCR", + "max_concurrent_folders": "الحد الأقصى للمجلدات المتزامنة", + "active_folders": "المجلدات النشطة", + "queued_folders": "المجلدات في قائمة الانتظار", + "folder_concurrency_updated": "تم تحديث حد التزامن للمجلدات" + }, + "weekdays": { + "monday": "الاثنين", + "tuesday": "الثلاثاء", + "wednesday": "الأربعاء", + "thursday": "الخميس", + "friday": "الجمعة", + "saturday": "السبت", + "sunday": "الأحد" + }, + "immediate ocr": "التعرف الفوري على الحروف", + "quick process": "معالجة سريعة", + "upload for immediate ocr": "تحميل مستند للمعالجة الفورية", + "select output formats": "تحديد تنسيقات الإخراج", + "plain text": "نص عادي", + "pdf simple": "PDF (بسيط)", + "pdf searchable": "PDF (قابل للبحث/مفهرس)", + "process now": "معالجة الآن", + "download text": "تنزيل النص", + "processing document": "معالجة المستند...", + "upload new document": "تحميل مستند جديد", + "rerun with different settings": "إعادة التشغيل بإعدادات مختلفة", + "temporary processing note": "تتم معالجة الملفات مؤقتًا وسيتم حذفها عند تحميل مستند جديد أو مغادرة هذه الصفحة.", + "drag drop files": "اسحب الملفات هنا أو انقر للتحديد", + "file uploaded": "تم تحميل الملف", + "no file uploaded": "لم يتم تحميل أي ملف", + "clear results": "مسح النتائج", + "select languages": "تحديد اللغات", + "select preset": "تحديد الإعداد المسبق", + "results ready": "النتائج جاهزة", + "download results": "تنزيل النتائج", + "file too large": "الملف كبير جدًا", + "unsupported file type": "نوع الملف غير مدعوم", + "upload failed": "فشل التحميل", + "processing failed": "فشلت المعالجة", + "return to home": "العودة إلى الصفحة الرئيسية", + "enable compression": "تفعيل ضغط PDF", + "compression info": "يقلل من حجم الملف ولكنه يستغرق وقتًا أطول ويستخدم المزيد من الذاكرة. قم بإلغاء التنشيط للمعالجة الأسرع.", + "compressing pdf starting": "ضغط PDF - البدء...", + "compressing pdf page": "ضغط PDF - صفحة {{current}}/{{total}}", + "compressing pdf finalizing": "ضغط PDF - الانتهاء...", + "compressing pdf": "ضغط PDF", + "compression complete": "اكتمل الضغط", + "compression quality": "جودة الضغط", + "compression auto": "تلقائي (موصى به)", + "compression auto desc": "يختار تلقائياً أفضل جودة بناءً على حجم الملف", + "compression fast": "سريع", + "compression fast desc": "معالجة أسرع، جودة أقل قليلاً (جيد للملفات الكبيرة)", + "compression high quality": "جودة عالية", + "compression high quality desc": "أفضل جودة، معالجة أبطأ (جيد للملفات الصغيرة)", + "compression settings": "إعدادات الضغط", + "compression target dpi": "DPI المستهدف", + "compression bg quality": "جودة الخلفية (1-100)", + "compression fg quality": "جودة المقدمة (1-100)", + "compression flatten to jpeg": "تسطيح إلى طبقة JPEG واحدة", + "compressing pdf starting": "ضغط PDF - جارٍ البدء...", + "compressing pdf page": "ضغط PDF - الصفحة {{current}}/{{total}}", + "compressing pdf finalizing": "ضغط PDF - جارٍ الإنهاء...", + "compressing pdf": "ضغط PDF", + "compression complete": "اكتمل الضغط", + "processing ocr": "معالجة OCR", + "processing ocr page": "معالجة OCR - الصفحة {{current}}/{{total}}", + "compressing file": "ضغط الملف الأصلي...", + "compressing file page": "ضغط - الصفحة {{current}}/{{total}}", + "adding text layer": "إضافة طبقة نص OCR...", + "adding text page": "إضافة نص - الصفحة {{current}}/{{total}}", + "completed": "مكتمل", + "error must select language": "يجب تحديد لغة واحدة على الأقل", + "error must select output": "يجب تحديد تنسيق إخراج واحد على الأقل", + "helper text language order": "للحصول على أفضل النتائج، حدد حسب الأهمية", + "error fetch default config": "تعذر استرداد أحدث التكوينات الافتراضية", + "error fetch preset": "تعذر استرداد التكوين المسبق", + "error fetch presets list": "تعذر تحديث قائمة التكوينات المسبقة", + "error dpi must be integer": "يجب أن تكون قيمة DPI عدداً صحيحاً!", + "success config saved": "تم حفظ تكوين OCR بنجاح.", + "error config save unexpected": "خطأ غير متوقع أثناء حفظ تكوين OCR.", + "error config save failed": "تعذر حفظ تكوين OCR.", + "queue": { + "title": "حالة قائمة انتظار المهام", + "page_title": "حالة قائمة الانتظار", + "tooltip": "حالة قائمة الانتظار", + "view_details": "انقر لعرض تفاصيل قائمة الانتظار", + "active": "نشطة", + "pending": "في قائمة الانتظار", + "queued": "المهام في قائمة الانتظار", + "processing": "قيد المعالجة", + "finished": "اكتمل", + "ago": "منذ", + "queued_files": "الملفات في انتظار قائمة الانتظار التسلسلية", + "scheduled": "المهام المجدولة", + "workers": "العمال", + "idle": "خامل", + "error": "خطأ في تحميل حالة قائمة الانتظار", + "task_breakdown": "تفصيل المهام", + "currently_processing": "قيد المعالجة حالياً", + "waiting_in_queue": "في انتظار قائمة الانتظار", + "scheduled_for_later": "مجدول لاحقاً", + "worker_details": "تفاصيل العمال", + "cancel": "إلغاء", + "remove": "إزالة", + "cancel_confirm": "هل أنت متأكد من إلغاء هذا OCR؟", + "remove_confirm": "هل أنت متأكد من إزالة هذا الملف من قائمة الانتظار؟", + "cancel_success": "تم إلغاء OCR بنجاح", + "remove_success": "تم إزالة الملف من قائمة الانتظار", + "cancel_error": "فشل إلغاء OCR", + "remove_error": "فشل إزالة الملف من قائمة الانتظار", + "currently_processing_files": "الملفات قيد المعالجة", + "folder_queue_status": "حالة قائمة انتظار المجلدات", + "active_folders": "المجلدات قيد المعالجة", + "queued_folders_display": "المجلدات في قائمة الانتظار", + "files": "ملفات", + "position": "موضع", + "task": { + "file_ocr": "OCR ملف", + "ocr_from_api": "OCR من API", + "page_ocr": "OCR صفحة", + "export_file": "تصدير ملف", + "prepare_file": "تحضير ملف", + "auto_segment": "تقسيم تلقائي" + } + }, + "ocr_help": { + "language": "حدد اللغة (اللغات) الموجودة في المستند. يمكن تحديد لغات متعددة إذا كان المستند يحتوي على لغات مختلطة. الترتيب مهم: قم بإدراج اللغة الأكثر انتشاراً أولاً للحصول على أفضل دقة.", + "special_modules": "وحدات الكشف الخاصة:\n• وحدة الكشف عن الرياضيات (equ): تمكن التعرف على المعادلات والصيغ الرياضية\n• الكشف عن الاتجاه والنص (osd): يكتشف تلقائياً اتجاه الصفحة ونظام الكتابة", + "dpi": "النقاط في البوصة - تحدد دقة الصورة الممسوحة ضوئياً. القيم الأعلى (300-600) توفر دقة أفضل ولكن معالجة أبطأ. اتركها فارغة لاستخدام الدقة الأصلية للمستند. القيم النموذجية: 150 (مسودة)، 300 (قياسي)، 600 (جودة عالية).", + "preprocessing": "خط معالجة مسبقة للصورة يحسن جودة الصورة قبل OCR. يطبق تحويل إلى تدرج رمادي، وتحسين التباين (CLAHE)، وتقليل الضوضاء (التمويه المتوسط)، والعتبة، والعمليات المورفولوجية، والتصحيح الاختياري للميل. قم بالتفعيل للحصول على نتائج أفضل مع الفحوصات ذات الجودة المنخفضة أو المستندات المتدهورة.", + "engine_mode": "طريقة معالجة محرك OCR:\n• Original: محرك قديم، أسرع لكن أقل دقة\n• LSTM: قائم على الشبكة العصبية، أكثر دقة للخطوط الحديثة\n• Combined: يستخدم كلا المحركين للحصول على أقصى دقة (أبطأ)\n• Default: الإعداد الموصى به من النظام (عادة LSTM)", + "segmentation": "كيف يحدد المحرك مناطق النص على الصفحة:\n• Default: الكشف التلقائي (موصى به لمعظم المستندات)\n• Single line/word/char: للصور التي تحتوي على عنصر نص واحد فقط\n• أوضاع Column/Block: لأنواع محددة من التخطيط (الصحف، النماذج)\n• Sparse text: للمستندات ذات عناصر النص المتناثرة", + "thresholding": "طريقة ثنائية الصورة لتحويل التدرج الرمادي إلى أبيض وأسود:\n• Otsu: حساب العتبة التلقائية (موصى به لمعظم الحالات)\n• Leptonica: تطبيق بديل لـ Otsu\n• Sauvola: طريقة تكيفية، أفضل للمستندات المتدهورة ذات الخلفية المتغيرة", + "tesseract_thresholding": "طريقة ثنائية الصورة المدمجة في Tesseract. هذا منفصل عن عتبة خط المعالجة المسبقة. يطبق فقط إذا كانت المعالجة المسبقة معطلة أو مضبوطة على 'None'. الخيارات: Otsu، Leptonica، Sauvola.", + "additional_params": "معاملات Tesseract المتقدمة بتنسيق 'مفتاح=قيمة'. مثال: 'tessedit_char_whitelist=0123456789' للتعرف على الأرقام فقط. راجع وثائق Tesseract للخيارات المتاحة.", + "compress_pdf": "يطبق ضغط الصور لتقليل حجم ملف PDF بنسبة 60-80٪ مع الحفاظ على دقة النص. موصى به لمعظم المستندات. قم بإلغاء التنشيط فقط إذا كنت بحاجة إلى جودة الصورة الأصلية أو تواجه مشاكل في المعالجة.", + "compression_target_dpi": "دقة الإخراج بالنقاط في البوصة. القيم الأقل (50-100) تنشئ ملفات أصغر بجودة مقبولة للنص. القيم الأعلى (150-300) تحافظ على المزيد من التفاصيل لكن تزيد حجم الملف. الافتراضي: 100 DPI.", + "compression_bg_quality": "جودة JPEG لخلفية/صور المستند (1-100). القيم الأقل تنشئ ملفات أصغر. موصى به: 30-50 للخلفيات. النص لا يتأثر.", + "compression_fg_quality": "جودة JPEG لطبقة النص/المقدمة (1-100). القيم الأعلى تحافظ على وضوح النص. موصى به: 70-90 للنص المقروء. الافتراضي: 80.", + "compression_flatten": "عند التفعيل، يدمج جميع الطبقات في صورة JPEG واحدة. ينشئ ملفات PDF أبسط لكن ضغط أقل كفاءة. قم بإلغاء التنشيط للحصول على أفضل ضغط (يستخدم MRC - محتوى نقطي مختلط مع طبقات خلفية ونص منفصلة).", + "output_formats": "حدد تنسيق إخراج واحد أو أكثر:\n• PDF مع فهرس: PDF قابل للبحث مع إحداثيات على مستوى الكلمة\n• PDF مع نص: PDF قابل للبحث قياسي (موصى به)\n• نص عادي: نص فقط، بدون تنسيق\n• نص محدد بالصفحة: نص مع فواصل الصفحات\n• CSV: إحداثيات الكلمات بتنسيق جدول بيانات\n• NER: كيانات مسماة (أشخاص، أماكن، منظمات)\n• hOCR/ALTO: تنسيقات منظمة مع معلومات تخطيط تفصيلية" + }, + "confirm delete": "هل أنت متأكد من رغبتك في حذف", + "page count": "صفحة (صفحات)", + "download starting": "سيبدأ التنزيل قريباً", + "uncompressed": "غير مضغوط", + "pdf with index": "PDF مع النص وفهرس الكلمات", + "pdf with index uncompressed": "PDF مع النص والفهرس (غير مضغوط)", + "pdf with text": "PDF مع النص", + "pdf with text uncompressed": "PDF مع النص (غير مضغوط)", + "plain text file": "نص", + "text with page separators": "نص مع فواصل الصفحات", + "word index csv": "فهرس الكلمات بتنسيق CSV", + "entities": "كيانات", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "صور مستخرجة" +} diff --git a/website/src/Languages/Chinese/translation.json b/website/src/Languages/Chinese/translation.json new file mode 100644 index 00000000..5c44a094 --- /dev/null +++ b/website/src/Languages/Chinese/translation.json @@ -0,0 +1,429 @@ +{ + "welcome": "欢迎", + "login": "登录", + "private space": "私人空间", + "new folder": "新建文件夹", + "new document": "上传文档", + "leave space": "离开空间", + "back": "返回", + "start": "开始", + "grid view": "网格视图", + "list view": "列表视图", + "empty folder title": "此文件夹为空", + "empty folder description": "添加文档或创建子文件夹以开始。", + "uploading": "上传中", + "ocr complete": "OCR 完成", + "pages": "页", + "document": "文档", + "documents": "文档", + "folder": "文件夹", + "folders": "文件夹", + "see document": "查看文档", + "edit text": "编辑文本", + "repeat ocr": "重复 OCR", + "run ocr": "运行 OCR", + "download txt": "下载 TXT", + "download pdf": "下载 PDF", + "download images": "下载图片", + "download original": "下载原始文件", + "open folder": "打开文件夹", + "custom config": "自定义 OCR 配置", + "search": "搜索", + "sync": "同步", + "finish": "完成", + "name": "名称", + "details": "详情", + "size": "大小", + "date of creation": "创建日期", + "process state": "处理状态", + "version": "版本", + "user manual": "用户手册", + "title": "光学字符识别工具", + "folder without files": "此文件夹为空", + "sub-folder": "子文件夹", + "page": "页面", + "words": "单词", + "config ocr": "配置 OCR", + "cancel ocr": "取消 OCR", + "config strategy": "配置策略", + "config strategy override": "对所有文件使用文件夹配置", + "config strategy respect": "尊重每个文件的单独配置", + "config strategy hybrid": "如果文件配置存在则使用,否则使用文件夹配置", + "config strategy hint": "选择如何将配置应用于此文件夹中的文件", + "clean all": "清除全部", + "auto layout": "自动布局", + "edit results": "编辑结果", + "delete": "删除", + "index": "索引", + "deindex": "取消索引", + "save": "保存", + "close": "关闭", + "retry": "重试", + "advanced": "高级", + "advanced ocr settings": "高级OCR设置", + "upload error": "文件上传错误", + "file download started": "文件下载已开始,请稍候", + "folder created successfully": "文件夹创建成功", + "folder name required": "您必须为文件夹分配名称", + "ocr started": "OCR 已开始,请稍候", + "folder queued": "正在文件夹队列中等待", + "edited results files to recreate": "已编辑结果,文件需要重新创建", + "queued file": "队列中(文件 {{current}}/{{total}})", + "processing ocr page": "正在处理 OCR - 第 {{current}}/{{total}} 页", + "generating results": "正在生成结果", + "generating text": "正在生成文本", + "generating delimited text": "正在生成分隔文本", + "generating pdf with index": "正在生成带索引的 PDF", + "generating pdf": "正在生成 PDF", + "generating csv": "正在生成 CSV", + "generating images": "正在生成图像", + "generating hocr": "正在生成 hOCR", + "generating alto xml": "正在生成 ALTO XML", + "generating index": "正在生成索引", + "generating pdf with index page": "正在生成带索引的 PDF {{current}}/{{total}}", + "generating pdf page": "正在生成 PDF {{current}}/{{total}}", + "compression complete": "压缩完成", + "compressing pdf finalizing": "正在压缩 PDF - 完成中...", + "compressing pdf": "正在压缩 PDF", + "compressing file": "正在压缩原始文件...", + "error preparing document": "准备文档时出错", + "error during ocr": "OCR 过程中出错", + "error during ocr page": "第 {{page}} 页 OCR 过程中出错", + "error generating results": "生成结果时出错", + "ocr interrupted retry": "OCR 已中断 - 您可以重试", + "invalid parameters": "参数无效", + "ocr cancelled": "OCR已取消", + "error canceling ocr": "取消OCR时出错", + "uploading stage": "上传中,请稍候...", + "preparing stage": "准备文档中...", + "preparing ocr": "准备OCR中...", + "confirm leave title": "您确定要离开吗?", + "confirm leave warning": "如果您不保存就离开,您将丢失所做的任何更改!", + "confirm leave button": "不保存就离开", + "save and leave button": "保存并离开", + "saving": "保存中", + "compressing": "压缩中", + "exporting": "导出中", + "ignore extract": "忽略/提取", + "join": "合并", + "separate": "分离", + "replicate": "复制", + "create new folder": "创建新文件夹", + "folder name": "文件夹名称*", + "folder name extra": "名称不能以 '_' 开头,也不能包含 '/' 或 '\\'", + "create": "创建", + "auto layout popup": "准备布局中,请稍候...", + "layout loading": "正在创建布局...", + "layout create": "创建布局", + "configure ocr": "配置 OCR", + "of document": "文档的", + "of folder": "文件夹的", + "languages": { + "arabic": "阿拉伯语", + "chinese_simplified": "中文(简体)", + "chinese_traditional": "中文(繁体)", + "german": "德语", + "english": "英语", + "french": "法语", + "hindi": "印地语", + "indonesian": "印尼语(Bahasa Indonesia)", + "italian": "意大利语", + "portuguese": "葡萄牙语", + "russian": "俄语", + "spanish": "西班牙语", + "math module": "数学检测模块", + "osd module": "方向和脚本检测模块" + }, + "output": { + "pdf indexed": "带文本和单词索引的 PDF", + "pdf": "带文本的 PDF(默认)", + "txt": "纯文本", + "txt delimited": "分页文本", + "csv": "CSV 格式的单词索引", + "ner": "命名实体(NER)", + "hocr": "hOCR(仅限单页文档)", + "xml": "ALTO(仅限单页文档)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract 原始", + "lstm": "Tesseract LSTM", + "combined": "LSTM + 原始组合", + "default": "默认模式" + }, + "segmentation mode": { + "auto with osd": "带 OSD 的自动页面分割", + "auto no osd": "无 OSD 或 OCR 的自动分割", + "default": "(默认)自动分割,无 OSD", + "column variable lines": "可变行大小的文本列", + "block vertical": "统一垂直文本块", + "block uniform": "统一文本块", + "single line": "单行文本图像", + "single word": "单个单词图像", + "single circle word": "圆圈中的单个单词图像", + "single char": "单个字符图像", + "sparse text": "稀疏文本;无序查找尽可能多的内容", + "sparse text osd": "带 OSD 的稀疏文本", + "single line hack": "绕过技巧:将图像视为单行文本" + }, + "threshold": { + "otsu": "Otsu(默认)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola" + }, + "default values": "默认值", + "output formats": "输出格式", + "language": "语言", + "languages_section": "语言", + "special modules": "特殊模块", + "more outputs": "更多格式", + "tesseract thresholding": "Tesseract 阈值处理", + "language hint": "为获得最佳结果,请按相关性顺序选择", + "language required": "您必须至少选择一种语言", + "output required": "您必须至少选择一种输出格式", + "dpi": "DPI(每英寸点数)", + "ocr engine": "OCR 引擎", + "engine mode": "引擎模式", + "segmentation": "分割", + "thresholding": "阈值处理", + "additional parameters": "附加参数", + "choose preset": "选择预设配置", + "select ocr preset": "选择OCR预设", + "use default": "使用默认", + "presets": { + "default": "默认", + "fast": "快速", + "balanced": "平衡", + "high-quality": "高质量", + "degraded-documents": "劣化文档", + "multi-column": "多列", + "tables-forms": "表格/表单", + "default_desc": "系统默认配置", + "fast_desc": "快速处理 - 适用于简单清晰的文档", + "balanced_desc": "速度和质量的平衡 - 良好的通用选择", + "high_quality_desc": "最高精度 - 用于重要文档", + "degraded_documents_desc": "针对旧文档或低质量扫描优化", + "multi_column_desc": "适用于报纸、杂志和多列布局", + "tables_forms_desc": "针对表格、表单和结构化数据优化" + }, + "lose results": "您将丢失上次的结果和之前的更改!", + "begin": "开始", + "clear all": "清除全部", + "alter existing config": "修改现有配置", + "sync_title": "同步外部文件", + "sync_description": "导入直接添加到存储文件夹的文件。选择同步范围:", + "sync_current": "仅当前文件夹", + "sync_recursive": "所有子文件夹", + "sync_success": "已导入 {count} 个文件", + "sync_skipped": "已跳过 {count} 个现有文件", + "sync_no_new": "未找到新文件", + "sync_error": "同步文件时出错", + "email": "电子邮件", + "password": "密码", + "submit": "提交", + "logout": "退出", + "confirm": "确认", + "refresh": "刷新", + "never": "从未", + "days": "天", + "hours": "小时", + "every": "每", + "hour": "小时", + "day": "天", + "admin": { + "login_title": "OCR 管理员登录", + "email_password_incorrect": "电子邮件或密码不正确", + "free_storage": "可用存储空间", + "manage_storage": "管理存储", + "configure_ocr_defaults": "配置 OCR 默认设置", + "view_workers_processes": "查看工作进程", + "last_cleanup": "上次清理", + "last_update": "最后更新", + "remove_private_spaces_older": "删除早于", + "change_max_age": "更改最大期限", + "api_documents": "API 文档", + "private_spaces": "私人空间", + "confirm_delete_space": "您确定要删除空间", + "confirm_delete_document": "您确定要删除 ID 为", + "confirm_remove_sessions": "您确定要删除早于", + "set_cleanup_schedule": "设置自动清理计划", + "by_interval": "按间隔", + "weekly": "每周", + "week_days": "星期", + "select_at_least_one_day": "您必须至少选择一天", + "monthly": "每月", + "dpi_integer_error": "DPI 值必须是整数!", + "manage_ocr_configurations": "管理 OCR 配置", + "editing_configuration": "编辑配置", + "default_config_cannot_delete": "无法删除默认配置", + "creating_new_configuration": "创建新配置:", + "default_config_must_define": "默认配置必须定义必需参数", + "select_at_least_one_output": "您必须至少选择一种输出格式", + "select_at_least_one_language": "您必须至少选择一种语言", + "save_configuration": "保存配置", + "confirm_delete_configuration": "您确定要删除配置", + "request_failed": "无法完成请求。", + "hours_positive_integer": "小时数必须是正整数!", + "day_between_1_31": "天必须是 1 到 31 之间的数字!", + "folder_concurrency_title": "文件夹 OCR 并发控制", + "max_concurrent_folders": "最大并发文件夹数", + "active_folders": "活动文件夹", + "queued_folders": "队列中的文件夹", + "folder_concurrency_updated": "文件夹并发限制已更新" + }, + "weekdays": { + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日" + }, + "immediate ocr": "即时OCR", + "quick process": "快速处理", + "upload for immediate ocr": "上传文档进行即时处理", + "select output formats": "选择输出格式", + "plain text": "纯文本", + "pdf simple": "PDF(简单)", + "pdf searchable": "PDF(可搜索/已索引)", + "process now": "立即处理", + "download text": "下载文本", + "processing document": "正在处理文档...", + "upload new document": "上传新文档", + "rerun with different settings": "使用不同设置重新运行", + "temporary processing note": "文件临时处理,上传新文档或离开此页面时将被删除。", + "drag drop files": "将文件拖到此处或单击以选择", + "file uploaded": "文件已上传", + "no file uploaded": "未上传文件", + "clear results": "清除结果", + "select languages": "选择语言", + "select preset": "选择预设", + "results ready": "结果已准备好", + "download results": "下载结果", + "file too large": "文件太大", + "unsupported file type": "不支持的文件类型", + "upload failed": "上传失败", + "processing failed": "处理失败", + "return to home": "返回主页", + "enable compression": "启用PDF压缩", + "compression info": "减小文件大小但需要更长时间和更多内存。禁用以加快处理速度。", + "compressing pdf starting": "压缩PDF - 开始...", + "compressing pdf page": "压缩PDF - 第{{current}}/{{total}}页", + "compressing pdf finalizing": "压缩PDF - 完成中...", + "compressing pdf": "压缩PDF", + "compression complete": "压缩完成", + "compression quality": "压缩质量", + "compression auto": "自动(推荐)", + "compression auto desc": "根据文件大小自动选择最佳质量", + "compression fast": "快速", + "compression fast desc": "处理更快,质量略低(适合大文件)", + "compression high quality": "高质量", + "compression high quality desc": "最佳质量,处理较慢(适合小文件)", + "compression settings": "压缩设置", + "compression target dpi": "目标DPI", + "compression bg quality": "背景质量(1-100)", + "compression fg quality": "前景质量(1-100)", + "compression flatten to jpeg": "压平为单一JPEG层", + "compressing pdf starting": "正在压缩PDF - 开始中...", + "compressing pdf page": "正在压缩PDF - 第{{current}}/{{total}}页", + "compressing pdf finalizing": "正在压缩PDF - 完成中...", + "compressing pdf": "正在压缩PDF", + "compression complete": "压缩完成", + "processing ocr": "正在处理OCR", + "processing ocr page": "正在处理OCR - 第{{current}}/{{total}}页", + "compressing file": "正在压缩原始文件...", + "compressing file page": "正在压缩 - 第{{current}}/{{total}}页", + "adding text layer": "正在添加OCR文本层...", + "adding text page": "正在添加文本 - 第{{current}}/{{total}}页", + "completed": "已完成", + "error must select language": "必须至少选择一种语言", + "error must select output": "必须至少选择一种输出格式", + "helper text language order": "为获得最佳结果,请按相关性顺序选择", + "error fetch default config": "无法检索最新的默认配置", + "error fetch preset": "无法检索预设配置", + "error fetch presets list": "无法更新预设配置列表", + "error dpi must be integer": "DPI值必须是整数!", + "success config saved": "OCR配置保存成功。", + "error config save unexpected": "保存OCR配置时发生意外错误。", + "error config save failed": "无法保存OCR配置。", + "queue": { + "title": "任务队列状态", + "page_title": "队列状态", + "tooltip": "队列状态", + "view_details": "点击查看队列详情", + "active": "活跃", + "pending": "排队中", + "queued": "排队任务", + "processing": "处理中", + "finished": "已完成", + "ago": "前", + "queued_files": "顺序队列中等待的文件", + "scheduled": "计划任务", + "workers": "工作进程", + "idle": "空闲", + "error": "加载队列状态时出错", + "task_breakdown": "任务分解", + "currently_processing": "当前处理中", + "waiting_in_queue": "队列中等待", + "scheduled_for_later": "稍后计划", + "worker_details": "工作进程详情", + "cancel": "取消", + "remove": "移除", + "cancel_confirm": "您确定要取消此OCR吗?", + "remove_confirm": "您确定要从队列中移除此文件吗?", + "cancel_success": "OCR已成功取消", + "remove_success": "文件已从队列中移除", + "cancel_error": "取消OCR失败", + "remove_error": "从队列中移除文件失败", + "currently_processing_files": "当前处理中的文件", + "folder_queue_status": "文件夹队列状态", + "active_folders": "处理中的文件夹", + "queued_folders_display": "队列中的文件夹", + "files": "文件", + "position": "位置", + "task": { + "file_ocr": "文件OCR", + "ocr_from_api": "API OCR", + "page_ocr": "页面OCR", + "export_file": "导出文件", + "prepare_file": "准备文件", + "auto_segment": "自动分割" + } + }, + "ocr_help": { + "language": "选择文档中存在的语言。如果文档包含混合语言,可以选择多种语言。顺序很重要:首先列出最常见的语言以获得最佳准确性。", + "special_modules": "特殊检测模块:\n• 数学检测模块(equ):启用数学方程和公式的识别\n• 方向和脚本检测(osd):自动检测页面方向和书写系统", + "dpi": "每英寸点数 - 定义扫描图像的分辨率。较高的值(300-600)提供更好的准确性但处理较慢。留空以使用文档的原生分辨率。典型值:150(草稿),300(标准),600(高质量)。", + "preprocessing": "图像预处理管道,在OCR之前增强图像质量。应用灰度转换、对比度增强(CLAHE)、降噪(中值模糊)、阈值处理、形态学操作和可选的倾斜校正。启用此选项可获得更好的低质量扫描或劣化文档的结果。", + "engine_mode": "OCR引擎处理方法:\n• Original:传统引擎,更快但准确性较低\n• LSTM:基于神经网络,对现代字体更准确\n• Combined:使用两个引擎以获得最大准确性(较慢)\n• Default:系统推荐设置(通常为LSTM)", + "segmentation": "引擎如何识别页面上的文本区域:\n• Default:自动检测(推荐用于大多数文档)\n• Single line/word/char:用于仅包含一个文本元素的图像\n• Column/Block模式:用于特定布局类型(报纸、表单)\n• Sparse text:用于具有分散文本元素的文档", + "thresholding": "图像二值化方法,用于将灰度转换为黑白:\n• Otsu:自动阈值计算(推荐用于大多数情况)\n• Leptonica:Otsu的替代实现\n• Sauvola:自适应方法,更适合背景变化的劣化文档", + "tesseract_thresholding": "Tesseract的内置图像二值化方法。这与预处理管道阈值是分开的。仅在预处理被禁用或设置为'None'时应用。选项:Otsu、Leptonica、Sauvola。", + "additional_params": "高级Tesseract参数,格式为'键=值'。示例:'tessedit_char_whitelist=0123456789'仅识别数字。请参阅Tesseract文档以了解可用选项。", + "compress_pdf": "应用图像压缩以将PDF文件大小减少60-80%,同时保持文本准确性。推荐用于大多数文档。仅在需要原始图像质量或遇到处理问题时禁用。", + "compression_target_dpi": "输出分辨率(每英寸点数)。较低的值(50-100)创建较小的文件,文本质量可接受。较高的值(150-300)保留更多细节但增加文件大小。默认:100 DPI。", + "compression_bg_quality": "文档背景/图像的JPEG质量(1-100)。较低的值创建较小的文件。推荐:背景为30-50。文本不受影响。", + "compression_fg_quality": "文本/前景层的JPEG质量(1-100)。较高的值保持文本清晰度。推荐:可读文本为70-90。默认:80。", + "compression_flatten": "启用时,将所有层合并为单个JPEG图像。创建更简单的PDF但压缩效率较低。禁用以获得最佳压缩(使用MRC - 混合光栅内容,具有独立的背景和文本层)。", + "output_formats": "选择一种或多种输出格式:\n• 带索引的PDF:可搜索的PDF,带有词级坐标\n• 带文本的PDF:标准可搜索PDF(推荐)\n• 纯文本:仅文本,无格式\n• 分页文本:带页面分隔符的文本\n• CSV:电子表格格式的词坐标\n• NER:命名实体(人物、地点、组织)\n• hOCR/ALTO:带有详细布局信息的结构化格式" + }, + "confirm delete": "您确定要删除", + "page count": "页", + "download starting": "您的下载即将开始", + "uncompressed": "未压缩", + "pdf with index": "带文本和单词索引的PDF", + "pdf with index uncompressed": "带文本和索引的PDF(未压缩)", + "pdf with text": "带文本的PDF", + "pdf with text uncompressed": "带文本的PDF(未压缩)", + "plain text file": "文本", + "text with page separators": "带页面分隔符的文本", + "word index csv": "CSV格式的单词索引", + "entities": "实体", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "提取的图像" +} diff --git a/website/src/Languages/English/translation.json b/website/src/Languages/English/translation.json new file mode 100644 index 00000000..bd7b7a52 --- /dev/null +++ b/website/src/Languages/English/translation.json @@ -0,0 +1,463 @@ +{ + "welcome": "Welcome", + "login": "Log in", + "private space": "Private Space", + "new folder": "New Folder", + "new document": "Upload Document", + "leave space": "Leave Space", + "back": "Back", + "start": "Start", + "grid view": "Grid View", + "list view": "List View", + "empty folder title": "This folder is empty", + "empty folder description": "Add a document or create a subfolder to get started.", + "uploading": "Uploading", + "ocr complete": "OCR Complete", + "pages": "pages", + "document": "document", + "documents": "documents", + "folder": "folder", + "folders": "folders", + "see document": "View Document", + "edit text": "Edit Text", + "repeat ocr": "Repeat OCR", + "run ocr": "Run OCR", + "download txt": "Download TXT", + "download pdf": "Download PDF", + "download images": "Download Images", + "download original": "Download Original", + "open folder": "Open Folder", + "custom config": "Custom OCR Configuration", + "search": "Search", + "sync": "Sync", + "finish": "Finish", + "name": "Name", + "details": "Details", + "size": "Size", + "date of creation": "Date of creation", + "process state": "Process State", + "version": "Version", + "user manual": "User Manual", + "title": "Optical Character Recognition Tool", + "folder without files": "This folder is empty", + "document": "Document", + "sub-folder": "Sub-folder", + "page": "Page", + "words": "Words", + "config ocr": "Configure OCR", + "cancel ocr": "Cancel OCR", + "config strategy": "Configuration Strategy", + "config strategy override": "Use folder config for all files", + "config strategy respect": "Respect each file's individual config", + "config strategy hybrid": "Use file config if exists, otherwise folder config", + "config strategy hint": "Choose how to apply configuration to files in this folder", + "clean all": "Clean all", + "auto layout": "Auto Layout", + "edit results": "Edit Results", + "delete": "Delete", + "index": "Index", + "deindex": "Deindex", + "save": "Save", + "close": "Close", + "retry": "Retry", + "advanced": "Advanced", + "advanced ocr settings": "Advanced OCR Settings", + "upload error": "File upload error", + "file download started": "File download started, please wait", + "folder created successfully": "Folder created successfully", + "folder name required": "You must assign a name to the folder", + "ocr started": "OCR started, please wait", + "folder queued": "Waiting in folder queue", + "edited results files to recreate": "Edited results, files to recreate", + "queued file": "In queue (file {{current}}/{{total}})", + "processing ocr page": "Processing OCR - Page {{current}}/{{total}}", + "generating results": "Generating results", + "generating text": "Generating text", + "generating delimited text": "Generating delimited text", + "generating pdf with index": "Generating PDF with index", + "generating pdf": "Generating PDF", + "generating csv": "Generating CSV", + "generating images": "Generating images", + "generating hocr": "Generating hOCR", + "generating alto xml": "Generating ALTO XML", + "generating index": "Generating index", + "generating pdf with index page": "Generating PDF with index {{current}}/{{total}}", + "generating pdf page": "Generating PDF {{current}}/{{total}}", + "compression complete": "Compression complete", + "compressing pdf finalizing": "Compressing PDF - Finalizing...", + "compressing pdf": "Compressing PDF", + "compressing file": "Compressing original file...", + "error preparing document": "Error preparing document", + "error during ocr": "Error during OCR", + "error during ocr page": "Error during OCR of page {{page}}", + "error generating results": "Error generating results", + "ocr interrupted retry": "OCR interrupted - you can try again", + "invalid parameters": "Invalid parameters", + "ocr cancelled": "OCR Cancelled", + "error canceling ocr": "Error canceling OCR", + "uploading stage": "Uploading, please wait...", + "preparing stage": "Preparing document...", + "preparing ocr": "Preparing OCR...", + "confirm leave title": "Are you sure you want to leave?", + "confirm leave warning": "If you leave without saving, you will lose any changes you have made!", + "confirm leave button": "Leave without saving", + "save and leave button": "Save and leave", + "saving": "Saving", + "compressing": "Compressing", + "exporting": "Exporting", + "ignore extract": "Ignore/Extract", + "join": "Join", + "separate": "Separate", + "replicate": "Replicate", + "create new folder": "Create a new folder", + "folder name": "Folder's name*", + "folder name extra": "The name can't start with '_' nor contain '/' or '\\'", + "create": "Create", + "auto layout popup": "Preparing layout, please wait...", + "layout loading": "Layout is being created...", + "layout create": "Create layout", + "configure ocr": "Configure OCR", + "of document": "of document", + "of folder": "of folder", + "languages": { + "arabic": "Arabic", + "chinese_simplified": "Chinese (Simplified)", + "chinese_traditional": "Chinese (Traditional)", + "german": "German", + "english": "English", + "french": "French", + "hindi": "Hindi", + "indonesian": "Indonesian (Bahasa Indonesia)", + "italian": "Italian", + "portuguese": "Portuguese", + "russian": "Russian", + "spanish": "Spanish", + "math module": "Math detection module", + "osd module": "Orientation and script detection module" + }, + "output": { + "pdf indexed": "PDF with text and word index", + "pdf": "PDF with text (default)", + "txt": "Plain text", + "txt delimited": "Page-delimited text", + "csv": "Word index in CSV format", + "ner": "Named Entities (NER)", + "hocr": "hOCR (only single-page documents)", + "xml": "ALTO (only single-page documents)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract Original", + "lstm": "Tesseract LSTM", + "combined": "Combined LSTM + Original", + "default": "Default mode" + }, + "segmentation mode": { + "auto with osd": "Automatic page segmentation with OSD", + "auto no osd": "Automatic segmentation without OSD or OCR", + "default": "(Default) Automatic segmentation, no OSD", + "column variable lines": "Text column with variable line sizes", + "block vertical": "Uniform vertical text block", + "block uniform": "Uniform text block", + "single line": "Image with a single line of text", + "single word": "Image with a single word", + "single circle word": "Image with a single word in a circle", + "single char": "Image with a single character", + "sparse text": "Sparse text; find as much as possible without order", + "sparse text osd": "Sparse text with OSD", + "single line hack": "Bypass trick: treat image as a single line of text" + }, + "threshold": { + "otsu": "Otsu (default)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola", + "adaptive_gaussian": "Adaptive Gaussian", + "none": "None (Preprocessing Only)" + }, + "preprocessing": { + "title": "Preprocessing Pipeline", + "enabled": "Enable Preprocessing", + "grayscale": "Convert to Grayscale", + "clahe": "CLAHE Enhancement", + "clahe_clip_limit": "Clip Limit", + "clahe_tile_size": "Tile Size", + "median_blur": "Median Blur", + "median_blur_kernel": "Kernel Size", + "threshold_method": "Threshold Method", + "adaptive_block_size": "Block Size", + "adaptive_c": "C Value", + "morphological_opening": "Morphological Opening", + "morphological_closing": "Morphological Closing", + "morph_kernel_size": "Morphology Kernel", + "deskew": "Auto-Deskew" + }, + "default values": "Default values", + "finish": "Finish", + "output formats": "Output formats", + "language": "Language", + "languages_section": "Languages", + "special modules": "Special Modules", + "more outputs": "More Outputs", + "tesseract thresholding": "Tesseract Thresholding", + "language hint": "For best results, select in order of relevance", + "language required": "You must select at least one language", + "output required": "You must select at least one output format", + "dpi": "DPI (Dots Per Inch)", + "ocr engine": "OCR Engine", + "engine mode": "Engine Mode", + "segmentation": "Segmentation", + "thresholding": "Thresholding", + "additional parameters": "Additional Parameters", + "compress pdf": "Compress PDF", + "compress pdf description": "Reduces PDF file size by 60-80% while maintaining text quality", + "choose preset": "Choose preset configuration", + "select ocr preset": "Select OCR Preset", + "use default": "Use default", + "presets": { + "default": "Default", + "fast": "Fast", + "balanced": "Balanced", + "high-quality": "High Quality", + "degraded-documents": "Degraded Documents", + "multi-column": "Multi-Column", + "tables-forms": "Tables/Forms", + "default_desc": "System default configuration", + "fast_desc": "Quick processing - ideal for simple, clean documents", + "balanced_desc": "Balance between speed and quality - good general choice", + "high_quality_desc": "Maximum accuracy - for important documents", + "degraded_documents_desc": "Optimized for old or poor quality scans", + "multi_column_desc": "For newspapers, magazines, and multi-column layouts", + "tables_forms_desc": "Optimized for tables, forms, and structured data" + }, + "lose results": "You will lose your last results and previous changes!", + "begin": "Start", + "clear all": "Clear All", + "alter existing config": "Alter Existing Config", + "sync": "Sync", + "sync_title": "Sync External Files", + "sync_description": "Import files that were added directly to the storage folder. Choose the scope of the sync:", + "sync_current": "Current Folder Only", + "sync_recursive": "All Subfolders", + "sync_success": "Imported {count} file(s)", + "sync_skipped": "Skipped {count} existing file(s)", + "sync_no_new": "No new files found", + "sync_error": "Error syncing files", + "email": "Email", + "password": "Password", + "submit": "Submit", + "logout": "Logout", + "confirm": "Confirm", + "refresh": "Refresh", + "never": "never", + "days": "day(s)", + "hours": "hours", + "every": "Every", + "hour": "Hour", + "day": "Day", + "admin": { + "login_title": "OCR Admin Login", + "email_password_incorrect": "Incorrect email or password", + "free_storage": "Free storage", + "manage_storage": "Manage Storage", + "configure_ocr_defaults": "Configure OCR Defaults", + "view_workers_processes": "View Workers and Processes", + "last_cleanup": "Last cleanup", + "last_update": "Last update", + "remove_private_spaces_older": "Remove private spaces older than", + "change_max_age": "Change maximum age", + "api_documents": "API Documents", + "private_spaces": "Private Spaces", + "confirm_delete_space": "Are you sure you want to delete space", + "confirm_delete_document": "Are you sure you want to delete document with ID", + "confirm_remove_sessions": "Are you sure you want to remove sessions older than", + "set_cleanup_schedule": "Set automatic cleanup schedule", + "by_interval": "By interval", + "weekly": "Weekly", + "week_days": "Week days", + "select_at_least_one_day": "You must select at least one day", + "monthly": "Monthly", + "dpi_integer_error": "DPI value must be an integer!", + "manage_ocr_configurations": "Manage OCR Configurations", + "editing_configuration": "Editing configuration", + "default_config_cannot_delete": "Default configuration cannot be deleted", + "creating_new_configuration": "Creating new configuration", + "default_config_must_define": "Default configuration must define mandatory parameters", + "select_at_least_one_output": "You must select at least one output format", + "select_at_least_one_language": "You must select at least one language", + "save_configuration": "Save configuration", + "confirm_delete_configuration": "Are you sure you want to delete configuration", + "request_failed": "Request could not be completed.", + "hours_positive_integer": "Number of hours must be a positive integer!", + "day_between_1_31": "Day must be a number between 1 and 31!", + "folder_concurrency_title": "Folder OCR Concurrency Control", + "max_concurrent_folders": "Maximum Concurrent Folders", + "active_folders": "Active folders", + "queued_folders": "Queued folders", + "folder_concurrency_updated": "Folder concurrency limit updated" + }, + "weekdays": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "immediate ocr": "Immediate OCR", + "quick process": "Quick Process", + "upload for immediate ocr": "Upload Document for Immediate Processing", + "select output formats": "Select Output Formats", + "plain text": "Plain Text", + "pdf simple": "PDF (Simple)", + "pdf searchable": "PDF (Searchable/Indexed)", + "process now": "Process Now", + "download text": "Download Text", + "processing document": "Processing document...", + "upload new document": "Upload New Document", + "rerun with different settings": "Re-run with Different Settings", + "temporary processing note": "Files are processed temporarily and will be deleted when you upload a new document or leave this page.", + "drag drop files": "Drag files here or click to select", + "file uploaded": "File uploaded", + "no file uploaded": "No file uploaded", + "clear results": "Clear Results", + "select languages": "Select Languages", + "select preset": "Select Preset", + "results ready": "Results Ready", + "download results": "Download Results", + "file too large": "File too large", + "unsupported file type": "Unsupported file type", + "upload failed": "Upload failed", + "processing failed": "Processing failed", + "return to home": "Return to Home", + "enable compression": "Enable PDF Compression", + "compression info": "Reduces file size but takes longer and uses more memory. Disable for faster processing.", + "compression quality": "Compression Quality", + "compression auto": "Auto (Recommended)", + "compression auto desc": "Automatically chooses best quality based on file size", + "compression fast": "Fast", + "compression fast desc": "Faster processing, slightly lower quality (good for large files)", + "compression high quality": "High Quality", + "compression high quality desc": "Best quality, slower processing (good for small files)", + "compression settings": "Compression Settings", + "compression target dpi": "Target DPI", + "compression bg quality": "Background Quality (1-100)", + "compression fg quality": "Foreground Quality (1-100)", + "compression flatten to jpeg": "Flatten to Single JPEG Layer", + "compressing pdf starting": "Compressing PDF - Starting...", + "compressing pdf page": "Compressing PDF - Page {{current}}/{{total}}", + "compressing pdf finalizing": "Compressing PDF - Finalizing...", + "compressing pdf": "Compressing PDF", + "compression complete": "Compression complete", + "processing ocr": "Processing OCR", + "processing ocr page": "Processing OCR - Page {{current}}/{{total}}", + "compressing file": "Compressing original file...", + "compressing file page": "Compressing - Page {{current}}/{{total}}", + "adding text layer": "Adding OCR text layer...", + "adding text page": "Adding text - Page {{current}}/{{total}}", + "completed": "Completed", + "error must select language": "You must select at least one language", + "error must select output": "You must select at least one output format", + "helper text language order": "For best results, select in order of relevance", + "error fetch default config": "Unable to retrieve the latest default configuration", + "error fetch preset": "Unable to retrieve the preset configuration", + "error fetch presets list": "Unable to update the list of preset configurations", + "error dpi must be integer": "The DPI value must be an integer!", + "success config saved": "OCR configuration saved successfully.", + "error config save unexpected": "Unexpected error while saving OCR configuration.", + "error config save failed": "Unable to save OCR configuration.", + "queue": { + "title": "Task Queue Status", + "page_title": "Queue Status", + "tooltip": "Queue Status", + "view_details": "Click to view queue details", + "active": "active", + "pending": "queued", + "queued": "Queued Tasks", + "processing": "Processing", + "finished": "Finished", + "ago": "ago", + "queued_files": "Files Waiting in Sequential Queue", + "scheduled": "Scheduled Tasks", + "workers": "Workers", + "idle": "Idle", + "error": "Error loading queue status", + "task_breakdown": "Task Breakdown", + "currently_processing": "Currently Processing", + "waiting_in_queue": "Waiting in Queue", + "scheduled_for_later": "Scheduled for Later", + "worker_details": "Worker Details", + "cancel": "Cancel", + "remove": "Remove", + "cancel_confirm": "Are you sure you want to cancel this OCR?", + "remove_confirm": "Are you sure you want to remove this file from the queue?", + "cancel_success": "OCR cancelled successfully", + "remove_success": "File removed from queue", + "cancel_error": "Failed to cancel OCR", + "remove_error": "Failed to remove file from queue", + "currently_processing_files": "Currently Processing Files", + "folder_queue_status": "Folder Queue Status", + "active_folders": "Currently Processing Folders", + "queued_folders_display": "Folders Waiting in Queue", + "files": "files", + "position": "Position", + "task": { + "file_ocr": "File OCR", + "ocr_from_api": "API OCR", + "page_ocr": "Page OCR", + "export_file": "Export File", + "prepare_file": "Prepare File", + "auto_segment": "Auto Segment" + } + }, + "ocr_help": { + "language": "Select the language(s) present in the document. Multiple languages can be selected if the document contains mixed languages. Order matters: list the most prevalent language first for best accuracy.", + "special_modules": "Special detection modules:\n• Math detection module (equ): Enables recognition of mathematical equations and formulas\n• Orientation and script detection (osd): Automatically detects page orientation and writing system", + "dpi": "Dots Per Inch - defines the resolution of the scanned image. Higher values (300-600) provide better accuracy but slower processing. Leave empty to use the document's native resolution. Typical values: 150 (draft), 300 (standard), 600 (high quality).", + "preprocessing": "Image preprocessing pipeline that enhances image quality before OCR. Applies grayscale conversion, contrast enhancement (CLAHE), noise reduction (median blur), thresholding, morphological operations, and optional deskewing. Enable for better results with low-quality scans or degraded documents.", + "engine_mode": "OCR engine processing method:\n• Original: Legacy engine, faster but less accurate\n• LSTM: Neural network-based, more accurate for modern fonts\n• Combined: Uses both engines for maximum accuracy (slower)\n• Default: System-recommended setting (typically LSTM)", + "segmentation": "How the engine identifies text regions on the page:\n• Default: Automatic detection (recommended for most documents)\n• Single line/word/char: For images containing only one text element\n• Column/Block modes: For specific layout types (newspapers, forms)\n• Sparse text: For documents with scattered text elements", + "thresholding": "Image binarization method to convert grayscale to black-and-white:\n• Otsu: Automatic threshold calculation (recommended for most cases)\n• Leptonica: Alternative Otsu implementation\n• Sauvola: Adaptive method, better for degraded documents with varying background", + "tesseract_thresholding": "Tesseract's built-in image binarization method. This is separate from the preprocessing pipeline threshold. Only applies if preprocessing is disabled or set to 'None'. Options: Otsu, Leptonica, Sauvola.", + "additional_params": "Advanced Tesseract parameters in 'key=value' format. Example: 'tessedit_char_whitelist=0123456789' to recognize only digits. Consult Tesseract documentation for available options.", + "compress_pdf": "Applies image compression to reduce PDF file size by 60-80% while preserving text accuracy. Recommended for most documents. Disable only if you need original image quality or are experiencing processing issues.", + "compression_target_dpi": "Output resolution in Dots Per Inch. Lower values (50-100) create smaller files with acceptable quality for text. Higher values (150-300) preserve more detail but increase file size. Default: 100 DPI.", + "compression_bg_quality": "JPEG quality for document background/images (1-100). Lower values create smaller files. Recommended: 30-50 for backgrounds. Text remains unaffected.", + "compression_fg_quality": "JPEG quality for text/foreground layer (1-100). Higher values preserve text clarity. Recommended: 70-90 for readable text. Default: 80.", + "compression_flatten": "When enabled, combines all layers into a single JPEG image. Creates simpler PDFs but less efficient compression. Disable for best compression (uses MRC - Mixed Raster Content with separate background and text layers).", + "output_formats": "Select one or more output formats:\n• PDF with index: Searchable PDF with word-level coordinates\n• PDF with text: Standard searchable PDF (recommended)\n• Plain text: Text only, no formatting\n• Page-delimited text: Text with page separators\n• CSV: Word coordinates in spreadsheet format\n• NER: Named entities (people, places, organizations)\n• hOCR/ALTO: Structured formats with detailed layout information" + }, + "confirm delete": "Are you sure you want to delete", + "page count": "page(s)", + "download starting": "Your download will start shortly", + "uncompressed": "uncompressed", + "pdf with index": "PDF with text and word index", + "pdf with index uncompressed": "PDF with text and index (uncompressed)", + "pdf with text": "PDF with text", + "pdf with text uncompressed": "PDF with text (uncompressed)", + "plain text file": "Text", + "text with page separators": "Text with page separators", + "word index csv": "Word index in CSV format", + "entities": "Entities", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "Extracted images", + "editing": { + "title": "Edit document results", + "add_remove_lines": "Add/Remove Lines", + "show_simple_text": "Show Simple Text", + "show_confidence": "Show Confidence Level", + "save": "Save", + "recreate_files": "Recreate Files", + "page": "Page", + "of": "of", + "words": "Words", + "check_spelling": "Check spelling", + "error_load": "Could not obtain results", + "success_submit": "Text submitted successfully", + "error_submit": "Could not submit results" + } +} diff --git a/website/src/Languages/French/translation.json b/website/src/Languages/French/translation.json new file mode 100644 index 00000000..2f7e174c --- /dev/null +++ b/website/src/Languages/French/translation.json @@ -0,0 +1,429 @@ +{ + "welcome": "Bienvenue", + "login": "Se connecter", + "private space": "Espace privé", + "new folder": "Nouveau dossier", + "new document": "Charger un document", + "leave space": "Quitter l'espace", + "back": "Retour", + "start": "Accueil", + "grid view": "Vue grille", + "list view": "Vue liste", + "empty folder title": "Ce dossier est vide", + "empty folder description": "Ajoutez un document ou créez un sous-dossier pour commencer.", + "uploading": "Téléchargement en cours", + "ocr complete": "OCR terminé", + "pages": "pages", + "document": "document", + "documents": "documents", + "folder": "dossier", + "folders": "dossiers", + "see document": "Voir le document", + "edit text": "Modifier le texte", + "repeat ocr": "Répéter l'OCR", + "run ocr": "Exécuter l'OCR", + "download txt": "Télécharger TXT", + "download pdf": "Télécharger PDF", + "download images": "Télécharger les images", + "download original": "Télécharger l'original", + "open folder": "Ouvrir le dossier", + "custom config": "Configuration OCR personnalisée", + "search": "Rechercher", + "sync": "Synchroniser", + "finish": "Terminer", + "name": "Nom", + "details": "Détails", + "size": "Taille", + "date of creation": "Date de création", + "process state": "État du processus", + "version": "Version", + "user manual": "Manuel d'utilisation", + "title": "Outil de reconnaissance optique de caractères", + "folder without files": "Ce dossier est vide", + "sub-folder": "Sous-dossier", + "page": "Page", + "words": "Mots", + "config ocr": "Configurer l'OCR", + "cancel ocr": "Annuler l'OCR", + "config strategy": "Stratégie de Configuration", + "config strategy override": "Utiliser la configuration du dossier pour tous les fichiers", + "config strategy respect": "Respecter la configuration individuelle de chaque fichier", + "config strategy hybrid": "Utiliser la configuration du fichier si elle existe, sinon celle du dossier", + "config strategy hint": "Choisissez comment appliquer la configuration aux fichiers de ce dossier", + "clean all": "Tout effacer", + "auto layout": "Mise en page automatique", + "edit results": "Modifier les résultats", + "delete": "Supprimer", + "index": "Indexer", + "deindex": "Désindexer", + "save": "Enregistrer", + "close": "Fermer", + "retry": "Réessayer", + "advanced": "Avancé", + "advanced ocr settings": "Paramètres OCR Avancés", + "upload error": "Erreur de téléchargement du fichier", + "file download started": "Le téléchargement du fichier a commencé, veuillez patienter", + "folder created successfully": "Dossier créé avec succès", + "folder name required": "Vous devez attribuer un nom au dossier", + "ocr started": "OCR démarré, veuillez patienter", + "folder queued": "En attente dans la file des dossiers", + "edited results files to recreate": "Résultats édités, fichiers à recréer", + "queued file": "En file d'attente (fichier {{current}}/{{total}})", + "processing ocr page": "Traitement OCR - Page {{current}}/{{total}}", + "generating results": "Génération des résultats", + "generating text": "Génération du texte", + "generating delimited text": "Génération du texte délimité", + "generating pdf with index": "Génération du PDF avec index", + "generating pdf": "Génération du PDF", + "generating csv": "Génération du CSV", + "generating images": "Génération des images", + "generating hocr": "Génération du hOCR", + "generating alto xml": "Génération du ALTO XML", + "generating index": "Génération de l'index", + "generating pdf with index page": "Génération du PDF avec index {{current}}/{{total}}", + "generating pdf page": "Génération du PDF {{current}}/{{total}}", + "compression complete": "Compression terminée", + "compressing pdf finalizing": "Compression du PDF - Finalisation...", + "compressing pdf": "Compression du PDF", + "compressing file": "Compression du fichier original...", + "error preparing document": "Erreur lors de la préparation du document", + "error during ocr": "Erreur pendant l'OCR", + "error during ocr page": "Erreur pendant l'OCR de la page {{page}}", + "error generating results": "Erreur lors de la génération des résultats", + "ocr interrupted retry": "OCR interrompu - vous pouvez réessayer", + "invalid parameters": "Paramètres invalides", + "ocr cancelled": "OCR Annulé", + "error canceling ocr": "Erreur d'annulation de l'OCR", + "uploading stage": "Téléchargement en cours, veuillez patienter...", + "preparing stage": "Préparation du document...", + "preparing ocr": "Préparation de l'OCR...", + "confirm leave title": "Êtes-vous sûr de vouloir partir?", + "confirm leave warning": "Si vous partez sans sauvegarder, vous perdrez toutes les modifications que vous avez apportées!", + "confirm leave button": "Partir sans sauvegarder", + "save and leave button": "Sauvegarder et partir", + "saving": "Sauvegarde", + "compressing": "Compression", + "exporting": "Exportation", + "ignore extract": "Ignorer/Extraire", + "join": "Joindre", + "separate": "Séparer", + "replicate": "Dupliquer", + "create new folder": "Créer un nouveau dossier", + "folder name": "Nom du dossier*", + "folder name extra": "Le nom ne peut pas commencer par '_' ni contenir '/' ou '\\'", + "create": "Créer", + "auto layout popup": "Préparation de la mise en page, veuillez patienter...", + "layout loading": "La mise en page est en cours de création...", + "layout create": "Créer une mise en page", + "configure ocr": "Configurer l'OCR", + "of document": "du document", + "of folder": "du dossier", + "languages": { + "arabic": "Arabe", + "chinese_simplified": "Chinois (simplifié)", + "chinese_traditional": "Chinois (traditionnel)", + "german": "Allemand", + "english": "Anglais", + "french": "Français", + "hindi": "Hindi", + "indonesian": "Indonésien (Bahasa Indonesia)", + "italian": "Italien", + "portuguese": "Portugais", + "russian": "Russe", + "spanish": "Espagnol", + "math module": "Module de détection mathématique", + "osd module": "Module de détection d'orientation et de script" + }, + "output": { + "pdf indexed": "PDF avec texte et index des mots", + "pdf": "PDF avec texte (par défaut)", + "txt": "Texte brut", + "txt delimited": "Texte délimité par pages", + "csv": "Index des mots au format CSV", + "ner": "Entités nommées (NER)", + "hocr": "hOCR (documents d'une seule page uniquement)", + "xml": "ALTO (documents d'une seule page uniquement)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract Original", + "lstm": "Tesseract LSTM", + "combined": "LSTM + Original combiné", + "default": "Mode par défaut" + }, + "segmentation mode": { + "auto with osd": "Segmentation automatique de page avec OSD", + "auto no osd": "Segmentation automatique sans OSD ni OCR", + "default": "(Par défaut) Segmentation automatique, sans OSD", + "column variable lines": "Colonne de texte avec tailles de lignes variables", + "block vertical": "Bloc de texte vertical uniforme", + "block uniform": "Bloc de texte uniforme", + "single line": "Image avec une seule ligne de texte", + "single word": "Image avec un seul mot", + "single circle word": "Image avec un seul mot dans un cercle", + "single char": "Image avec un seul caractère", + "sparse text": "Texte clairsemé ; trouver autant que possible sans ordre", + "sparse text osd": "Texte clairsemé avec OSD", + "single line hack": "Astuce de contournement : traiter l'image comme une seule ligne de texte" + }, + "threshold": { + "otsu": "Otsu (par défaut)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola" + }, + "default values": "Valeurs par défaut", + "output formats": "Formats de sortie", + "language": "Langue", + "languages_section": "Langues", + "special modules": "Modules spéciaux", + "more outputs": "Plus de formats", + "tesseract thresholding": "Seuillage Tesseract", + "language hint": "Pour de meilleurs résultats, sélectionnez par ordre de pertinence", + "language required": "Vous devez sélectionner au moins une langue", + "output required": "Vous devez sélectionner au moins un format de sortie", + "dpi": "DPI (points par pouce)", + "ocr engine": "Moteur OCR", + "engine mode": "Mode du moteur", + "segmentation": "Segmentation", + "thresholding": "Seuillage", + "additional parameters": "Paramètres supplémentaires", + "choose preset": "Choisir une configuration prédéfinie", + "select ocr preset": "Sélectionner un preset OCR", + "use default": "Utiliser par défaut", + "presets": { + "default": "Par défaut", + "fast": "Rapide", + "balanced": "Équilibré", + "high-quality": "Haute Qualité", + "degraded-documents": "Documents Dégradés", + "multi-column": "Multi-Colonnes", + "tables-forms": "Tableaux/Formulaires", + "default_desc": "Configuration par défaut du système", + "fast_desc": "Traitement rapide - idéal pour les documents simples et propres", + "balanced_desc": "Équilibre entre vitesse et qualité - bon choix général", + "high_quality_desc": "Précision maximale - pour les documents importants", + "degraded_documents_desc": "Optimisé pour les documents anciens ou de mauvaise qualité", + "multi_column_desc": "Pour les journaux, magazines et mises en page multi-colonnes", + "tables_forms_desc": "Optimisé pour les tableaux, formulaires et données structurées" + }, + "lose results": "Vous perdrez vos derniers résultats et les modifications précédentes !", + "begin": "Commencer", + "clear all": "Tout effacer", + "alter existing config": "Modifier la configuration existante", + "sync_title": "Synchroniser les fichiers externes", + "sync_description": "Importer les fichiers ajoutés directement dans le dossier de stockage. Choisissez la portée de la synchronisation :", + "sync_current": "Dossier actuel uniquement", + "sync_recursive": "Tous les sous-dossiers", + "sync_success": "{count} fichier(s) importé(s)", + "sync_skipped": "{count} fichier(s) existant(s) ignoré(s)", + "sync_no_new": "Aucun nouveau fichier trouvé", + "sync_error": "Erreur de synchronisation des fichiers", + "email": "Email", + "password": "Mot de passe", + "submit": "Soumettre", + "logout": "Se déconnecter", + "confirm": "Confirmer", + "refresh": "Actualiser", + "never": "jamais", + "days": "jour(s)", + "hours": "heures", + "every": "Tous les", + "hour": "Heure", + "day": "Jour", + "admin": { + "login_title": "Connexion administrateur OCR", + "email_password_incorrect": "Email ou mot de passe incorrect", + "free_storage": "Espace de stockage disponible", + "manage_storage": "Gérer le stockage", + "configure_ocr_defaults": "Configurer les paramètres OCR par défaut", + "view_workers_processes": "Voir les workers et les processus", + "last_cleanup": "Dernier nettoyage", + "last_update": "Dernière mise à jour", + "remove_private_spaces_older": "Supprimer les espaces privés datant de plus de", + "change_max_age": "Modifier l'âge maximum", + "api_documents": "Documents API", + "private_spaces": "Espaces privés", + "confirm_delete_space": "Êtes-vous sûr de vouloir supprimer l'espace", + "confirm_delete_document": "Êtes-vous sûr de vouloir supprimer le document avec l'ID", + "confirm_remove_sessions": "Êtes-vous sûr de vouloir supprimer les sessions datant de plus de", + "set_cleanup_schedule": "Définir le planning de nettoyage automatique", + "by_interval": "Par intervalle", + "weekly": "Hebdomadaire", + "week_days": "Jours de la semaine", + "select_at_least_one_day": "Vous devez sélectionner au moins un jour", + "monthly": "Mensuel", + "dpi_integer_error": "La valeur DPI doit être un nombre entier !", + "manage_ocr_configurations": "Gérer les configurations OCR", + "editing_configuration": "Modification de la configuration", + "default_config_cannot_delete": "La configuration par défaut ne peut pas être supprimée", + "creating_new_configuration": "Création d'une nouvelle configuration :", + "default_config_must_define": "La configuration par défaut doit définir les paramètres obligatoires", + "select_at_least_one_output": "Vous devez sélectionner au moins un format de sortie", + "select_at_least_one_language": "Vous devez sélectionner au moins une langue", + "save_configuration": "Enregistrer la configuration", + "confirm_delete_configuration": "Êtes-vous sûr de vouloir supprimer la configuration", + "request_failed": "Impossible de compléter la requête.", + "hours_positive_integer": "Le nombre d'heures doit être un entier positif !", + "day_between_1_31": "Le jour doit être un nombre entre 1 et 31 !", + "folder_concurrency_title": "Contrôle de Concurrence OCR des Dossiers", + "max_concurrent_folders": "Dossiers Simultanés Maximum", + "active_folders": "Dossiers actifs", + "queued_folders": "Dossiers en file", + "folder_concurrency_updated": "Limite de concurrence des dossiers mise à jour" + }, + "weekdays": { + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday": "Samedi", + "sunday": "Dimanche" + }, + "immediate ocr": "OCR Immédiat", + "quick process": "Traitement Rapide", + "upload for immediate ocr": "Télécharger un Document pour Traitement Immédiat", + "select output formats": "Sélectionner les Formats de Sortie", + "plain text": "Texte Brut", + "pdf simple": "PDF (Simple)", + "pdf searchable": "PDF (Consultable/Indexé)", + "process now": "Traiter Maintenant", + "download text": "Télécharger le Texte", + "processing document": "Traitement du document...", + "upload new document": "Télécharger un Nouveau Document", + "rerun with different settings": "Réexécuter avec des Paramètres Différents", + "temporary processing note": "Les fichiers sont traités temporairement et seront supprimés lorsque vous téléchargerez un nouveau document ou quitterez cette page.", + "drag drop files": "Glissez les fichiers ici ou cliquez pour sélectionner", + "file uploaded": "Fichier téléchargé", + "no file uploaded": "Aucun fichier téléchargé", + "clear results": "Effacer les Résultats", + "select languages": "Sélectionner les Langues", + "select preset": "Sélectionner le Preset", + "results ready": "Résultats Prêts", + "download results": "Télécharger les Résultats", + "file too large": "Fichier trop volumineux", + "unsupported file type": "Type de fichier non pris en charge", + "upload failed": "Échec du téléchargement", + "processing failed": "Échec du traitement", + "return to home": "Retour à l'Accueil", + "enable compression": "Activer la Compression PDF", + "compression info": "Réduit la taille du fichier mais prend plus de temps et utilise plus de mémoire. Désactiver pour un traitement plus rapide.", + "compression quality": "Qualité de Compression", + "compression auto": "Automatique (Recommandé)", + "compression auto desc": "Choisit automatiquement la meilleure qualité en fonction de la taille du fichier", + "compression fast": "Rapide", + "compression fast desc": "Traitement plus rapide, qualité légèrement inférieure (bon pour les gros fichiers)", + "compression high quality": "Haute Qualité", + "compression high quality desc": "Meilleure qualité, traitement plus lent (bon pour les petits fichiers)", + "compressing pdf starting": "Compression du PDF - Démarrage...", + "compressing pdf page": "Compression du PDF - Page {{current}}/{{total}}", + "compressing pdf finalizing": "Compression du PDF - Finalisation...", + "compressing pdf": "Compression du PDF", + "compression complete": "Compression terminée", + "compression settings": "Paramètres de Compression", + "compression target dpi": "DPI Cible", + "compression bg quality": "Qualité de l'Arrière-plan (1-100)", + "compression fg quality": "Qualité du Texte (1-100)", + "compression flatten to jpeg": "Aplatir en Couche JPEG Unique", + "compressing pdf starting": "Compression du PDF - Démarrage...", + "compressing pdf page": "Compression du PDF - Page {{current}}/{{total}}", + "compressing pdf finalizing": "Compression du PDF - Finalisation...", + "compressing pdf": "Compression du PDF", + "compression complete": "Compression terminée", + "processing ocr": "Traitement OCR", + "processing ocr page": "Traitement OCR - Page {{current}}/{{total}}", + "compressing file": "Compression du fichier original...", + "compressing file page": "Compression - Page {{current}}/{{total}}", + "adding text layer": "Ajout de la couche de texte OCR...", + "adding text page": "Ajout du texte - Page {{current}}/{{total}}", + "completed": "Terminé", + "error must select language": "Vous devez sélectionner au moins une langue", + "error must select output": "Vous devez sélectionner au moins un format de sortie", + "helper text language order": "Pour de meilleurs résultats, sélectionnez par ordre de pertinence", + "error fetch default config": "Impossible de récupérer la dernière configuration par défaut", + "error fetch preset": "Impossible de récupérer la configuration prédéfinie", + "error fetch presets list": "Impossible de mettre à jour la liste des configurations prédéfinies", + "error dpi must be integer": "La valeur DPI doit être un nombre entier !", + "success config saved": "Configuration OCR enregistrée avec succès.", + "error config save unexpected": "Erreur inattendue lors de l'enregistrement de la configuration OCR.", + "error config save failed": "Impossible d'enregistrer la configuration OCR.", + "queue": { + "title": "État de la File de Tâches", + "page_title": "État de la File", + "tooltip": "État de la File", + "view_details": "Cliquez pour voir les détails de la file", + "active": "actives", + "pending": "en attente", + "queued": "Tâches en File", + "processing": "Traitement en cours", + "finished": "Terminé", + "ago": "il y a", + "queued_files": "Fichiers en Attente dans la File Séquentielle", + "scheduled": "Tâches Planifiées", + "workers": "Travailleurs", + "idle": "Inactif", + "error": "Erreur lors du chargement de l'état de la file", + "task_breakdown": "Répartition des Tâches", + "currently_processing": "Traitement en Cours", + "waiting_in_queue": "En Attente dans la File", + "scheduled_for_later": "Planifié pour Plus Tard", + "worker_details": "Détails des Travailleurs", + "cancel": "Annuler", + "remove": "Retirer", + "cancel_confirm": "Êtes-vous sûr de vouloir annuler cette OCR ?", + "remove_confirm": "Êtes-vous sûr de vouloir retirer ce fichier de la file ?", + "cancel_success": "OCR annulé avec succès", + "remove_success": "Fichier retiré de la file", + "cancel_error": "Échec de l'annulation de l'OCR", + "remove_error": "Échec du retrait du fichier de la file", + "currently_processing_files": "Fichiers en Cours de Traitement", + "folder_queue_status": "État de la File de Dossiers", + "active_folders": "Dossiers en Traitement", + "queued_folders_display": "Dossiers en Attente", + "files": "fichiers", + "position": "Position", + "task": { + "file_ocr": "OCR de Fichier", + "ocr_from_api": "OCR depuis API", + "page_ocr": "OCR de Page", + "export_file": "Exporter Fichier", + "prepare_file": "Préparer Fichier", + "auto_segment": "Segmentation Automatique" + } + }, + "ocr_help": { + "language": "Sélectionnez la/les langue(s) présente(s) dans le document. Plusieurs langues peuvent être sélectionnées si le document contient des langues mixtes. L'ordre est important : listez d'abord la langue la plus répandue pour une meilleure précision.", + "special_modules": "Modules de détection spéciaux :\n• Module de détection mathématique (equ) : Permet la reconnaissance d'équations et de formules mathématiques\n• Détection d'orientation et de script (osd) : Détecte automatiquement l'orientation de la page et le système d'écriture", + "dpi": "Points par Pouce - définit la résolution de l'image numérisée. Des valeurs plus élevées (300-600) offrent une meilleure précision mais un traitement plus lent. Laissez vide pour utiliser la résolution native du document. Valeurs typiques : 150 (brouillon), 300 (standard), 600 (haute qualité).", + "preprocessing": "Pipeline de prétraitement d'image qui améliore la qualité de l'image avant l'OCR. Applique une conversion en niveaux de gris, une amélioration du contraste (CLAHE), une réduction du bruit (flou médian), un seuillage, des opérations morphologiques et une correction d'inclinaison optionnelle. Activez pour de meilleurs résultats avec des numérisations de mauvaise qualité ou des documents dégradés.", + "engine_mode": "Méthode de traitement du moteur OCR :\n• Original : Moteur hérité, plus rapide mais moins précis\n• LSTM : Basé sur réseau neuronal, plus précis pour les polices modernes\n• Combined : Utilise les deux moteurs pour une précision maximale (plus lent)\n• Default : Paramètre recommandé par le système (typiquement LSTM)", + "segmentation": "Comment le moteur identifie les régions de texte sur la page :\n• Default : Détection automatique (recommandé pour la plupart des documents)\n• Single line/word/char : Pour les images contenant un seul élément de texte\n• Modes Column/Block : Pour des types de mise en page spécifiques (journaux, formulaires)\n• Sparse text : Pour les documents avec des éléments de texte dispersés", + "thresholding": "Méthode de binarisation d'image pour convertir les niveaux de gris en noir et blanc :\n• Otsu : Calcul automatique du seuil (recommandé pour la plupart des cas)\n• Leptonica : Implémentation alternative d'Otsu\n• Sauvola : Méthode adaptative, meilleure pour les documents dégradés avec fond variable", + "tesseract_thresholding": "Méthode de binarisation d'image intégrée de Tesseract. C'est séparé du seuil du pipeline de prétraitement. S'applique uniquement si le prétraitement est désactivé ou défini sur 'None'. Options : Otsu, Leptonica, Sauvola.", + "additional_params": "Paramètres avancés de Tesseract au format 'clé=valeur'. Exemple : 'tessedit_char_whitelist=0123456789' pour reconnaître uniquement les chiffres. Consultez la documentation Tesseract pour les options disponibles.", + "compress_pdf": "Applique une compression d'image pour réduire la taille du fichier PDF de 60-80% tout en préservant la précision du texte. Recommandé pour la plupart des documents. Désactivez uniquement si vous avez besoin de la qualité d'image originale ou si vous rencontrez des problèmes de traitement.", + "compression_target_dpi": "Résolution de sortie en Points par Pouce. Des valeurs plus basses (50-100) créent des fichiers plus petits avec une qualité acceptable pour le texte. Des valeurs plus élevées (150-300) préservent plus de détails mais augmentent la taille du fichier. Par défaut : 100 DPI.", + "compression_bg_quality": "Qualité JPEG pour l'arrière-plan/images du document (1-100). Des valeurs plus basses créent des fichiers plus petits. Recommandé : 30-50 pour les arrière-plans. Le texte n'est pas affecté.", + "compression_fg_quality": "Qualité JPEG pour la couche de texte/premier plan (1-100). Des valeurs plus élevées préservent la clarté du texte. Recommandé : 70-90 pour un texte lisible. Par défaut : 80.", + "compression_flatten": "Lorsqu'activé, combine toutes les couches en une seule image JPEG. Crée des PDF plus simples mais une compression moins efficace. Désactivez pour une meilleure compression (utilise MRC - Contenu Raster Mixte avec des couches séparées pour l'arrière-plan et le texte).", + "output_formats": "Sélectionnez un ou plusieurs formats de sortie :\n• PDF avec index : PDF consultable avec coordonnées au niveau des mots\n• PDF avec texte : PDF consultable standard (recommandé)\n• Texte brut : Texte uniquement, sans formatage\n• Texte délimité par page : Texte avec séparateurs de page\n• CSV : Coordonnées des mots au format tableur\n• NER : Entités nommées (personnes, lieux, organisations)\n• hOCR/ALTO : Formats structurés avec informations détaillées de mise en page" + }, + "confirm delete": "Êtes-vous sûr de vouloir supprimer", + "page count": "page(s)", + "download starting": "Votre téléchargement va commencer dans quelques instants", + "uncompressed": "non compressé", + "pdf with index": "PDF avec texte et index des mots", + "pdf with index uncompressed": "PDF avec texte et index (non compressé)", + "pdf with text": "PDF avec texte", + "pdf with text uncompressed": "PDF avec texte (non compressé)", + "plain text file": "Texte", + "text with page separators": "Texte avec séparateurs de page", + "word index csv": "Index des mots au format CSV", + "entities": "Entités", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "Images extraites" +} diff --git a/website/src/Languages/Portuguese/translation.json b/website/src/Languages/Portuguese/translation.json new file mode 100644 index 00000000..b964c3fd --- /dev/null +++ b/website/src/Languages/Portuguese/translation.json @@ -0,0 +1,463 @@ +{ + "welcome": "Bem-vindo", + "login": "Iniciar sessão", + "private space": "Espaço Privado", + "new folder": "Nova Pasta", + "new document": "Carregar Documento", + "leave space": "Sair do Espaço", + "back": "Voltar", + "start": "Início", + "grid view": "Vista em Grelha", + "list view": "Vista em Lista", + "empty folder title": "Esta pasta está vazia", + "empty folder description": "Adicione um documento ou crie uma subpasta para começar.", + "uploading": "A enviar", + "ocr complete": "OCR Completo", + "pages": "páginas", + "document": "documento", + "documents": "documentos", + "folder": "pasta", + "folders": "pastas", + "see document": "Ver Documento", + "edit text": "Editar Texto", + "repeat ocr": "Repetir OCR", + "run ocr": "Executar OCR", + "download txt": "Descarregar TXT", + "download pdf": "Descarregar PDF", + "download images": "Descarregar Imagens", + "download original": "Descarregar Original", + "open folder": "Abrir Pasta", + "custom config": "Configuração Personalizada de OCR", + "search": "Pesquisar", + "sync": "Sincronizar", + "finish": "Terminar", + "name": "Nome", + "details": "Detalhes", + "size": "Tamanho", + "date of creation": "Data de Criação", + "process state": "Estado do Processo", + "version": "Versão", + "user manual": "Manual do utilizador", + "title": "Ferramenta de Reconhecimento Óptico de Caracteres", + "folder without files": "A pasta não contém documentos", + "document": "Documento", + "sub-folder": "Sub-pasta", + "page": "Página", + "words": "Palavras", + "config ocr": "Configurar OCR", + "cancel ocr": "Cancelar OCR", + "config strategy": "Estratégia de Configuração", + "config strategy override": "Usar configuração da pasta para todos os ficheiros", + "config strategy respect": "Respeitar configuração individual de cada ficheiro", + "config strategy hybrid": "Usar configuração do ficheiro se existir, caso contrário usar da pasta", + "config strategy hint": "Escolha como aplicar a configuração aos ficheiros desta pasta", + "clean all": "Limpar tudo", + "auto layout": "Segmentação automática", + "edit results": "Editar Resultados", + "delete": "Apagar", + "index": "Indexar", + "deindex": "Desindexar", + "save": "Guardar", + "close": "Fechar", + "retry": "Tentar Novamente", + "advanced": "Avançado", + "advanced ocr settings": "Configurações Avançadas de OCR", + "upload error": "Erro ao carregar documento", + "file download started": "A transferência do ficheiro começou, por favor aguarde", + "folder created successfully": "Pasta criada com sucesso", + "folder name required": "Deve atribuir um nome à pasta", + "ocr started": "O OCR começou, por favor aguarde", + "folder queued": "A aguardar na fila de pastas", + "edited results files to recreate": "Resultados editados, ficheiros por recriar", + "queued file": "Na fila (ficheiro {{current}}/{{total}})", + "processing ocr page": "A processar OCR - Página {{current}}/{{total}}", + "generating results": "A gerar resultados", + "generating text": "A gerar texto", + "generating delimited text": "A gerar texto delimitado", + "generating pdf with index": "A gerar PDF com índice", + "generating pdf": "A gerar PDF", + "generating csv": "A gerar CSV", + "generating images": "A gerar imagens", + "generating hocr": "A gerar hOCR", + "generating alto xml": "A gerar ALTO XML", + "generating index": "A gerar índice", + "generating pdf with index page": "A gerar PDF com índice {{current}}/{{total}}", + "generating pdf page": "A gerar PDF {{current}}/{{total}}", + "compression complete": "Compressão concluída", + "compressing pdf finalizing": "A comprimir PDF - A finalizar...", + "compressing pdf": "A comprimir PDF", + "compressing file": "A comprimir ficheiro original...", + "error preparing document": "Erro a preparar documento", + "error during ocr": "Erro durante OCR", + "error during ocr page": "Erro durante OCR da página {{page}}", + "error generating results": "Erro a gerar resultados", + "ocr interrupted retry": "OCR interrompido - pode tentar novamente", + "invalid parameters": "Parâmetros inválidos", + "ocr cancelled": "OCR Cancelado", + "error canceling ocr": "Erro ao cancelar OCR", + "uploading stage": "A enviar, por favor aguarde...", + "preparing stage": "A preparar o documento...", + "preparing ocr": "A preparar OCR...", + "confirm leave title": "Tem a certeza que quer sair?", + "confirm leave warning": "Se sair sem gravar, irá perder qualquer alteração que tenha feito!", + "confirm leave button": "Sair sem gravar", + "save and leave button": "Gravar e sair", + "saving": "A gravar", + "compressing": "A comprimir", + "exporting": "A exportar", + "ignore extract": "Ignorar/Extrair", + "join": "Agrupar", + "separate": "Desagrupar", + "replicate": "Replicar", + "create new folder": "Criar uma nova pasta", + "folder name": "Nome da pasta*", + "folder name extra": "O nome não pode começar com '_' nem conter '/' ou '\\'", + "create": "Criar", + "auto layout popup": "A segmentar automaticamente... Por favor aguarde", + "layout loading": "O documento está a ser segmentado...", + "layout create": "Segmentar o documento", + "configure ocr": "Configurar OCR", + "of document": "do documento", + "of folder": "da pasta", + "languages": { + "arabic": "Árabe", + "chinese_simplified": "Chinês (Simplificado)", + "chinese_traditional": "Chinês (Tradicional)", + "german": "Alemão", + "english": "Inglês", + "french": "Francês", + "hindi": "Hindi", + "indonesian": "Indonésio (Bahasa Indonesia)", + "italian": "Italiano", + "portuguese": "Português", + "russian": "Russo", + "spanish": "Espanhol Castelhano", + "math module": "Módulo de deteção de matemática / equações", + "osd module": "Módulo de orientação e deteção de scripts" + }, + "output": { + "pdf indexed": "PDF com texto e índice de palavras", + "pdf": "PDF com texto (por defeito)", + "txt": "Texto", + "txt delimited": "Texto com separador por página", + "csv": "Índice de palavras em formato CSV", + "ner": "Entidades (NER)", + "hocr": "hOCR (apenas documentos com 1 página)", + "xml": "ALTO (apenas documentos com 1 página)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract Original", + "lstm": "Tesseract LSTM", + "combined": "Tesseract LSTM + Original combinado", + "default": "Modo disponível por defeito" + }, + "segmentation mode": { + "auto with osd": "OCR com segmentação automática de página e OSD", + "auto no osd": "Segmentação automática sem OSD nem OCR", + "default": "(Por defeito) OCR com segmentação automática, sem OSD", + "column variable lines": "Coluna de texto com linhas de tamanho variável", + "block vertical": "Bloco uniforme de texto, alinhado verticalmente", + "block uniform": "Bloco uniforme de texto", + "single line": "Imagem com apenas uma linha de texto", + "single word": "Imagem com apenas uma palavra", + "single circle word": "Imagem com apenas uma palavra num círculo", + "single char": "Imagem com apenas um carácter", + "sparse text": "Texto disperso; procurar o máximo de texto sem ordem particular", + "sparse text osd": "Texto disperso com OSD", + "single line hack": "Contornando truques específicos do Tesseract" + }, + "threshold": { + "otsu": "Otsu (por defeito)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola", + "adaptive_gaussian": "Gaussiano Adaptativo", + "none": "Nenhum (Apenas Pré-processamento)" + }, + "preprocessing": { + "title": "Pipeline de Pré-processamento", + "enabled": "Ativar Pré-processamento", + "grayscale": "Converter para Tons de Cinza", + "clahe": "Melhoria CLAHE", + "clahe_clip_limit": "Limite de Corte", + "clahe_tile_size": "Tamanho do Mosaico", + "median_blur": "Desfoque Mediano", + "median_blur_kernel": "Tamanho do Kernel", + "threshold_method": "Método de Thresholding", + "adaptive_block_size": "Tamanho do Bloco", + "adaptive_c": "Valor C", + "morphological_opening": "Abertura Morfológica", + "morphological_closing": "Fechamento Morfológico", + "morph_kernel_size": "Kernel Morfológico", + "deskew": "Correção Automática de Inclinação" + }, + "default values": "Valores Por Defeito", + "finish": "Terminar", + "output formats": "Formatos de resultado", + "language": "Língua", + "languages_section": "Línguas", + "special modules": "Módulos Especiais", + "more outputs": "Mais Formatos", + "tesseract thresholding": "Limiarização do Tesseract", + "language hint": "Para melhores resultados, selecione por ordem de relevância", + "language required": "Deve selecionar pelo menos uma língua", + "output required": "Deve selecionar pelo menos um formato de resultado", + "dpi": "DPI (Pontos Por Polegada)", + "ocr engine": "Motor de OCR", + "engine mode": "Modo do motor", + "segmentation": "Segmentação", + "thresholding": "Thresholding", + "additional parameters": "Parâmetros adicionais", + "compress pdf": "Comprimir PDF", + "compress pdf description": "Reduz o tamanho do ficheiro PDF em 60-80% mantendo a qualidade de texto", + "choose preset": "Escolher configuração predefinida", + "select ocr preset": "Selecionar Preset de OCR", + "use default": "Usar predefinido", + "presets": { + "default": "Predefinido", + "fast": "Rápido", + "balanced": "Equilibrado", + "high-quality": "Alta Qualidade", + "degraded-documents": "Documentos Degradados", + "multi-column": "Multi-Coluna", + "tables-forms": "Tabelas/Formulários", + "default_desc": "Configuração padrão do sistema", + "fast_desc": "Processamento rápido - ideal para documentos simples e limpos", + "balanced_desc": "Equilíbrio entre velocidade e qualidade - boa escolha geral", + "high_quality_desc": "Máxima precisão - para documentos importantes", + "degraded_documents_desc": "Otimizado para documentos antigos ou de baixa qualidade", + "multi_column_desc": "Para jornais, revistas e layouts de múltiplas colunas", + "tables_forms_desc": "Otimizado para tabelas, formulários e dados estruturados" + }, + "lose results": "Irá perder os resultados e alterações anteriores!", + "begin": "Começar", + "clear all": "Limpar Tudo", + "alter existing config": "Alterar Configuração Existente", + "sync": "Sincronizar", + "sync_title": "Sincronizar Ficheiros Externos", + "sync_description": "Importar ficheiros que foram adicionados diretamente à pasta de armazenamento. Escolha o âmbito da sincronização:", + "sync_current": "Apenas Pasta Atual", + "sync_recursive": "Todas as Subpastas", + "sync_success": "{count} ficheiro(s) importado(s)", + "sync_skipped": "{count} ficheiro(s) existente(s) ignorado(s)", + "sync_no_new": "Nenhum ficheiro novo encontrado", + "sync_error": "Erro ao sincronizar ficheiros", + "email": "Email", + "password": "Password", + "submit": "Submeter", + "logout": "Sair", + "confirm": "Confirmar", + "refresh": "Refresh", + "never": "nunca", + "days": "dia(s)", + "hours": "horas", + "every": "A cada", + "hour": "Hora", + "day": "Dia", + "admin": { + "login_title": "OCR Admin Login", + "email_password_incorrect": "Email ou password incorretos", + "free_storage": "Armazenamento livre", + "manage_storage": "Gerir Armazenamento", + "configure_ocr_defaults": "Configurar Predefinições OCR", + "view_workers_processes": "Ver Workers e Processos", + "last_cleanup": "Última limpeza", + "last_update": "Último update", + "remove_private_spaces_older": "Remover espaços privados com mais de", + "change_max_age": "Alterar idade máxima", + "api_documents": "Documentos de API", + "private_spaces": "Espaços Privados", + "confirm_delete_space": "Tem a certeza que quer apagar o espaço", + "confirm_delete_document": "Tem a certeza que quer apagar o documento com ID", + "confirm_remove_sessions": "Tem a certeza que quer remover as sessões com mais de", + "set_cleanup_schedule": "Definir horário de limpeza automática", + "by_interval": "Por intervalo", + "weekly": "Semanalmente", + "week_days": "Dias da semana", + "select_at_least_one_day": "Deve selecionar pelo menos um dia", + "monthly": "Mensalmente", + "dpi_integer_error": "O valor de DPI deve ser um número inteiro!", + "manage_ocr_configurations": "Gerir Configurações de OCR", + "editing_configuration": "A alterar configuração", + "default_config_cannot_delete": "A configuração default não pode ser apagada", + "creating_new_configuration": "A criar nova configuração:", + "default_config_must_define": "A configuração default deve definir os parâmetros obrigatórios", + "select_at_least_one_output": "Deve selecionar pelo menos um formato de resultado", + "select_at_least_one_language": "Deve selecionar pelo menos uma língua", + "save_configuration": "Guardar a configuração", + "confirm_delete_configuration": "Tem a certeza que quer apagar a configuração", + "request_failed": "Não foi possível concluir o pedido.", + "hours_positive_integer": "O número de horas deve ser um valor inteiro positivo!", + "day_between_1_31": "O dia deve ser um número entre 1 e 31!", + "folder_concurrency_title": "Controlo de Concorrência de OCR de Pastas", + "max_concurrent_folders": "Máximo de Pastas Simultâneas", + "active_folders": "Pastas ativas", + "queued_folders": "Pastas em fila", + "folder_concurrency_updated": "Limite de concorrência de pastas atualizado" + }, + "weekdays": { + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo" + }, + "immediate ocr": "OCR Imediato", + "quick process": "Processamento Rápido", + "upload for immediate ocr": "Carregar Documento para Processamento Imediato", + "select output formats": "Selecionar Formatos de Saída", + "plain text": "Texto Simples", + "pdf simple": "PDF (Simples)", + "pdf searchable": "PDF (Pesquisável/Indexado)", + "process now": "Processar Agora", + "download text": "Descarregar Texto", + "processing document": "A processar documento...", + "upload new document": "Carregar Novo Documento", + "rerun with different settings": "Executar Novamente com Definições Diferentes", + "temporary processing note": "Os ficheiros são processados temporariamente e serão apagados quando carregar um novo documento ou sair desta página.", + "drag drop files": "Arraste ficheiros para aqui ou clique para selecionar", + "file uploaded": "Ficheiro carregado", + "no file uploaded": "Nenhum ficheiro carregado", + "clear results": "Limpar Resultados", + "select languages": "Selecionar Línguas", + "select preset": "Selecionar Preset", + "results ready": "Resultados Prontos", + "download results": "Descarregar Resultados", + "file too large": "Ficheiro demasiado grande", + "unsupported file type": "Tipo de ficheiro não suportado", + "upload failed": "Falha ao carregar ficheiro", + "processing failed": "Falha ao processar documento", + "return to home": "Voltar ao Início", + "enable compression": "Ativar Compressão de PDF", + "compression info": "Reduz o tamanho do ficheiro mas demora mais tempo e usa mais memória. Desativar para processamento mais rápido.", + "compression quality": "Qualidade de Compressão", + "compression auto": "Automático (Recomendado)", + "compression auto desc": "Escolhe automaticamente a melhor qualidade com base no tamanho do ficheiro", + "compression fast": "Rápido", + "compression fast desc": "Processamento mais rápido, qualidade ligeiramente inferior (bom para ficheiros grandes)", + "compression high quality": "Alta Qualidade", + "compression high quality desc": "Melhor qualidade, processamento mais lento (bom para ficheiros pequenos)", + "compression settings": "Definições de Compressão", + "compression target dpi": "DPI de Destino", + "compression bg quality": "Qualidade do Fundo (1-100)", + "compression fg quality": "Qualidade do Texto (1-100)", + "compression flatten to jpeg": "Achatar para Camada JPEG Única", + "compressing pdf starting": "A comprimir PDF - A iniciar...", + "compressing pdf page": "A comprimir PDF - Página {{current}}/{{total}}", + "compressing pdf finalizing": "A comprimir PDF - A finalizar...", + "compressing pdf": "A comprimir PDF", + "compression complete": "Compressão concluída", + "processing ocr": "A processar OCR", + "processing ocr page": "A processar OCR - Página {{current}}/{{total}}", + "compressing file": "A comprimir ficheiro original...", + "compressing file page": "A comprimir - Página {{current}}/{{total}}", + "adding text layer": "A adicionar texto OCR...", + "adding text page": "A adicionar texto - Página {{current}}/{{total}}", + "completed": "Concluído", + "error must select language": "Deve selecionar pelo menos uma língua", + "error must select output": "Deve selecionar pelo menos um formato de resultado", + "helper text language order": "Para melhores resultados, selecione por ordem de relevância", + "error fetch default config": "Não foi possível obter a configuração por defeito mais atual", + "error fetch preset": "Não foi possível obter a configuração predefinida", + "error fetch presets list": "Não foi possível atualizar a lista de configurações predefinidas", + "error dpi must be integer": "O valor de DPI deve ser um número inteiro!", + "success config saved": "Configuração de OCR guardada com sucesso.", + "error config save unexpected": "Erro inesperado ao guardar a configuração de OCR.", + "error config save failed": "Não foi possível guardar a configuração de OCR.", + "queue": { + "title": "Estado da Fila de Tarefas", + "page_title": "Estado da Fila", + "tooltip": "Estado da Fila", + "view_details": "Clique para ver detalhes da fila", + "active": "ativas", + "pending": "em fila", + "queued": "Tarefas em Fila", + "processing": "A Processar", + "finished": "Concluído", + "ago": "atrás", + "queued_files": "Ficheiros à Espera na Fila Sequencial", + "scheduled": "Tarefas Agendadas", + "workers": "Trabalhadores", + "idle": "Inativo", + "error": "Erro ao carregar estado da fila", + "task_breakdown": "Distribuição de Tarefas", + "currently_processing": "A Processar Atualmente", + "waiting_in_queue": "À Espera na Fila", + "scheduled_for_later": "Agendadas para Mais Tarde", + "worker_details": "Detalhes dos Trabalhadores", + "cancel": "Cancelar", + "remove": "Remover", + "cancel_confirm": "Tem a certeza que quer cancelar este OCR?", + "remove_confirm": "Tem a certeza que quer remover este ficheiro da fila?", + "cancel_success": "OCR cancelado com sucesso", + "remove_success": "Ficheiro removido da fila", + "cancel_error": "Falha ao cancelar OCR", + "remove_error": "Falha ao remover ficheiro da fila", + "currently_processing_files": "Ficheiros em Processamento", + "folder_queue_status": "Estado da Fila de Pastas", + "active_folders": "Pastas em Processamento", + "queued_folders_display": "Pastas em Espera na Fila", + "files": "ficheiros", + "position": "Posição", + "task": { + "file_ocr": "OCR de Ficheiro", + "ocr_from_api": "OCR via API", + "page_ocr": "OCR de Página", + "export_file": "Exportar Ficheiro", + "prepare_file": "Preparar Ficheiro", + "auto_segment": "Segmentação Automática" + } + }, + "ocr_help": { + "language": "Selecione a(s) língua(s) presentes no documento. Podem ser selecionadas múltiplas línguas se o documento contiver texto em várias línguas. A ordem é importante: liste primeiro a língua mais prevalente para obter melhor precisão.", + "special_modules": "Módulos de deteção especiais:\n• Módulo de deteção de matemática (equ): Permite o reconhecimento de equações e fórmulas matemáticas\n• Orientação e deteção de scripts (osd): Deteta automaticamente a orientação da página e o sistema de escrita", + "dpi": "Dots Per Inch - define a resolução da imagem digitalizada. Valores mais altos (300-600) proporcionam melhor precisão mas processamento mais lento. Deixe vazio para usar a resolução nativa do documento. Valores típicos: 150 (rascunho), 300 (padrão), 600 (alta qualidade).", + "preprocessing": "Pipeline de pré-processamento de imagem que melhora a qualidade da imagem antes do OCR. Aplica conversão para tons de cinza, realce de contraste (CLAHE), redução de ruído (median blur), limiarização, operações morfológicas e opcional correção de inclinação. Ative para melhores resultados com digitalizações de baixa qualidade ou documentos degradados.", + "engine_mode": "Método de processamento do motor OCR:\n• Original: Motor legado, mais rápido mas menos preciso\n• LSTM: Baseado em rede neural, mais preciso para fontes modernas\n• Combined: Usa ambos os motores para precisão máxima (mais lento)\n• Default: Configuração recomendada pelo sistema (tipicamente LSTM)", + "segmentation": "Como o motor identifica regiões de texto na página:\n• Default: Deteção automática (recomendado para a maioria dos documentos)\n• Single line/word/char: Para imagens contendo apenas um elemento de texto\n• Modos Column/Block: Para tipos específicos de layout (jornais, formulários)\n• Sparse text: Para documentos com elementos de texto dispersos", + "thresholding": "Método de binarização de imagem para converter tons de cinza em preto e branco:\n• Otsu: Cálculo automático de limiar (recomendado para a maioria dos casos)\n• Leptonica: Implementação alternativa de Otsu\n• Sauvola: Método adaptativo, melhor para documentos degradados com fundo variável", + "tesseract_thresholding": "Método de binarização integrado do Tesseract. É separado do limiar do pipeline de pré-processamento. Aplica-se apenas se o pré-processamento estiver desativado ou definido como 'None'. Opções: Otsu, Leptonica, Sauvola.", + "additional_params": "Parâmetros avançados do Tesseract no formato 'chave=valor'. Exemplo: 'tessedit_char_whitelist=0123456789' para reconhecer apenas dígitos. Consulte a documentação do Tesseract para opções disponíveis.", + "compress_pdf": "Aplica compressão de imagem para reduzir o tamanho do ficheiro PDF em 60-80% mantendo a precisão do texto. Recomendado para a maioria dos documentos. Desative apenas se necessitar da qualidade de imagem original ou estiver a ter problemas de processamento.", + "compression_target_dpi": "Resolução de saída em Pontos Por Polegada. Valores mais baixos (50-100) criam ficheiros menores com qualidade aceitável para texto. Valores mais altos (150-300) preservam mais detalhes mas aumentam o tamanho do ficheiro. Padrão: 100 DPI.", + "compression_bg_quality": "Qualidade JPEG para o fundo/imagens do documento (1-100). Valores mais baixos criam ficheiros menores. Recomendado: 30-50 para fundos. O texto não é afetado.", + "compression_fg_quality": "Qualidade JPEG para texto/camada de primeiro plano (1-100). Valores mais altos preservam a clareza do texto. Recomendado: 70-90 para texto legível. Padrão: 80.", + "compression_flatten": "Quando ativado, combina todas as camadas numa única imagem JPEG. Cria PDFs mais simples mas compressão menos eficiente. Desative para melhor compressão (usa MRC - Conteúdo Raster Misto com camadas separadas de fundo e texto).", + "output_formats": "Selecione um ou mais formatos de saída:\n• PDF com índice: PDF pesquisável com coordenadas ao nível de palavra\n• PDF com texto: PDF pesquisável padrão (recomendado)\n• Texto simples: Apenas texto, sem formatação\n• Texto delimitado por página: Texto com separadores de página\n• CSV: Coordenadas de palavras em formato de folha de cálculo\n• NER: Entidades nomeadas (pessoas, locais, organizações)\n• hOCR/ALTO: Formatos estruturados com informação detalhada de layout" + }, + "confirm delete": "Tem a certeza que quer apagar", + "page count": "página(s)", + "download starting": "O seu download vai começar em breves momentos", + "uncompressed": "não comprimido", + "pdf with index": "PDF com texto e índice de palavras", + "pdf with index uncompressed": "PDF com texto e índice (não comprimido)", + "pdf with text": "PDF com texto", + "pdf with text uncompressed": "PDF com texto (não comprimido)", + "plain text file": "Texto", + "text with page separators": "Texto com separadores de páginas", + "word index csv": "Índice de palavras em formato CSV", + "entities": "Entidades", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "Imagens extraídas", + "editing": { + "title": "Editar os resultados do documento", + "add_remove_lines": "Adicionar/Remover Linhas", + "show_simple_text": "Mostrar Texto Simples", + "show_confidence": "Mostrar Grau de Confiança", + "save": "Guardar", + "recreate_files": "Recriar Ficheiros", + "page": "Página", + "of": "de", + "words": "Palavras", + "check_spelling": "Verificar ortografia", + "error_load": "Não foi possível obter resultados", + "success_submit": "Texto submetido com sucesso", + "error_submit": "Não foi possível submeter os resultados" + } +} diff --git a/website/src/Languages/Russian/translation.json b/website/src/Languages/Russian/translation.json new file mode 100644 index 00000000..7d0341d4 --- /dev/null +++ b/website/src/Languages/Russian/translation.json @@ -0,0 +1,429 @@ +{ + "welcome": "Добро пожаловать", + "login": "Войти", + "private space": "Личное пространство", + "new folder": "Новая папка", + "new document": "Загрузить документ", + "leave space": "Покинуть пространство", + "back": "Назад", + "start": "Начало", + "grid view": "Вид сетки", + "list view": "Вид списка", + "empty folder title": "Эта папка пуста", + "empty folder description": "Добавьте документ или создайте подпапку для начала.", + "uploading": "Загрузка", + "ocr complete": "OCR завершено", + "pages": "страницы", + "document": "документ", + "documents": "документы", + "folder": "папка", + "folders": "папки", + "see document": "Просмотреть документ", + "edit text": "Редактировать текст", + "repeat ocr": "Повторить OCR", + "run ocr": "Запустить OCR", + "download txt": "Скачать TXT", + "download pdf": "Скачать PDF", + "download images": "Скачать изображения", + "download original": "Скачать оригинал", + "open folder": "Открыть папку", + "custom config": "Пользовательская конфигурация OCR", + "search": "Поиск", + "sync": "Синхронизация", + "finish": "Завершить", + "name": "Имя", + "details": "Детали", + "size": "Размер", + "date of creation": "Дата создания", + "process state": "Состояние процесса", + "version": "Версия", + "user manual": "Руководство пользователя", + "title": "Инструмент оптического распознавания символов", + "folder without files": "Эта папка пуста", + "sub-folder": "Подпапка", + "page": "Страница", + "words": "Слова", + "config ocr": "Настроить OCR", + "cancel ocr": "Отменить OCR", + "config strategy": "Стратегия конфигурации", + "config strategy override": "Использовать конфигурацию папки для всех файлов", + "config strategy respect": "Учитывать индивидуальную конфигурацию каждого файла", + "config strategy hybrid": "Использовать конфигурацию файла, если она существует, иначе конфигурацию папки", + "config strategy hint": "Выберите, как применять конфигурацию к файлам в этой папке", + "clean all": "Очистить всё", + "auto layout": "Автоматическая разметка", + "edit results": "Редактировать результаты", + "delete": "Удалить", + "index": "Индексировать", + "deindex": "Деиндексировать", + "save": "Сохранить", + "close": "Закрыть", + "retry": "Повторить", + "advanced": "Дополнительно", + "advanced ocr settings": "Дополнительные настройки OCR", + "upload error": "Ошибка загрузки файла", + "file download started": "Загрузка файла началась, пожалуйста, подождите", + "folder created successfully": "Папка успешно создана", + "folder name required": "Вы должны присвоить имя папке", + "ocr started": "OCR начато, пожалуйста, подождите", + "folder queued": "Ожидание в очереди папок", + "edited results files to recreate": "Отредактированные результаты, файлы для пересоздания", + "queued file": "В очереди (файл {{current}}/{{total}})", + "processing ocr page": "Обработка OCR - Страница {{current}}/{{total}}", + "generating results": "Генерация результатов", + "generating text": "Генерация текста", + "generating delimited text": "Генерация размеченного текста", + "generating pdf with index": "Генерация PDF с индексом", + "generating pdf": "Генерация PDF", + "generating csv": "Генерация CSV", + "generating images": "Генерация изображений", + "generating hocr": "Генерация hOCR", + "generating alto xml": "Генерация ALTO XML", + "generating index": "Генерация индекса", + "generating pdf with index page": "Генерация PDF с индексом {{current}}/{{total}}", + "generating pdf page": "Генерация PDF {{current}}/{{total}}", + "compression complete": "Сжатие завершено", + "compressing pdf finalizing": "Сжатие PDF - Завершение...", + "compressing pdf": "Сжатие PDF", + "compressing file": "Сжатие исходного файла...", + "error preparing document": "Ошибка при подготовке документа", + "error during ocr": "Ошибка во время OCR", + "error during ocr page": "Ошибка во время OCR страницы {{page}}", + "error generating results": "Ошибка при генерации результатов", + "ocr interrupted retry": "OCR прерван - вы можете попробовать снова", + "invalid parameters": "Недопустимые параметры", + "ocr cancelled": "OCR отменен", + "error canceling ocr": "Ошибка отмены OCR", + "uploading stage": "Загрузка, пожалуйста подождите...", + "preparing stage": "Подготовка документа...", + "preparing ocr": "Подготовка OCR...", + "confirm leave title": "Вы уверены, что хотите выйти?", + "confirm leave warning": "Если вы выйдете без сохранения, вы потеряете все внесенные изменения!", + "confirm leave button": "Выйти без сохранения", + "save and leave button": "Сохранить и выйти", + "saving": "Сохранение", + "compressing": "Сжатие", + "exporting": "Экспорт", + "ignore extract": "Игнорировать/Извлечь", + "join": "Объединить", + "separate": "Разделить", + "replicate": "Дублировать", + "create new folder": "Создать новую папку", + "folder name": "Имя папки*", + "folder name extra": "Имя не может начинаться с '_' или содержать '/' или '\\'", + "create": "Создать", + "auto layout popup": "Подготовка разметки, пожалуйста подождите...", + "layout loading": "Разметка создаётся...", + "layout create": "Создать разметку", + "configure ocr": "Настроить OCR", + "of document": "документа", + "of folder": "папки", + "languages": { + "arabic": "Арабский", + "chinese_simplified": "Китайский (упрощённый)", + "chinese_traditional": "Китайский (традиционный)", + "german": "Немецкий", + "english": "Английский", + "french": "Французский", + "hindi": "Хинди", + "indonesian": "Индонезийский (Bahasa Indonesia)", + "italian": "Итальянский", + "portuguese": "Португальский", + "russian": "Русский", + "spanish": "Испанский", + "math module": "Модуль обнаружения математики", + "osd module": "Модуль обнаружения ориентации и скрипта" + }, + "output": { + "pdf indexed": "PDF с текстом и индексом слов", + "pdf": "PDF с текстом (по умолчанию)", + "txt": "Простой текст", + "txt delimited": "Текст с разделением по страницам", + "csv": "Индекс слов в формате CSV", + "ner": "Именованные сущности (NER)", + "hocr": "hOCR (только одностраничные документы)", + "xml": "ALTO (только одностраничные документы)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract оригинальный", + "lstm": "Tesseract LSTM", + "combined": "Комбинированный LSTM + оригинальный", + "default": "Режим по умолчанию" + }, + "segmentation mode": { + "auto with osd": "Автоматическая сегментация страницы с OSD", + "auto no osd": "Автоматическая сегментация без OSD или OCR", + "default": "(По умолчанию) Автоматическая сегментация, без OSD", + "column variable lines": "Колонка текста с переменными размерами строк", + "block vertical": "Однородный вертикальный текстовый блок", + "block uniform": "Однородный текстовый блок", + "single line": "Изображение с одной строкой текста", + "single word": "Изображение с одним словом", + "single circle word": "Изображение с одним словом в круге", + "single char": "Изображение с одним символом", + "sparse text": "Разреженный текст; найти как можно больше без порядка", + "sparse text osd": "Разреженный текст с OSD", + "single line hack": "Обходной приём: обрабатывать изображение как одну строку текста" + }, + "threshold": { + "otsu": "Otsu (по умолчанию)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola" + }, + "default values": "Значения по умолчанию", + "output formats": "Форматы вывода", + "language": "Язык", + "languages_section": "Языки", + "special modules": "Специальные модули", + "more outputs": "Больше форматов", + "tesseract thresholding": "Пороговая обработка Tesseract", + "language hint": "Для лучших результатов выбирайте в порядке релевантности", + "language required": "Необходимо выбрать хотя бы один язык", + "output required": "Необходимо выбрать хотя бы один формат вывода", + "dpi": "DPI (точек на дюйм)", + "ocr engine": "Движок OCR", + "engine mode": "Режим движка", + "segmentation": "Сегментация", + "thresholding": "Пороговая обработка", + "additional parameters": "Дополнительные параметры", + "choose preset": "Выбрать предустановленную конфигурацию", + "select ocr preset": "Выбрать пресет OCR", + "use default": "Использовать по умолчанию", + "presets": { + "default": "По умолчанию", + "fast": "Быстрый", + "balanced": "Сбалансированный", + "high-quality": "Высокое качество", + "degraded-documents": "Поврежденные документы", + "multi-column": "Мульти-колонка", + "tables-forms": "Таблицы/Формы", + "default_desc": "Конфигурация системы по умолчанию", + "fast_desc": "Быстрая обработка - идеально для простых и чистых документов", + "balanced_desc": "Баланс между скоростью и качеством - хороший общий выбор", + "high_quality_desc": "Максимальная точность - для важных документов", + "degraded_documents_desc": "Оптимизировано для старых или низкокачественных сканов", + "multi_column_desc": "Для газет, журналов и многоколоночных макетов", + "tables_forms_desc": "Оптимизировано для таблиц, форм и структурированных данных" + }, + "lose results": "Вы потеряете последние результаты и предыдущие изменения!", + "begin": "Начать", + "clear all": "Очистить всё", + "alter existing config": "Изменить существующую конфигурацию", + "sync_title": "Синхронизировать внешние файлы", + "sync_description": "Импортировать файлы, добавленные непосредственно в папку хранения. Выберите область синхронизации:", + "sync_current": "Только текущая папка", + "sync_recursive": "Все подпапки", + "sync_success": "Импортировано файлов: {count}", + "sync_skipped": "Пропущено существующих файлов: {count}", + "sync_no_new": "Новых файлов не найдено", + "sync_error": "Ошибка синхронизации файлов", + "email": "Электронная почта", + "password": "Пароль", + "submit": "Отправить", + "logout": "Выйти", + "confirm": "Подтвердить", + "refresh": "Обновить", + "never": "никогда", + "days": "дней", + "hours": "часов", + "every": "Каждые", + "hour": "Час", + "day": "День", + "admin": { + "login_title": "Вход администратора OCR", + "email_password_incorrect": "Неверная электронная почта или пароль", + "free_storage": "Свободное хранилище", + "manage_storage": "Управление хранилищем", + "configure_ocr_defaults": "Настроить параметры OCR по умолчанию", + "view_workers_processes": "Просмотреть рабочие процессы", + "last_cleanup": "Последняя очистка", + "last_update": "Последнее обновление", + "remove_private_spaces_older": "Удалить личные пространства старше", + "change_max_age": "Изменить максимальный возраст", + "api_documents": "API документы", + "private_spaces": "Личные пространства", + "confirm_delete_space": "Вы уверены, что хотите удалить пространство", + "confirm_delete_document": "Вы уверены, что хотите удалить документ с ID", + "confirm_remove_sessions": "Вы уверены, что хотите удалить сеансы старше", + "set_cleanup_schedule": "Установить график автоматической очистки", + "by_interval": "По интервалу", + "weekly": "Еженедельно", + "week_days": "Дни недели", + "select_at_least_one_day": "Необходимо выбрать хотя бы один день", + "monthly": "Ежемесячно", + "dpi_integer_error": "Значение DPI должно быть целым числом!", + "manage_ocr_configurations": "Управление конфигурациями OCR", + "editing_configuration": "Редактирование конфигурации", + "default_config_cannot_delete": "Конфигурация по умолчанию не может быть удалена", + "creating_new_configuration": "Создание новой конфигурации:", + "default_config_must_define": "Конфигурация по умолчанию должна определять обязательные параметры", + "select_at_least_one_output": "Необходимо выбрать хотя бы один формат вывода", + "select_at_least_one_language": "Необходимо выбрать хотя бы один язык", + "save_configuration": "Сохранить конфигурацию", + "confirm_delete_configuration": "Вы уверены, что хотите удалить конфигурацию", + "request_failed": "Не удалось выполнить запрос.", + "hours_positive_integer": "Количество часов должно быть положительным целым числом!", + "day_between_1_31": "День должен быть числом от 1 до 31!", + "folder_concurrency_title": "Контроль Параллельной Обработки Папок OCR", + "max_concurrent_folders": "Максимум Одновременных Папок", + "active_folders": "Активные папки", + "queued_folders": "Папки в очереди", + "folder_concurrency_updated": "Лимит параллельной обработки папок обновлен" + }, + "weekdays": { + "monday": "Понедельник", + "tuesday": "Вторник", + "wednesday": "Среда", + "thursday": "Четверг", + "friday": "Пятница", + "saturday": "Суббота", + "sunday": "Воскресенье" + }, + "immediate ocr": "Немедленное распознавание", + "quick process": "Быстрая обработка", + "upload for immediate ocr": "Загрузить документ для немедленной обработки", + "select output formats": "Выбрать форматы вывода", + "plain text": "Простой текст", + "pdf simple": "PDF (Простой)", + "pdf searchable": "PDF (Поисковый/Индексированный)", + "process now": "Обработать сейчас", + "download text": "Скачать текст", + "processing document": "Обработка документа...", + "upload new document": "Загрузить новый документ", + "rerun with different settings": "Повторить с другими настройками", + "temporary processing note": "Файлы обрабатываются временно и будут удалены при загрузке нового документа или выходе с этой страницы.", + "drag drop files": "Перетащите файлы сюда или нажмите для выбора", + "file uploaded": "Файл загружен", + "no file uploaded": "Файл не загружен", + "clear results": "Очистить результаты", + "select languages": "Выбрать языки", + "select preset": "Выбрать предустановку", + "results ready": "Результаты готовы", + "download results": "Скачать результаты", + "file too large": "Файл слишком большой", + "unsupported file type": "Неподдерживаемый тип файла", + "upload failed": "Ошибка загрузки", + "processing failed": "Ошибка обработки", + "return to home": "Вернуться на главную", + "enable compression": "Включить сжатие PDF", + "compression info": "Уменьшает размер файла, но занимает больше времени и использует больше памяти. Отключите для более быстрой обработки.", + "compressing pdf starting": "Сжатие PDF - Начало...", + "compressing pdf page": "Сжатие PDF - Страница {{current}}/{{total}}", + "compressing pdf finalizing": "Сжатие PDF - Завершение...", + "compressing pdf": "Сжатие PDF", + "compression complete": "Сжатие завершено", + "compression quality": "Качество сжатия", + "compression auto": "Автоматическое (Рекомендуется)", + "compression auto desc": "Автоматически выбирает лучшее качество на основе размера файла", + "compression fast": "Быстрое", + "compression fast desc": "Более быстрая обработка, немного ниже качество (хорошо для больших файлов)", + "compression high quality": "Высокое качество", + "compression high quality desc": "Лучшее качество, более медленная обработка (хорошо для маленьких файлов)", + "compression settings": "Настройки сжатия", + "compression target dpi": "Целевой DPI", + "compression bg quality": "Качество фона (1-100)", + "compression fg quality": "Качество текста (1-100)", + "compression flatten to jpeg": "Свести в один слой JPEG", + "compressing pdf starting": "Сжатие PDF - Начало...", + "compressing pdf page": "Сжатие PDF - Страница {{current}}/{{total}}", + "compressing pdf finalizing": "Сжатие PDF - Завершение...", + "compressing pdf": "Сжатие PDF", + "compression complete": "Сжатие завершено", + "processing ocr": "Обработка OCR", + "processing ocr page": "Обработка OCR - Страница {{current}}/{{total}}", + "compressing file": "Сжатие исходного файла...", + "compressing file page": "Сжатие - Страница {{current}}/{{total}}", + "adding text layer": "Добавление текстового слоя OCR...", + "adding text page": "Добавление текста - Страница {{current}}/{{total}}", + "completed": "Завершено", + "error must select language": "Необходимо выбрать хотя бы один язык", + "error must select output": "Необходимо выбрать хотя бы один формат вывода", + "helper text language order": "Для лучших результатов выбирайте в порядке релевантности", + "error fetch default config": "Не удалось получить последнюю конфигурацию по умолчанию", + "error fetch preset": "Не удалось получить предустановленную конфигурацию", + "error fetch presets list": "Не удалось обновить список предустановленных конфигураций", + "error dpi must be integer": "Значение DPI должно быть целым числом!", + "success config saved": "Конфигурация OCR успешно сохранена.", + "error config save unexpected": "Неожиданная ошибка при сохранении конфигурации OCR.", + "error config save failed": "Не удалось сохранить конфигурацию OCR.", + "queue": { + "title": "Состояние очереди задач", + "page_title": "Состояние очереди", + "tooltip": "Состояние очереди", + "view_details": "Нажмите для просмотра деталей очереди", + "active": "активных", + "pending": "в очереди", + "queued": "Задачи в очереди", + "processing": "Обработка", + "finished": "Завершено", + "ago": "назад", + "queued_files": "Файлы в ожидании в последовательной очереди", + "scheduled": "Запланированные задачи", + "workers": "Рабочие процессы", + "idle": "Простаивает", + "error": "Ошибка загрузки состояния очереди", + "task_breakdown": "Разбивка задач", + "currently_processing": "Обрабатывается сейчас", + "waiting_in_queue": "Ожидание в очереди", + "scheduled_for_later": "Запланировано на позже", + "worker_details": "Детали рабочих процессов", + "cancel": "Отменить", + "remove": "Удалить", + "cancel_confirm": "Вы уверены, что хотите отменить этот OCR?", + "remove_confirm": "Вы уверены, что хотите удалить этот файл из очереди?", + "cancel_success": "OCR успешно отменён", + "remove_success": "Файл удалён из очереди", + "cancel_error": "Не удалось отменить OCR", + "remove_error": "Не удалось удалить файл из очереди", + "currently_processing_files": "Файлы в Обработке", + "folder_queue_status": "Состояние Очереди Папок", + "active_folders": "Папки в Обработке", + "queued_folders_display": "Папки в Очереди", + "files": "файлов", + "position": "Позиция", + "task": { + "file_ocr": "OCR файла", + "ocr_from_api": "OCR через API", + "page_ocr": "OCR страницы", + "export_file": "Экспорт файла", + "prepare_file": "Подготовка файла", + "auto_segment": "Автоматическая сегментация" + } + }, + "ocr_help": { + "language": "Выберите язык(и), присутствующий в документе. Можно выбрать несколько языков, если документ содержит смешанные языки. Порядок важен: указывайте наиболее распространённый язык первым для лучшей точности.", + "special_modules": "Специальные модули обнаружения:\n• Модуль обнаружения математики (equ): Позволяет распознавать математические уравнения и формулы\n• Обнаружение ориентации и скрипта (osd): Автоматически определяет ориентацию страницы и систему письма", + "dpi": "Точек на дюйм - определяет разрешение отсканированного изображения. Более высокие значения (300-600) обеспечивают лучшую точность, но медленнее обработку. Оставьте пустым для использования собственного разрешения документа. Типичные значения: 150 (черновик), 300 (стандарт), 600 (высокое качество).", + "preprocessing": "Конвейер предварительной обработки изображения, который улучшает качество изображения перед OCR. Применяет преобразование в градации серого, улучшение контраста (CLAHE), снижение шума (медианное размытие), пороговую обработку, морфологические операции и опциональную коррекцию наклона. Включите для лучших результатов с низкокачественными сканами или повреждёнными документами.", + "engine_mode": "Метод обработки движка OCR:\n• Original: Устаревший движок, быстрее, но менее точный\n• LSTM: На основе нейронной сети, более точный для современных шрифтов\n• Combined: Использует оба движка для максимальной точности (медленнее)\n• Default: Рекомендуемая системой настройка (обычно LSTM)", + "segmentation": "Как движок определяет текстовые области на странице:\n• Default: Автоматическое определение (рекомендуется для большинства документов)\n• Single line/word/char: Для изображений, содержащих только один текстовый элемент\n• Режимы Column/Block: Для конкретных типов макета (газеты, формы)\n• Sparse text: Для документов с разбросанными текстовыми элементами", + "thresholding": "Метод бинаризации изображения для преобразования градаций серого в чёрно-белое:\n• Otsu: Автоматический расчёт порога (рекомендуется для большинства случаев)\n• Leptonica: Альтернативная реализация Otsu\n• Sauvola: Адаптивный метод, лучше для повреждённых документов с меняющимся фоном", + "tesseract_thresholding": "Встроенный метод бинаризации изображения Tesseract. Это отдельно от порога конвейера предварительной обработки. Применяется только если предварительная обработка отключена или установлена в 'None'. Опции: Otsu, Leptonica, Sauvola.", + "additional_params": "Расширенные параметры Tesseract в формате 'ключ=значение'. Пример: 'tessedit_char_whitelist=0123456789' для распознавания только цифр. Обратитесь к документации Tesseract для доступных опций.", + "compress_pdf": "Применяет сжатие изображения для уменьшения размера файла PDF на 60-80% при сохранении точности текста. Рекомендуется для большинства документов. Отключайте только если вам нужно оригинальное качество изображения или возникают проблемы с обработкой.", + "compression_target_dpi": "Выходное разрешение в точках на дюйм. Более низкие значения (50-100) создают меньшие файлы с приемлемым качеством для текста. Более высокие значения (150-300) сохраняют больше деталей, но увеличивают размер файла. По умолчанию: 100 DPI.", + "compression_bg_quality": "Качество JPEG для фона/изображений документа (1-100). Более низкие значения создают меньшие файлы. Рекомендуется: 30-50 для фона. Текст остаётся неизменным.", + "compression_fg_quality": "Качество JPEG для текста/переднего плана (1-100). Более высокие значения сохраняют чёткость текста. Рекомендуется: 70-90 для читаемого текста. По умолчанию: 80.", + "compression_flatten": "При включении объединяет все слои в одно изображение JPEG. Создаёт более простые PDF, но менее эффективное сжатие. Отключите для лучшего сжатия (использует MRC - смешанное растровое содержимое с отдельными слоями фона и текста).", + "output_formats": "Выберите один или несколько форматов вывода:\n• PDF с индексом: Поисковый PDF с координатами на уровне слов\n• PDF с текстом: Стандартный поисковый PDF (рекомендуется)\n• Простой текст: Только текст, без форматирования\n• Текст с разделением по страницам: Текст с разделителями страниц\n• CSV: Координаты слов в формате электронной таблицы\n• NER: Именованные сущности (люди, места, организации)\n• hOCR/ALTO: Структурированные форматы с подробной информацией о макете" + }, + "confirm delete": "Вы уверены, что хотите удалить", + "page count": "страниц(ы)", + "download starting": "Загрузка начнётся в ближайшее время", + "uncompressed": "несжатый", + "pdf with index": "PDF с текстом и индексом слов", + "pdf with index uncompressed": "PDF с текстом и индексом (несжатый)", + "pdf with text": "PDF с текстом", + "pdf with text uncompressed": "PDF с текстом (несжатый)", + "plain text file": "Текст", + "text with page separators": "Текст с разделителями страниц", + "word index csv": "Индекс слов в формате CSV", + "entities": "Сущности", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "Извлечённые изображения" +} diff --git a/website/src/Languages/Spanish/translation.json b/website/src/Languages/Spanish/translation.json new file mode 100644 index 00000000..65217520 --- /dev/null +++ b/website/src/Languages/Spanish/translation.json @@ -0,0 +1,429 @@ +{ + "welcome": "Bienvenido", + "login": "Iniciar sesión", + "private space": "Espacio privado", + "new folder": "Nueva carpeta", + "new document": "Cargar documento", + "leave space": "Salir del espacio", + "back": "Atrás", + "start": "Inicio", + "grid view": "Vista de cuadrícula", + "list view": "Vista de lista", + "empty folder title": "Esta carpeta está vacía", + "empty folder description": "Añade un documento o crea una subcarpeta para empezar.", + "uploading": "Subiendo", + "ocr complete": "OCR completado", + "pages": "páginas", + "document": "documento", + "documents": "documentos", + "folder": "carpeta", + "folders": "carpetas", + "see document": "Ver documento", + "edit text": "Editar texto", + "repeat ocr": "Repetir OCR", + "run ocr": "Ejecutar OCR", + "download txt": "Descargar TXT", + "download pdf": "Descargar PDF", + "download images": "Descargar imágenes", + "download original": "Descargar original", + "open folder": "Abrir carpeta", + "custom config": "Configuración OCR personalizada", + "search": "Buscar", + "sync": "Sincronizar", + "finish": "Finalizar", + "name": "Nombre", + "details": "Detalles", + "size": "Tamaño", + "date of creation": "Fecha de creación", + "process state": "Estado del proceso", + "version": "Versión", + "user manual": "Manual de usuario", + "title": "Herramienta de reconocimiento óptico de caracteres", + "folder without files": "Esta carpeta está vacía", + "sub-folder": "Subcarpeta", + "page": "Página", + "words": "Palabras", + "config ocr": "Configurar OCR", + "cancel ocr": "Cancelar OCR", + "config strategy": "Estrategia de Configuración", + "config strategy override": "Usar configuración de carpeta para todos los archivos", + "config strategy respect": "Respetar configuración individual de cada archivo", + "config strategy hybrid": "Usar configuración de archivo si existe, de lo contrario usar la de carpeta", + "config strategy hint": "Elija cómo aplicar la configuración a los archivos de esta carpeta", + "clean all": "Limpiar todo", + "auto layout": "Diseño automático", + "edit results": "Editar resultados", + "delete": "Eliminar", + "index": "Indexar", + "deindex": "Desindexar", + "save": "Guardar", + "close": "Cerrar", + "retry": "Reintentar", + "advanced": "Avanzado", + "advanced ocr settings": "Configuración Avanzada de OCR", + "upload error": "Error al subir el archivo", + "file download started": "La descarga del archivo ha comenzado, por favor espere", + "folder created successfully": "Carpeta creada con éxito", + "folder name required": "Debe asignar un nombre a la carpeta", + "ocr started": "OCR iniciado, por favor espere", + "folder queued": "Esperando en la cola de carpetas", + "edited results files to recreate": "Resultados editados, archivos por recrear", + "queued file": "En cola (archivo {{current}}/{{total}})", + "processing ocr page": "Procesando OCR - Página {{current}}/{{total}}", + "generating results": "Generando resultados", + "generating text": "Generando texto", + "generating delimited text": "Generando texto delimitado", + "generating pdf with index": "Generando PDF con índice", + "generating pdf": "Generando PDF", + "generating csv": "Generando CSV", + "generating images": "Generando imágenes", + "generating hocr": "Generando hOCR", + "generating alto xml": "Generando ALTO XML", + "generating index": "Generando índice", + "generating pdf with index page": "Generando PDF con índice {{current}}/{{total}}", + "generating pdf page": "Generando PDF {{current}}/{{total}}", + "compression complete": "Compresión completa", + "compressing pdf finalizing": "Comprimiendo PDF - Finalizando...", + "compressing pdf": "Comprimiendo PDF", + "compressing file": "Comprimiendo archivo original...", + "error preparing document": "Error al preparar el documento", + "error during ocr": "Error durante OCR", + "error during ocr page": "Error durante OCR de la página {{page}}", + "error generating results": "Error al generar resultados", + "ocr interrupted retry": "OCR interrumpido - puede intentarlo de nuevo", + "invalid parameters": "Parámetros inválidos", + "ocr cancelled": "OCR Cancelado", + "error canceling ocr": "Error al cancelar OCR", + "uploading stage": "Subiendo, por favor espere...", + "preparing stage": "Preparando documento...", + "preparing ocr": "Preparando OCR...", + "confirm leave title": "¿Estás seguro de que quieres salir?", + "confirm leave warning": "¡Si sales sin guardar, perderás cualquier cambio que hayas hecho!", + "confirm leave button": "Salir sin guardar", + "save and leave button": "Guardar y salir", + "saving": "Guardando", + "compressing": "Comprimiendo", + "exporting": "Exportando", + "ignore extract": "Ignorar/Extraer", + "join": "Unir", + "separate": "Separar", + "replicate": "Duplicar", + "create new folder": "Crear una nueva carpeta", + "folder name": "Nombre de la carpeta*", + "folder name extra": "El nombre no puede empezar con '_' ni contener '/' o '\\'", + "create": "Crear", + "auto layout popup": "Preparando diseño, por favor espere...", + "layout loading": "Se está creando el diseño...", + "layout create": "Crear diseño", + "configure ocr": "Configurar OCR", + "of document": "del documento", + "of folder": "de la carpeta", + "languages": { + "arabic": "Árabe", + "chinese_simplified": "Chino (simplificado)", + "chinese_traditional": "Chino (tradicional)", + "german": "Alemán", + "english": "Inglés", + "french": "Francés", + "hindi": "Hindi", + "indonesian": "Indonesio (Bahasa Indonesia)", + "italian": "Italiano", + "portuguese": "Portugués", + "russian": "Ruso", + "spanish": "Español", + "math module": "Módulo de detección matemática", + "osd module": "Módulo de detección de orientación y script" + }, + "output": { + "pdf indexed": "PDF con texto e índice de palabras", + "pdf": "PDF con texto (predeterminado)", + "txt": "Texto plano", + "txt delimited": "Texto delimitado por páginas", + "csv": "Índice de palabras en formato CSV", + "ner": "Entidades nombradas (NER)", + "hocr": "hOCR (solo documentos de una página)", + "xml": "ALTO (solo documentos de una página)" + }, + "engine": { + "pytesseract": "PyTesseract", + "tesserOCR": "TesserOCR" + }, + "mode": { + "original": "Tesseract Original", + "lstm": "Tesseract LSTM", + "combined": "LSTM + Original combinado", + "default": "Modo predeterminado" + }, + "segmentation mode": { + "auto with osd": "Segmentación automática de página con OSD", + "auto no osd": "Segmentación automática sin OSD ni OCR", + "default": "(Predeterminado) Segmentación automática, sin OSD", + "column variable lines": "Columna de texto con tamaños de línea variables", + "block vertical": "Bloque de texto vertical uniforme", + "block uniform": "Bloque de texto uniforme", + "single line": "Imagen con una sola línea de texto", + "single word": "Imagen con una sola palabra", + "single circle word": "Imagen con una sola palabra en un círculo", + "single char": "Imagen con un solo carácter", + "sparse text": "Texto disperso; encontrar todo lo posible sin orden", + "sparse text osd": "Texto disperso con OSD", + "single line hack": "Truco de omisión: tratar imagen como una sola línea de texto" + }, + "threshold": { + "otsu": "Otsu (predeterminado)", + "leptonica": "Leptonica Otsu", + "sauvola": "Sauvola" + }, + "default values": "Valores predeterminados", + "output formats": "Formatos de salida", + "language": "Idioma", + "languages_section": "Idiomas", + "special modules": "Módulos especiales", + "more outputs": "Más formatos", + "tesseract thresholding": "Umbralización de Tesseract", + "language hint": "Para mejores resultados, seleccione en orden de relevancia", + "language required": "Debe seleccionar al menos un idioma", + "output required": "Debe seleccionar al menos un formato de salida", + "dpi": "DPI (puntos por pulgada)", + "ocr engine": "Motor OCR", + "engine mode": "Modo del motor", + "segmentation": "Segmentación", + "thresholding": "Umbralización", + "additional parameters": "Parámetros adicionales", + "choose preset": "Elegir configuración predefinida", + "select ocr preset": "Seleccionar preset de OCR", + "use default": "Usar predeterminado", + "presets": { + "default": "Predeterminado", + "fast": "Rápido", + "balanced": "Equilibrado", + "high-quality": "Alta Calidad", + "degraded-documents": "Documentos Degradados", + "multi-column": "Multi-Columna", + "tables-forms": "Tablas/Formularios", + "default_desc": "Configuración predeterminada del sistema", + "fast_desc": "Procesamiento rápido - ideal para documentos simples y limpios", + "balanced_desc": "Balance entre velocidad y calidad - buena elección general", + "high_quality_desc": "Máxima precisión - para documentos importantes", + "degraded_documents_desc": "Optimizado para documentos antiguos o de baja calidad", + "multi_column_desc": "Para periódicos, revistas y diseños de múltiples columnas", + "tables_forms_desc": "Optimizado para tablas, formularios y datos estructurados" + }, + "lose results": "¡Perderá sus últimos resultados y cambios previos!", + "begin": "Comenzar", + "clear all": "Limpiar todo", + "alter existing config": "Modificar configuración existente", + "sync_title": "Sincronizar archivos externos", + "sync_description": "Importar archivos que fueron añadidos directamente a la carpeta de almacenamiento. Elija el alcance de la sincronización:", + "sync_current": "Solo carpeta actual", + "sync_recursive": "Todas las subcarpetas", + "sync_success": "{count} archivo(s) importado(s)", + "sync_skipped": "{count} archivo(s) existente(s) omitido(s)", + "sync_no_new": "No se encontraron archivos nuevos", + "sync_error": "Error al sincronizar archivos", + "email": "Correo electrónico", + "password": "Contraseña", + "submit": "Enviar", + "logout": "Cerrar sesión", + "confirm": "Confirmar", + "refresh": "Actualizar", + "never": "nunca", + "days": "día(s)", + "hours": "horas", + "every": "Cada", + "hour": "Hora", + "day": "Día", + "admin": { + "login_title": "Inicio de sesión de administrador OCR", + "email_password_incorrect": "Correo electrónico o contraseña incorrectos", + "free_storage": "Almacenamiento libre", + "manage_storage": "Administrar almacenamiento", + "configure_ocr_defaults": "Configurar valores predeterminados de OCR", + "view_workers_processes": "Ver workers y procesos", + "last_cleanup": "Última limpieza", + "last_update": "Última actualización", + "remove_private_spaces_older": "Eliminar espacios privados con más de", + "change_max_age": "Cambiar edad máxima", + "api_documents": "Documentos de API", + "private_spaces": "Espacios privados", + "confirm_delete_space": "¿Está seguro de que desea eliminar el espacio", + "confirm_delete_document": "¿Está seguro de que desea eliminar el documento con ID", + "confirm_remove_sessions": "¿Está seguro de que desea eliminar las sesiones con más de", + "set_cleanup_schedule": "Establecer programa de limpieza automática", + "by_interval": "Por intervalo", + "weekly": "Semanalmente", + "week_days": "Días de la semana", + "select_at_least_one_day": "Debe seleccionar al menos un día", + "monthly": "Mensualmente", + "dpi_integer_error": "¡El valor de DPI debe ser un número entero!", + "manage_ocr_configurations": "Administrar configuraciones de OCR", + "editing_configuration": "Editando configuración", + "default_config_cannot_delete": "La configuración predeterminada no se puede eliminar", + "creating_new_configuration": "Creando nueva configuración:", + "default_config_must_define": "La configuración predeterminada debe definir los parámetros obligatorios", + "select_at_least_one_output": "Debe seleccionar al menos un formato de salida", + "select_at_least_one_language": "Debe seleccionar al menos un idioma", + "save_configuration": "Guardar configuración", + "confirm_delete_configuration": "¿Está seguro de que desea eliminar la configuración", + "request_failed": "No se pudo completar la solicitud.", + "hours_positive_integer": "¡El número de horas debe ser un entero positivo!", + "day_between_1_31": "¡El día debe ser un número entre 1 y 31!", + "folder_concurrency_title": "Control de Concurrencia de OCR de Carpetas", + "max_concurrent_folders": "Máximo de Carpetas Simultáneas", + "active_folders": "Carpetas activas", + "queued_folders": "Carpetas en cola", + "folder_concurrency_updated": "Límite de concurrencia de carpetas actualizado" + }, + "weekdays": { + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo" + }, + "immediate ocr": "OCR Inmediato", + "quick process": "Procesamiento Rápido", + "upload for immediate ocr": "Cargar Documento para Procesamiento Inmediato", + "select output formats": "Seleccionar Formatos de Salida", + "plain text": "Texto Plano", + "pdf simple": "PDF (Simple)", + "pdf searchable": "PDF (Buscable/Indexado)", + "process now": "Procesar Ahora", + "download text": "Descargar Texto", + "processing document": "Procesando documento...", + "upload new document": "Cargar Nuevo Documento", + "rerun with different settings": "Volver a Ejecutar con Configuraciones Diferentes", + "temporary processing note": "Los archivos se procesan temporalmente y se eliminarán cuando cargue un nuevo documento o salga de esta página.", + "drag drop files": "Arrastre archivos aquí o haga clic para seleccionar", + "file uploaded": "Archivo cargado", + "no file uploaded": "Ningún archivo cargado", + "clear results": "Borrar Resultados", + "select languages": "Seleccionar Idiomas", + "select preset": "Seleccionar Preset", + "results ready": "Resultados Listos", + "download results": "Descargar Resultados", + "file too large": "Archivo demasiado grande", + "unsupported file type": "Tipo de archivo no soportado", + "upload failed": "Error al cargar", + "processing failed": "Error al procesar", + "return to home": "Volver al Inicio", + "enable compression": "Activar Compresión de PDF", + "compression info": "Reduce el tamaño del archivo pero tarda más y usa más memoria. Desactivar para procesamiento más rápido.", + "compression quality": "Calidad de Compresión", + "compression auto": "Automático (Recomendado)", + "compression auto desc": "Elige automáticamente la mejor calidad según el tamaño del archivo", + "compression fast": "Rápido", + "compression fast desc": "Procesamiento más rápido, calidad ligeramente inferior (bueno para archivos grandes)", + "compression high quality": "Alta Calidad", + "compression high quality desc": "Mejor calidad, procesamiento más lento (bueno para archivos pequeños)", + "compressing pdf starting": "Comprimiendo PDF - Iniciando...", + "compressing pdf page": "Comprimiendo PDF - Página {{current}}/{{total}}", + "compressing pdf finalizing": "Comprimiendo PDF - Finalizando...", + "compressing pdf": "Comprimiendo PDF", + "compression complete": "Compresión completa", + "compression settings": "Configuraciones de Compresión", + "compression target dpi": "DPI de Destino", + "compression bg quality": "Calidad del Fondo (1-100)", + "compression fg quality": "Calidad del Texto (1-100)", + "compression flatten to jpeg": "Aplanar a Capa JPEG Única", + "compressing pdf starting": "Comprimiendo PDF - Iniciando...", + "compressing pdf page": "Comprimiendo PDF - Página {{current}}/{{total}}", + "compressing pdf finalizing": "Comprimiendo PDF - Finalizando...", + "compressing pdf": "Comprimiendo PDF", + "compression complete": "Compresión completa", + "processing ocr": "Procesando OCR", + "processing ocr page": "Procesando OCR - Página {{current}}/{{total}}", + "compressing file": "Comprimiendo archivo original...", + "compressing file page": "Comprimiendo - Página {{current}}/{{total}}", + "adding text layer": "Agregando capa de texto OCR...", + "adding text page": "Agregando texto - Página {{current}}/{{total}}", + "completed": "Completado", + "error must select language": "Debe seleccionar al menos un idioma", + "error must select output": "Debe seleccionar al menos un formato de salida", + "helper text language order": "Para mejores resultados, seleccione en orden de relevancia", + "error fetch default config": "No se pudo obtener la configuración predeterminada más reciente", + "error fetch preset": "No se pudo obtener la configuración preestablecida", + "error fetch presets list": "No se pudo actualizar la lista de configuraciones preestablecidas", + "error dpi must be integer": "¡El valor de DPI debe ser un número entero!", + "success config saved": "Configuración de OCR guardada con éxito.", + "error config save unexpected": "Error inesperado al guardar la configuración de OCR.", + "error config save failed": "No se pudo guardar la configuración de OCR.", + "queue": { + "title": "Estado de la Cola de Tareas", + "page_title": "Estado de la Cola", + "tooltip": "Estado de la Cola", + "view_details": "Haga clic para ver detalles de la cola", + "active": "activas", + "pending": "en cola", + "queued": "Tareas en Cola", + "processing": "Procesando", + "finished": "Terminado", + "ago": "hace", + "queued_files": "Archivos Esperando en Cola Secuencial", + "scheduled": "Tareas Programadas", + "workers": "Trabajadores", + "idle": "Inactivo", + "error": "Error al cargar el estado de la cola", + "task_breakdown": "Desglose de Tareas", + "currently_processing": "Procesando Actualmente", + "waiting_in_queue": "Esperando en Cola", + "scheduled_for_later": "Programadas para Más Tarde", + "worker_details": "Detalles de Trabajadores", + "cancel": "Cancelar", + "remove": "Eliminar", + "cancel_confirm": "¿Está seguro de que desea cancelar este OCR?", + "remove_confirm": "¿Está seguro de que desea eliminar este archivo de la cola?", + "cancel_success": "OCR cancelado con éxito", + "remove_success": "Archivo eliminado de la cola", + "cancel_error": "Error al cancelar OCR", + "remove_error": "Error al eliminar archivo de la cola", + "currently_processing_files": "Archivos en Procesamiento", + "folder_queue_status": "Estado de Cola de Carpetas", + "active_folders": "Carpetas en Procesamiento", + "queued_folders_display": "Carpetas en Espera", + "files": "archivos", + "position": "Posición", + "task": { + "file_ocr": "OCR de Archivo", + "ocr_from_api": "OCR desde API", + "page_ocr": "OCR de Página", + "export_file": "Exportar Archivo", + "prepare_file": "Preparar Archivo", + "auto_segment": "Segmentación Automática" + } + }, + "ocr_help": { + "language": "Seleccione el/los idioma(s) presente(s) en el documento. Se pueden seleccionar varios idiomas si el documento contiene idiomas mixtos. El orden importa: liste primero el idioma más prevalente para obtener la mejor precisión.", + "special_modules": "Módulos de detección especiales:\n• Módulo de detección matemática (equ): Permite el reconocimiento de ecuaciones y fórmulas matemáticas\n• Detección de orientación y script (osd): Detecta automáticamente la orientación de la página y el sistema de escritura", + "dpi": "Puntos por Pulgada - define la resolución de la imagen escaneada. Valores más altos (300-600) proporcionan mejor precisión pero procesamiento más lento. Déjelo vacío para usar la resolución nativa del documento. Valores típicos: 150 (borrador), 300 (estándar), 600 (alta calidad).", + "preprocessing": "Pipeline de preprocesamiento de imagen que mejora la calidad de la imagen antes del OCR. Aplica conversión a escala de grises, mejora de contraste (CLAHE), reducción de ruido (desenfoque mediano), umbralización, operaciones morfológicas y corrección de inclinación opcional. Active para mejores resultados con escaneos de baja calidad o documentos degradados.", + "engine_mode": "Método de procesamiento del motor OCR:\n• Original: Motor heredado, más rápido pero menos preciso\n• LSTM: Basado en red neuronal, más preciso para fuentes modernas\n• Combined: Usa ambos motores para máxima precisión (más lento)\n• Default: Configuración recomendada por el sistema (típicamente LSTM)", + "segmentation": "Cómo el motor identifica regiones de texto en la página:\n• Default: Detección automática (recomendado para la mayoría de documentos)\n• Single line/word/char: Para imágenes que contienen solo un elemento de texto\n• Modos Column/Block: Para tipos de diseño específicos (periódicos, formularios)\n• Sparse text: Para documentos con elementos de texto dispersos", + "thresholding": "Método de binarización de imagen para convertir escala de grises a blanco y negro:\n• Otsu: Cálculo automático de umbral (recomendado para la mayoría de casos)\n• Leptonica: Implementación alternativa de Otsu\n• Sauvola: Método adaptativo, mejor para documentos degradados con fondo variable", + "tesseract_thresholding": "Método de binarización de imagen integrado de Tesseract. Es independiente del umbral del pipeline de preprocesamiento. Se aplica solo si el preprocesamiento está desactivado o configurado como 'None'. Opciones: Otsu, Leptonica, Sauvola.", + "additional_params": "Parámetros avanzados de Tesseract en formato 'clave=valor'. Ejemplo: 'tessedit_char_whitelist=0123456789' para reconocer solo dígitos. Consulte la documentación de Tesseract para opciones disponibles.", + "compress_pdf": "Aplica compresión de imagen para reducir el tamaño del archivo PDF en un 60-80% manteniendo la precisión del texto. Recomendado para la mayoría de documentos. Desactive solo si necesita calidad de imagen original o está experimentando problemas de procesamiento.", + "compression_target_dpi": "Resolución de salida en Puntos por Pulgada. Valores más bajos (50-100) crean archivos más pequeños con calidad aceptable para texto. Valores más altos (150-300) preservan más detalles pero aumentan el tamaño del archivo. Predeterminado: 100 DPI.", + "compression_bg_quality": "Calidad JPEG para fondo/imágenes del documento (1-100). Valores más bajos crean archivos más pequeños. Recomendado: 30-50 para fondos. El texto no se ve afectado.", + "compression_fg_quality": "Calidad JPEG para capa de texto/primer plano (1-100). Valores más altos preservan la claridad del texto. Recomendado: 70-90 para texto legible. Predeterminado: 80.", + "compression_flatten": "Cuando está habilitado, combina todas las capas en una sola imagen JPEG. Crea PDFs más simples pero compresión menos eficiente. Deshabilite para mejor compresión (usa MRC - Contenido Ráster Mixto con capas separadas de fondo y texto).", + "output_formats": "Seleccione uno o más formatos de salida:\n• PDF con índice: PDF consultable con coordenadas a nivel de palabra\n• PDF con texto: PDF consultable estándar (recomendado)\n• Texto plano: Solo texto, sin formato\n• Texto delimitado por página: Texto con separadores de página\n• CSV: Coordenadas de palabras en formato de hoja de cálculo\n• NER: Entidades nombradas (personas, lugares, organizaciones)\n• hOCR/ALTO: Formatos estructurados con información detallada de diseño" + }, + "confirm delete": "¿Está seguro de que desea eliminar", + "page count": "página(s)", + "download starting": "Su descarga comenzará en breve", + "uncompressed": "sin comprimir", + "pdf with index": "PDF con texto e índice de palabras", + "pdf with index uncompressed": "PDF con texto e índice (sin comprimir)", + "pdf with text": "PDF con texto", + "pdf with text uncompressed": "PDF con texto (sin comprimir)", + "plain text file": "Texto", + "text with page separators": "Texto con separadores de página", + "word index csv": "Índice de palabras en formato CSV", + "entities": "Entidades", + "hocr": "hOCR", + "alto": "ALTO", + "extracted images": "Imágenes extraídas" +} diff --git a/website/src/defaultOcrConfigs.js b/website/src/defaultOcrConfigs.js index 1d16b187..74293227 100644 --- a/website/src/defaultOcrConfigs.js +++ b/website/src/defaultOcrConfigs.js @@ -1,64 +1,97 @@ +import i18next from "i18next"; + export const defaultLangs = ["por"]; -export const tesseractLangList = [ - { value: "deu", description: "Alemão"}, - { value: "spa", description: "Espanhol Castelhano"}, - { value: "fra", description: "Francês"}, - { value: "eng", description: "Inglês"}, - { value: "por", description: "Português"}, - { value: "equ", description: "Módulo de detecção de matemática / equações"}, - { value: "osd", description: "Módulo de orientação e detecção de scripts"}, -] + +// Actual language packs +export const tesseractLanguagesList = () => [ + { value: "ara", translationKey: "languages.arabic" }, + { value: "chi_sim", translationKey: "languages.chinese_simplified" }, + { value: "chi_tra", translationKey: "languages.chinese_traditional" }, + { value: "deu", translationKey: "languages.german" }, + { value: "eng", translationKey: "languages.english" }, + { value: "fra", translationKey: "languages.french" }, + { value: "hin", translationKey: "languages.hindi" }, + { value: "ind", translationKey: "languages.indonesian" }, + { value: "ita", translationKey: "languages.italian" }, + { value: "por", translationKey: "languages.portuguese" }, + { value: "rus", translationKey: "languages.russian" }, + { value: "spa", translationKey: "languages.spanish" }, +]; + +// Special detection modules +export const tesseractModulesList = () => [ + { value: "equ", translationKey: "languages.math module" }, + { value: "osd", translationKey: "languages.osd module" }, +]; + +// Combined list for backwards compatibility +export const tesseractLangList = () => [ + ...tesseractLanguagesList(), + ...tesseractModulesList(), +]; export const defaultOutputs = ["pdf"]; -export const tesseractOutputsList = [ - { value: "pdf_indexed", description: "PDF com texto e índice de palavras"}, - { value: "pdf", description: "PDF com texto (por defeito)"}, - { value: "txt", description: "Texto"}, - { value: "txt_delimited", description: "Texto com separador por página"}, - { value: "csv", description: "Índice de palavras em formato CSV"}, - { value: "ner", description: "Entidades (NER)"}, - { value: "hocr", description: "hOCR (apenas documentos com 1 página)"}, - { value: "xml", description: "ALTO (apenas documentos com 1 página)"}, -] -export const defaultEngine = "pytesseract"; -export const engineList = [ - { value: "pytesseract", description: "PyTesseract"}, - { value: "tesserOCR", description: "TesserOCR"}, -] +// Primary outputs shown on main page +export const tesseractMainOutputsList = () => [ + { value: "pdf", translationKey: "output.pdf" }, + { value: "pdf_indexed", translationKey: "output.pdf indexed" }, + { value: "txt", translationKey: "output.txt" }, +]; + +// Advanced outputs shown in "More outputs" dialog +export const tesseractAdvancedOutputsList = () => [ + { value: "txt_delimited", translationKey: "output.txt delimited" }, + { value: "csv", translationKey: "output.csv" }, + { value: "ner", translationKey: "output.ner" }, + { value: "hocr", translationKey: "output.hocr" }, + { value: "xml", translationKey: "output.xml" }, +]; + +// Combined list for backwards compatibility +export const tesseractOutputsList = () => [ + ...tesseractMainOutputsList(), + ...tesseractAdvancedOutputsList(), +]; + +export const defaultEngine = "tesserocr"; +export const engineList = () => [ + { value: "tesserocr", description: i18next.t("engine.tesserOCR") }, +]; export const defaultEngineMode = 3; -export const tesseractModeList = [ - { value: 0, description: "Tesseract Original"}, - { value: 1, description: "Tesseract LSTM"}, - { value: 2, description: "Tesseract LSTM + Original combinado"}, - { value: 3, description: "Modo disponível por defeito"}, -] +export const tesseractModeList = () => [ + { value: 0, description: i18next.t("mode.original") }, + { value: 1, description: i18next.t("mode.lstm") }, + { value: 2, description: i18next.t("mode.combined") }, + { value: 3, description: i18next.t("mode.default") }, +]; export const defaultSegmentationMode = 3; -export const tesseractSegmentList = [ - //{ value: 0, description: "Apenas Orientation and Script Detection (OSD)"}, // TODO: allow producing only OSD file without OCR - { value: 1, description: "OCR com segmentação automática de página e OSD"}, - { value: 2, description: "Segmentação automática de página sem OSD nem OCR"}, - { value: 3, description: "(Por defeito) OCR com segmentação automática, sem OSD"}, - { value: 4, description: "Coluna de texto com linhas de tamanho variável"}, - { value: 5, description: "Bloco uniforme de texto, alinhado verticalmente"}, - { value: 6, description: "Bloco uniforme de texto"}, - { value: 7, description: "Imagem com apenas uma linha de texto"}, - { value: 8, description: "Imagem com apenas uma palavra"}, - { value: 9, description: "Imagem com apenas uma palavra num círculo"}, - { value: 10, description: "Imagem com apenas um caracter"}, - { value: 11, description: "Texto disperso; procurar o máximo de texto sem ordem particular"}, - { value: 12, description: "Texto disperso com OSD"}, - { value: 13, description: "Contornando truques específicos do Tesseract, tratar imagem como apenas uma linha de texto"}, -] +export const tesseractSegmentList = () => [ + { value: 1, description: i18next.t("segmentation mode.auto with osd") }, + { value: 2, description: i18next.t("segmentation mode.auto no osd") }, + { value: 3, description: i18next.t("segmentation mode.default") }, + { value: 4, description: i18next.t("segmentation mode.column variable lines") }, + { value: 5, description: i18next.t("segmentation mode.block vertical") }, + { value: 6, description: i18next.t("segmentation mode.block uniform") }, + { value: 7, description: i18next.t("segmentation mode.single line") }, + { value: 8, description: i18next.t("segmentation mode.single word") }, + { value: 9, description: i18next.t("segmentation mode.single circle word") }, + { value: 10, description: i18next.t("segmentation mode.single char") }, + { value: 11, description: i18next.t("segmentation mode.sparse text") }, + { value: 12, description: i18next.t("segmentation mode.sparse text osd") }, + { value: 13, description: i18next.t("segmentation mode.single line hack") }, +]; -export const defaultThresholding = 0; -export const tesseractThreshList = [ - { value: 0, description: "Otsu (por defeito)"}, - { value: 1, description: "LeptonicaOtsu"}, - { value: 2, description: "Sauvola"}, -] +export const defaultThresholding = 3; // Adaptive Gaussian (preprocessing default) +export const tesseractThreshList = () => [ + { value: 0, description: i18next.t("threshold.otsu") }, + { value: 1, description: i18next.t("threshold.leptonica") }, + { value: 2, description: i18next.t("threshold.sauvola") }, + { value: 3, description: i18next.t("threshold.adaptive_gaussian") }, + { value: -1, description: i18next.t("threshold.none") }, +]; export const defaultConfig = { lang: defaultLangs, @@ -69,7 +102,24 @@ export const defaultConfig = { engineMode: defaultEngineMode, segmentMode: defaultSegmentationMode, thresholdMethod: defaultThresholding, -} + compress: true, + preprocessing: { + enabled: true, + grayscale: true, + clahe: true, + clahe_clip_limit: 2.5, // Moderate contrast (was 3.0) + clahe_tile_size: 8, + median_blur: true, + median_blur_kernel: 9, // Much stronger blur to remove speckle noise (was 5) + threshold_method: "otsu", // OTSU works better for uniform backgrounds (was adaptive_gaussian) + adaptive_block_size: 21, + adaptive_c: 5, + morphological_opening: true, + morphological_closing: true, + morph_kernel_size: 7, // Larger kernel for aggressive cleanup (was 5) + deskew: true, + }, +}; export const emptyConfig = { lang: [], @@ -80,4 +130,21 @@ export const emptyConfig = { thresholdMethod: -1, dpiVal: null, otherParams: null, -} + compress: true, + preprocessing: { + enabled: true, + grayscale: true, + clahe: true, + clahe_clip_limit: 2.5, + clahe_tile_size: 8, + median_blur: true, + median_blur_kernel: 9, + threshold_method: "otsu", + adaptive_block_size: 21, + adaptive_c: 5, + morphological_opening: true, + morphological_closing: true, + morph_kernel_size: 7, + deskew: false, + }, +}; diff --git a/website/src/i18n.js b/website/src/i18n.js new file mode 100644 index 00000000..08142963 --- /dev/null +++ b/website/src/i18n.js @@ -0,0 +1,28 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import en from "./Languages/English/translation.json"; +import pt from "./Languages/Portuguese/translation.json"; +import ar from "./Languages/Arabic/translation.json"; +import zh from "./Languages/Chinese/translation.json"; +import fr from "./Languages/French/translation.json"; +import ru from "./Languages/Russian/translation.json"; +import es from "./Languages/Spanish/translation.json"; + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + pt: { translation: pt }, + ar: { translation: ar }, + zh: { translation: zh }, + fr: { translation: fr }, + ru: { translation: ru }, + es: { translation: es }, + }, + lng: "en", // default language + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; diff --git a/website/src/index.css b/website/src/index.css index ec2585e8..a46f60ba 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -1,3 +1,11 @@ +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @@ -5,9 +13,46 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +/* Custom scrollbar styling */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--gray-100); +} + +::-webkit-scrollbar-thumb { + background: var(--gray-400); + border-radius: var(--radius-full); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--gray-500); +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: var(--gray-400) var(--gray-100); +} + +/* Selection styling */ +::selection { + background-color: var(--accent-primary); + color: white; +} + +::-moz-selection { + background-color: var(--accent-primary); + color: white; +} diff --git a/website/src/static/Logo_of_the_United_Nations.svg b/website/src/static/Logo_of_the_United_Nations.svg new file mode 100644 index 00000000..a07a9882 --- /dev/null +++ b/website/src/static/Logo_of_the_United_Nations.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/src/static/logoApp.png b/website/src/static/logoApp.png new file mode 100644 index 00000000..8393a76a Binary files /dev/null and b/website/src/static/logoApp.png differ diff --git a/website/src/utils/keyboardShortcuts.js b/website/src/utils/keyboardShortcuts.js new file mode 100644 index 00000000..53147a74 --- /dev/null +++ b/website/src/utils/keyboardShortcuts.js @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; + +// Keyboard shortcuts manager +export const useKeyboardShortcuts = (shortcuts) => { + useEffect(() => { + const handleKeyDown = (event) => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const modifier = isMac ? event.metaKey : event.ctrlKey; + + // Check each shortcut + for (const shortcut of shortcuts) { + const { key, ctrl, shift, alt, callback, preventDefault = true } = shortcut; + + const modifierMatch = ctrl ? modifier : !modifier; + const shiftMatch = shift ? event.shiftKey : !event.shiftKey; + const altMatch = alt ? event.altKey : !event.altKey; + const keyMatch = event.key.toLowerCase() === key.toLowerCase(); + + if (modifierMatch && shiftMatch && altMatch && keyMatch) { + if (preventDefault) { + event.preventDefault(); + } + callback(event); + break; + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [shortcuts]); +}; + +// Format shortcut display text +export const formatShortcut = (shortcut) => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const parts = []; + + if (shortcut.ctrl) { + parts.push(isMac ? '⌘' : 'Ctrl'); + } + if (shortcut.shift) { + parts.push(isMac ? '⇧' : 'Shift'); + } + if (shortcut.alt) { + parts.push(isMac ? '⌥' : 'Alt'); + } + parts.push(shortcut.key.toUpperCase()); + + return parts.join(isMac ? '' : '+'); +}; + +// Default shortcuts configuration +export const SHORTCUTS = { + SEARCH: { key: 'k', ctrl: true, label: 'Search' }, + UPLOAD: { key: 'u', ctrl: true, label: 'Upload File' }, + NEW_FOLDER: { key: 'n', ctrl: true, label: 'New Folder' }, + DELETE: { key: 'Delete', label: 'Delete' }, + ESCAPE: { key: 'Escape', label: 'Close/Cancel' }, + SELECT_ALL: { key: 'a', ctrl: true, label: 'Select All' }, +}; + +