diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95ac607..e93deb9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,10 +19,14 @@ "axios": "^1.3.0", "date-fns": "^4.1.0", "eventemitter3": "^5.0.1", + "i18next": "^25.5.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.0", + "react-i18next": "^16.0.0", "react-router-dom": "^7.0.0", "uuid": "^13.0.0" }, @@ -101,7 +105,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -448,7 +451,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -472,7 +474,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -535,7 +536,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -579,7 +579,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1190,7 +1189,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.4", @@ -1301,7 +1299,6 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz", "integrity": "sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.3", @@ -1880,7 +1877,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1932,8 +1930,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai-subset": { "version": "1.3.6", @@ -1958,7 +1955,6 @@ "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -1980,7 +1976,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1991,7 +1986,6 @@ "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2136,6 +2130,7 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2265,7 +2260,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -2364,6 +2358,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2420,6 +2415,7 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2432,7 +2428,8 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2493,6 +2490,15 @@ "node": ">= 6" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -2539,7 +2545,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -2616,7 +2621,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -2984,6 +2990,7 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -3055,6 +3062,15 @@ "node": ">=18" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3083,6 +3099,55 @@ "node": ">= 14" } }, + "node_modules/i18next": { + "version": "25.5.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz", + "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3162,7 +3227,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -3285,6 +3349,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3374,6 +3439,48 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -3510,7 +3617,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3589,7 +3695,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3679,7 +3784,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3689,7 +3793,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3730,6 +3833,32 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz", + "integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", @@ -4127,6 +4256,7 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4270,7 +4400,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4353,7 +4483,6 @@ "integrity": "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5520,6 +5649,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e361b86..1ebe754 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,10 +30,14 @@ "axios": "^1.3.0", "date-fns": "^4.1.0", "eventemitter3": "^5.0.1", + "i18next": "^25.5.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.0", + "react-i18next": "^16.0.0", "react-router-dom": "^7.0.0", "uuid": "^13.0.0" }, diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json new file mode 100644 index 0000000..9c52c0c --- /dev/null +++ b/frontend/public/locales/en/translation.json @@ -0,0 +1,1494 @@ +{ + "common": { + "appName": "Readur", + "appTagline": "AI Document Platform", + "welcome": "Welcome to {{appName}}", + "welcomeBack": "Welcome back, {{username}}! 👋", + "or": "or", + "copyright": "© 2026 Readur. Powered by advanced OCR and AI technology.", + "actions": { + "save": "Save", + "cancel": "Cancel", + "close": "Close", + "delete": "Delete", + "edit": "Edit", + "view": "View", + "download": "Download", + "upload": "Upload", + "search": "Search", + "filter": "Filter", + "sort": "Sort", + "clear": "Clear", + "refresh": "Refresh", + "retry": "Retry", + "create": "Create", + "update": "Update", + "viewDetails": "View Details", + "back": "Back" + }, + "status": { + "loading": "Loading...", + "processing": "Processing...", + "completed": "Completed", + "failed": "Failed", + "pending": "Pending", + "success": "Success", + "error": "Error" + }, + "time": { + "seconds": "{{count}} seconds", + "minutes": "{{count}} minutes", + "hours": "{{count}} hours", + "days": "{{count}} days" + }, + "sizes": { + "bytes": "{{count}} Bytes", + "kb": "{{count}} KB", + "mb": "{{count}} MB", + "gb": "{{count}} GB" + }, + "moreCount": "+{{count}} more", + "of": "of", + "and": "and" + }, + "auth": { + "signIn": "Sign in", + "signingIn": "Signing in...", + "signInToAccount": "Sign in to your account", + "signInWithOIDC": "Sign in with OIDC", + "redirecting": "Redirecting...", + "username": "Username", + "password": "Password", + "usernameRequired": "Username is required", + "passwordRequired": "Password is required", + "logout": "Logout", + "profile": "Profile", + "intelligentDocumentPlatform": "Your intelligent document management platform", + "errors": { + "invalidCredentials": "Invalid username or password. Please check your credentials and try again.", + "accountDisabled": "Your account has been disabled. Please contact an administrator for assistance.", + "userNotFound": "No account found with this username. Please check your username or contact support.", + "sessionExpired": "Your session has expired. Please try logging in again.", + "networkError": "Network error. Please check your connection and try again.", + "serverError": "Server error. Please try again later or contact support if the problem persists.", + "oidcAuthFailed": "OIDC authentication failed. Please check with your administrator.", + "oidcNotConfigured": "OIDC is not configured on this server. Please use username/password login.", + "oidcInitFailed": "Failed to initiate OIDC login. Please try again.", + "loginFailed": "Failed to log in. Please check your credentials." + } + }, + "navigation": { + "dashboard": "Dashboard", + "upload": "Upload", + "documents": "Documents", + "search": "Search", + "labels": "Labels", + "sources": "Sources", + "watchFolder": "Watch Folder", + "documentManagement": "Document Management", + "ignoredFiles": "Ignored Files" + }, + "dashboard": { + "greeting": "Here's what's happening with your documents today.", + "stats": { + "totalDocuments": { + "title": "Total Documents", + "subtitle": "Files in your library", + "trend": "{{count}} total", + "trendEmpty": "No documents yet" + }, + "storageUsed": { + "title": "Storage Used", + "subtitle": "Total file size", + "trend": "{{size}} used", + "trendEmpty": "No storage used" + }, + "ocrProcessed": { + "title": "OCR Processed", + "subtitle": "Text extracted documents", + "trend": "{{percentage}}% completion", + "trendEmpty": "0% completion" + }, + "searchable": { + "title": "Searchable", + "subtitle": "Ready for search", + "trend": "{{count}} indexed", + "trendEmpty": "Nothing indexed yet" + } + }, + "recentDocuments": { + "title": "Recent Documents", + "viewAll": "View All", + "noDocuments": "No documents yet", + "uploadFirst": "Upload your first document to get started" + }, + "quickActions": { + "title": "Quick Actions", + "upload": { + "title": "Upload Documents", + "description": "Add new files for OCR processing" + }, + "search": { + "title": "Search Library", + "description": "Find documents by content or metadata" + }, + "browse": { + "title": "Browse Documents", + "description": "View and manage your document library" + } + } + }, + "search": { + "title": "Search Documents", + "placeholder": "Search documents by content, filename, or tags... Try 'invoice', 'contract', or tag:important", + "searchPlaceholder": "Search documents...", + "noResults": { + "title": "No results found for \"{{query}}\"", + "subtitle": "Try adjusting your search terms or filters", + "suggestions": { + "title": "Suggestions:", + "simpler": "Try simpler or more general terms", + "spelling": "Check spelling and try different keywords", + "removeFilters": "Remove some filters to broaden your search", + "useQuotes": "Use quotes for exact phrases" + } + }, + "searching": "Searching...", + "searchingAsYouType": "Searching as you type...", + "quickResults": "Quick Results", + "resultsCount": "{{count}} found", + "viewAllResults": "View all results for \"{{query}}\"", + "recentSearches": "Recent Searches", + "startTyping": "Start typing to search documents", + "popularSearches": "Popular searches:", + "noDocumentsFound": "No documents found for \"{{query}}\"", + "pressEnterAdvanced": "Press Enter to search with advanced options", + "trySuggestions": "Try these suggestions:", + "settings": { + "title": "Search Settings" + }, + "modes": { + "smart": "Smart", + "exactPhrase": "Exact phrase", + "similarWords": "Similar words", + "advanced": "Advanced", + "enhanced": "Enhanced" + }, + "quickSuggestions": { + "title": "Quick suggestions:" + }, + "relatedSearches": { + "title": "Related searches:" + }, + "filters": { + "title": "Filters", + "tags": "Tags", + "selectTags": "Select Tags", + "ocrStatus": "OCR Status", + "ocrText": "OCR Text", + "allDocuments": "All Documents", + "hasOcrText": "Has OCR Text", + "noOcrText": "No OCR Text", + "dateRange": "Date Range", + "daysAgo": "Days ago: {{min}} - {{max}}", + "dateMarks": { + "today": "Today", + "30d": "30d", + "90d": "90d", + "1y": "1y" + }, + "fileSize": "File Size", + "sizeRange": "Size: {{min}}MB - {{max}}MB" + }, + "status": { + "searching": "Searching...", + "resultsFound": "{{count}} results found" + }, + "display": { + "settings": "Display Settings", + "textSettings": "Text Display Settings", + "fontSizeLabel": "Font Size: {{size}}px", + "snippetsPerResult": "Snippets per result: {{count}}", + "contextLength": "Context Length: {{length}} characters", + "viewMode": { + "label": "View Mode", + "compact": "Compact", + "detailed": "Detailed", + "contextFocus": "Context Focus" + }, + "highlightStyle": { + "label": "Highlight Style", + "background": "Background Color", + "underline": "Underline", + "bold": "Bold Text" + } + }, + "results": { + "showing": "Showing:", + "snippetsCount": "{{count}} snippets", + "fontSize": "{{size}}px font", + "hasOcr": " • OCR", + "tags": "Tags:", + "pagination": "Showing {{start}}-{{end}} of {{total}} results" + }, + "empty": { + "title": "Start searching your documents", + "subtitle": "Use the enhanced search bar above to find documents by content, filename, or tags" + }, + "tips": { + "title": "Search Tips:", + "exactPhrase": "Use quotes for exact phrases: \"project plan\"", + "tags": "Search by tags: tag:important or tag:invoice", + "combine": "Combine terms: contract AND payment", + "wildcards": "Use wildcards: proj* for project, projects, etc." + }, + "examples": { + "invoice": "Try: invoice", + "contract": "Try: contract", + "tagImportant": "Try: tag:important" + }, + "actions": { + "clearFilters": "Clear Filters", + "newSearch": "New Search" + } + }, + "upload": { + "title": "Upload Documents", + "subtitle": "Transform your documents with intelligent OCR processing", + "features": { + "aiOcr": { + "title": "AI-Powered OCR", + "description": "Advanced text extraction from any document type" + }, + "fullTextSearch": { + "title": "Full-Text Search", + "description": "Find documents instantly by content or metadata" + }, + "lightningFast": { + "title": "Lightning Fast", + "description": "Process documents in seconds, not minutes" + }, + "secure": { + "title": "Secure & Private", + "description": "Your documents are encrypted and protected" + }, + "multiLanguage": { + "title": "Multi-Language", + "description": "Support for 100+ languages and scripts" + } + }, + "tips": { + "title": "📋 Upload Tips", + "highRes": "• For best OCR results, use high-resolution images", + "pdfText": "• PDF files with text layers are processed faster", + "clarity": "• Ensure documents are well-lit and clearly readable", + "maxSize": "• Maximum file size is 50MB per document" + }, + "dropzone": { + "dragDrop": "Drag & drop files here", + "dropHere": "Drop files here", + "browse": "or click to browse your computer", + "chooseFiles": "Choose Files", + "maxFileSize": "Maximum file size: 50MB per file", + "fileTypes": { + "pdf": "PDF", + "images": "Images", + "text": "Text", + "word": "Word" + } + }, + "languageSettings": { + "title": "🌐 OCR Language Settings", + "description": "Select languages for optimal OCR text recognition" + }, + "labelAssignment": { + "title": "📋 Label Assignment", + "description": "Select labels to automatically assign to all uploaded documents", + "placeholder": "Choose labels for your documents...", + "helperText": "These labels will be applied to all uploaded documents" + }, + "fileList": { + "title": "Files ({{count}})", + "clearCompleted": "Clear Completed", + "uploading": "Uploading... ({{completed}}/{{total}})", + "uploadingSimple": "Uploading...", + "uploadAll": "Upload All" + }, + "errors": { + "sessionExpired": "Your session has expired. Please refresh the page and log in again.", + "labelPermissionDenied": "You do not have permission to access labels.", + "labelNetworkError": "Network error loading labels. Please check your connection.", + "fileTooLarge": "File is too large. Maximum size is 50MB.", + "unsupportedFormat": "Unsupported file format. Please use PDF, images, text, or Word documents.", + "processingFailed": "Failed to process document. Please try again or contact support.", + "permissionDenied": "You do not have permission to upload documents.", + "networkError": "Network error. Please check your connection and try again.", + "serverError": "Server error. Please try again later." + } + }, + "documents": { + "title": "Documents", + "subtitle": "Manage and explore your document library", + "viewDocument": "View document", + "downloadDocument": "Download document", + "deleteDocument": "Delete document", + "unknownDocument": "Unknown Document", + "search": { + "placeholder": "Search documents..." + }, + "selection": { + "select": "Select", + "cancel": "Cancel", + "count": "{{count}} of {{total}} documents selected", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "deleteSelected": "Delete Selected ({{count}})" + }, + "filters": { + "ocrStatus": "OCR Status", + "all": "All", + "completed": "Completed", + "processing": "Processing", + "failed": "Failed", + "pending": "Pending" + }, + "sort": { + "label": "Sort", + "newestFirst": "Newest First", + "oldestFirst": "Oldest First", + "nameAZ": "Name A-Z", + "nameZA": "Name Z-A", + "largestFirst": "Largest First", + "smallestFirst": "Smallest First" + }, + "ocrStatus": { + "confidence": "OCR {{percent}}%", + "done": "OCR Done", + "processing": "Processing...", + "failed": "OCR Failed", + "pending": "Pending" + }, + "actions": { + "editLabels": "Edit Labels", + "retryOcr": "Retry OCR", + "retryingOcr": "Retrying OCR...", + "retryHistory": "Retry History" + }, + "empty": { + "title": "No documents found", + "searchSubtitle": "Try adjusting your search terms", + "uploadSubtitle": "Upload your first document to get started" + }, + "dialogs": { + "editLabels": { + "title": "Edit Document Labels", + "placeholder": "Select labels for this document..." + }, + "delete": { + "title": "Delete Document", + "message": "Are you sure you want to delete \"{{filename}}\"?", + "warning": "This action cannot be undone. The document file and all associated data will be permanently removed.", + "deleting": "Deleting...", + "delete": "Delete" + }, + "bulkDelete": { + "title": "Delete Multiple Documents", + "message": "Are you sure you want to delete {{count}} selected document{{plural}}?", + "warning": "This action cannot be undone. All selected documents and their associated data will be permanently removed.", + "listTitle": "Documents to be deleted:", + "moreCount": "... and {{count}} more", + "deleteButton": "Delete {{count}} Document{{plural}}" + } + }, + "pagination": { + "showing": "Showing {{start}}-{{end}} of {{total}} documents", + "withOcrStatus": " with OCR status: {{status}}", + "matching": " matching \"{{query}}\"" + } + }, + "documentDetails": { + "subtitle": "Comprehensive document analysis and metadata viewer", + "actions": { + "backToDocuments": "Back to Documents", + "download": "Download", + "viewDocument": "View Document", + "viewOcrText": "View OCR Text", + "viewProcessedImage": "View Processed Image", + "retryOcr": "Retry OCR", + "retryHistory": "Retry History", + "editLabels": "Edit Labels", + "deleteDocument": "Delete Document" + }, + "errors": { + "notFound": "Document not found" + }, + "metadata": { + "fileSize": "File Size", + "uploadDate": "Upload Date", + "sourceType": "Source Type", + "originalPath": "Original Path", + "originalCreated": "Original Created", + "originalModified": "Original Modified", + "ocrStatus": "OCR Status", + "textExtracted": "Text Extracted" + }, + "ocr": { + "title": "🔍 Extracted Text (OCR)", + "expandTooltip": "Expand to view full text with search", + "expand": "Expand", + "loading": "Loading OCR analysis...", + "confidence": "Confidence", + "words": "Words", + "processingTime": "Processing Time", + "error": "OCR Processing Error", + "noText": "No OCR text available for this document.", + "completed": "✅ Processing completed: {{date}}", + "loadFailed": "OCR text is available but failed to load. Please try refreshing the page." + }, + "tagsLabels": { + "title": "🏷️ Tags & Labels", + "tags": "Tags", + "labels": "Labels", + "noLabels": "No labels assigned to this document" + }, + "dialogs": { + "ocrText": { + "title": "Extracted Text (OCR)", + "confidence": "{{percent}}% confidence", + "words": "{{count}} words", + "loading": "Loading OCR text...", + "error": "OCR Error: {{message}}", + "noText": "No OCR text available for this document.", + "processingTime": "Processing time: {{time}}ms", + "completed": "Completed: {{date}}" + }, + "ocrExpanded": { + "title": "🔍 Extracted Text (OCR) - Full View", + "searchPlaceholder": "Search within extracted text...", + "matches": "{{count}} match{{plural}} found", + "noMatches": "No matches found", + "loading": "Loading OCR text...", + "error": "OCR Error: {{message}}", + "noText": "No OCR text available for this document." + }, + "processedImage": { + "title": "Processed Image - OCR Enhancement Applied", + "description": "This is the enhanced image that was actually processed by the OCR engine. You can adjust OCR enhancement settings in the Settings page.", + "noImage": "No processed image available" + }, + "editLabels": { + "title": "Edit Document Labels", + "description": "Select labels to assign to this document", + "placeholder": "Choose labels for this document...", + "saveLabels": "Save Labels" + }, + "delete": { + "title": "Delete Document", + "warning": "This action cannot be undone.", + "message": "Are you sure you want to delete {{filename}}?", + "details": "This will permanently remove the document and all associated data including OCR text, labels, and processing history.", + "deleting": "Deleting...", + "delete": "Delete Document" + } + } + }, + "ignoredFiles": { + "title": "Ignored Files", + "subtitle": "View and manage files that were intentionally ignored during processing", + "filters": { + "searchPlaceholder": "Search by filename or path...", + "reason": "Reason", + "allReasons": "All Reasons", + "duplicateHash": "Duplicate Hash", + "tooLarge": "Too Large", + "unsupportedFormat": "Unsupported Format", + "excluded": "Excluded", + "permissionDenied": "Permission Denied", + "corrupted": "Corrupted", + "other": "Other" + }, + "table": { + "filename": "Filename", + "path": "Path", + "reason": "Reason", + "size": "Size", + "ignoredAt": "Ignored At" + }, + "empty": { + "title": "No Ignored Files", + "subtitle": "No files have been ignored. All processed files were successfully handled." + }, + "noResults": { + "title": "No Results Found", + "subtitle": "No ignored files match your current filters. Try adjusting your search or filter criteria." + }, + "pagination": { + "showing": "Showing {{start}}-{{end}} of {{total}} ignored files" + }, + "errors": { + "loadFailed": "Failed to load ignored files", + "tryAgain": "Please try again later" + }, + "reasons": { + "duplicate_hash": "Duplicate Hash - File already exists in the system", + "file_too_large": "File Too Large - Exceeds maximum size limit", + "unsupported_format": "Unsupported Format - File type not supported", + "excluded_by_pattern": "Excluded - File matches exclusion pattern", + "permission_denied": "Permission Denied - Cannot access file", + "file_corrupted": "File Corrupted - Cannot read or process file", + "unknown": "Unknown - Reason not specified" + } + }, + "documentManagement": { + "title": "Document Management", + "retryAll": "Retry All Documents", + "retrying": "Retrying All...", + "retryFailedOnly": "Retry Failed Only", + "tabs": { + "failedDocuments": "Failed Documents{{showCount, select, true { ({{count}}) } other {}}", + "failedDocumentsTooltip": "View and manage documents that failed during processing (OCR, ingestion, validation, etc.)", + "cleanup": "Document Cleanup{{showCount, select, true { ({{count}}) } other {}}", + "cleanupTooltip": "Manage and clean up documents with quality issues - low OCR confidence or failed processing", + "duplicates": "Duplicate Files{{showCount, select, true { ({{count}}) } other {}}", + "duplicatesTooltip": "View and manage duplicate document groups - documents with identical content", + "ignoredFiles": "Ignored Files{{showCount, select, true { ({{count}}) } other {}}", + "ignoredFilesTooltip": "Manage files that have been ignored during sync operations" + }, + "stats": { + "totalFailed": "Total Failed", + "failureCategories": "Failure Categories", + "noFailureData": "No failure data available" + }, + "advancedRetry": { + "title": "Advanced Retry Options", + "button": "Advanced Retry", + "description": "Use advanced filtering and selection options to retry specific subsets of failed documents based on file type, failure reason, size, and more." + }, + "filters": { + "title": "Filter Options", + "stage": "Filter by Stage", + "reason": "Filter by Reason", + "allStages": "All Stages", + "allReasons": "All Reasons", + "clearFilters": "Clear Filters", + "stages": { + "ocr": "OCR Processing", + "ingestion": "Document Ingestion", + "validation": "Validation", + "storage": "File Storage", + "processing": "Processing", + "sync": "Synchronization" + }, + "reasons": { + "duplicateContent": "Duplicate Content", + "lowConfidence": "Low OCR Confidence", + "unsupportedFormat": "Unsupported Format", + "fileTooLarge": "File Too Large", + "fileCorrupted": "File Corrupted", + "ocrTimeout": "OCR Timeout", + "pdfParsingError": "PDF Parsing Error", + "other": "Other" + } + }, + "alerts": { + "noFailedTitle": "Great news!", + "noFailedMessage": "No documents have failed OCR processing. All your documents are processing successfully.", + "overviewTitle": "Failed Documents Overview", + "overviewMessage": "These documents failed at various stages of processing: ingestion, validation, OCR, storage, etc. Use the filters above to narrow down by failure stage or specific reason. You can retry processing for recoverable failures." + }, + "table": { + "document": "Document", + "failureType": "Failure Type", + "retryCount": "Retry Count", + "lastFailed": "Last Failed", + "actions": "Actions", + "attempts": "{{count}} attempts", + "unknown": "Unknown" + }, + "actions": { + "retryOcr": "Retry OCR", + "viewDetails": "View Details", + "retryHistory": "Retry History", + "download": "Download Document" + }, + "details": { + "errorDetails": "Error Details", + "failureReason": "Failure Reason", + "notSpecified": "Not specified", + "ocrResults": "OCR Results", + "confidencePercent": "{{percent}}% confidence", + "wordsFound": "{{count}} words found", + "errorMessage": "Error Message", + "noErrorMessage": "No error message available", + "lastAttempt": "Last Attempt", + "noPreviousAttempts": "No previous attempts", + "fileCreated": "File Created" + }, + "retry": { + "queuedSuccess": "OCR retry queued for \"{{filename}}\". Estimated wait time: {{minutes}} minutes.", + "unknown": "Unknown", + "failed": "Failed to retry OCR", + "processingFailed": "Failed to retry OCR processing", + "bulkSuccess": "Successfully queued {{count}} documents for OCR retry. Estimated processing time: {{minutes}} minutes.", + "noDocuments": "No documents found to retry", + "bulkFailed": "Failed to retry documents. Please try again.", + "requeuedSuccess": "Successfully queued {{count}} failed documents for OCR retry. Check the queue stats for progress.", + "noFailedDocuments": "No failed documents found to retry", + "requeuedFailed": "Failed to retry all failed OCR documents", + "advancedSuccess": "Successfully queued {{queued}} of {{matched}} documents for retry. Estimated processing time: {{minutes}} minutes." + }, + "cleanup": { + "previewFailed": "Failed to preview low confidence documents", + "noDocuments": "No documents to delete", + "deleteFailed": "Failed to delete low confidence documents", + "previewFailedDocs": "Failed to preview failed documents", + "deleteFailedDocs": "Failed to delete failed documents" + }, + "ignoredFiles": { + "removedSuccess": "Files removed from ignored list", + "deleteFailed": "Failed to delete ignored files", + "fileRemovedSuccess": "File removed from ignored list", + "fileDeleteFailed": "Failed to delete ignored file" + }, + "errors": { + "loadFailedDocuments": "Failed to load failed documents", + "sessionExpired": "Your session has expired. Please refresh the page and log in again.", + "permissionDenied": "You do not have permission to view failed documents.", + "noFailedDocumentsFound": "No failed documents found or they may have been processed.", + "networkError": "Network error. Please check your connection and try again.", + "serverError": "Server error. Please try again later.", + "loadDuplicates": "Failed to load duplicate documents", + "permissionDeniedDuplicates": "You do not have permission to view duplicate documents.", + "documentNotFound": "Document not found. It may have been deleted or processed already.", + "cannotRetry": "Document cannot be retried due to processing issues. Please check the document format.", + "permissionDeniedRetry": "You do not have permission to retry OCR processing.", + "serverErrorSupport": "Server error. Please try again later or contact support.", + "loadIgnoredFiles": "Failed to load ignored files", + "permissionDeniedIgnored": "You do not have permission to view ignored files." + } + }, + "watchFolder": { + "title": "Watch Folder", + "refreshAll": "Refresh All", + "retryFailedJobs": "Retry {{count}} Failed Jobs", + "requeuing": "Requeuing...", + "personalWatchDirectory": "Personal Watch Directory", + "admin": "Admin", + "directoryStatus": "Directory Status", + "directoryExists": "Directory Exists", + "directoryMissing": "Directory Missing", + "watchStatus": "Watch Status", + "enabled": "Enabled", + "disabled": "Disabled", + "yourPersonalWatchDirectory": "Your Personal Watch Directory", + "directoryNotExist": "Your personal watch directory doesn't exist yet. Create it to start uploading files to your own dedicated folder.", + "creatingDirectory": "Creating Directory...", + "createPersonalDirectory": "Create Personal Directory", + "unableToLoad": "Unable to load personal watch directory information. Please try refreshing the page.", + "systemConfiguration": "System Configuration", + "globalWatchFolderConfiguration": "Global Watch Folder Configuration", + "adminOnly": "Admin Only", + "systemWideInfo": "This is the system-wide watch folder configuration. All users can view this information.", + "watchedDirectory": "Watched Directory", + "status": "Status", + "active": "Active", + "inactive": "Inactive", + "watchStrategy": "Watch Strategy", + "scanInterval": "Scan Interval", + "seconds": "{{count}} seconds", + "maxFileAge": "Max File Age", + "hours": "{{count}} hours", + "supportedFileTypes": "Supported File Types", + "processingQueue": "Processing Queue", + "pending": "Pending", + "processing": "Processing", + "failed": "Failed", + "completedToday": "Completed Today", + "averageWaitTime": "Average Wait Time", + "oldestPendingItem": "Oldest Pending Item", + "lastUpdated": "Last updated: {{time}}", + "howWatchFolderWorks": "How Watch Folder Works", + "watchFolderDescription": "The watch folder system automatically monitors the configured directory for new files and processes them for OCR.", + "processingPipeline": "Processing Pipeline:", + "pipelineSteps": { + "fileDetection": "File Detection: New files are detected using hybrid watching (inotify + polling)", + "validation": "Validation: Files are checked for supported format and size limits", + "deduplication": "Deduplication: System prevents processing of duplicate files", + "storage": "Storage: Files are moved to the document storage system", + "ocrQueue": "OCR Queue: Documents are queued for OCR processing with priority" + }, + "hybridStrategyInfo": "The system uses a hybrid watching strategy that automatically detects filesystem type and chooses the optimal monitoring approach (inotify for local filesystems, polling for network mounts)." + }, + "settings": { + "title": "Settings", + "apiDocumentation": "API Documentation", + "debug": "Debug", + "language": "Language", + "selectLanguage": "Select language", + "tabs": { + "general": "General", + "ocrSettings": "OCR Settings", + "userManagement": "User Management", + "serverConfiguration": "Server Configuration" + }, + "general": { + "title": "General Settings", + "ocrConfiguration": { + "title": "OCR Configuration", + "description": "Configure languages for OCR text extraction. Multiple languages help with mixed-language documents.", + "autoDetectLanguageCombination": "Auto-detect language combinations", + "autoDetectLanguageCombinationHelper": "Automatically suggest optimal language combinations based on document content analysis", + "concurrentOcrJobs": "Concurrent OCR Jobs", + "concurrentOcrJobsHelper": "Number of OCR jobs that can run simultaneously", + "ocrTimeout": "OCR Timeout (seconds)", + "ocrTimeoutHelper": "Maximum time for OCR processing per file", + "cpuPriority": "CPU Priority", + "cpuPriorityLow": "Low", + "cpuPriorityNormal": "Normal", + "cpuPriorityHigh": "High" + }, + "ocrControls": { + "title": "OCR Processing Controls (Admin Only)", + "description": "Control OCR processing to manage CPU usage and allow users to use the application without performance impact.", + "pauseOcr": "Pause OCR Processing", + "resumeOcr": "Resume OCR Processing", + "ocrStatusLabel": "OCR Status: {{status}}", + "ocrPausedMessage": "OCR processing is paused. No new jobs will be processed.", + "ocrActiveMessage": "OCR processing is active. Documents will be processed automatically.", + "pausedAlertTitle": "OCR Processing Paused", + "pausedAlertMessage": "New documents will not be processed for OCR text extraction until processing is resumed. Users can still upload and view documents, but search functionality may be limited." + }, + "fileProcessing": { + "title": "File Processing", + "maxFileSize": "Max File Size (MB)", + "maxFileSizeHelper": "Maximum allowed file size for uploads", + "memoryLimit": "Memory Limit (MB)", + "memoryLimitHelper": "Memory limit per OCR job", + "autoRotateImages": "Auto-rotate Images", + "autoRotateImagesHelper": "Automatically detect and correct image orientation", + "enableImagePreprocessing": "Enable Image Preprocessing", + "enableImagePreprocessingHelper": "Enhance images for better OCR accuracy (deskew, denoise, contrast)", + "preprocessingWarning": "⚠️ Warning: Enabling preprocessing can significantly alter OCR text results and may reduce accuracy for some documents", + "enableBackgroundOcr": "Enable Background OCR", + "enableBackgroundOcrHelper": "Process OCR in the background after file upload" + }, + "searchConfiguration": { + "title": "Search Configuration", + "resultsPerPage": "Results Per Page", + "snippetLength": "Snippet Length", + "snippetLengthHelper": "Characters to show in search result previews", + "fuzzySearchThreshold": "Fuzzy Search Threshold", + "fuzzySearchThresholdHelper": "Tolerance for spelling mistakes (0.0-1.0)" + }, + "storageManagement": { + "title": "Storage Management", + "retentionDays": "Retention Days", + "retentionDaysHelper": "Auto-delete documents after X days (leave empty to disable)", + "enableAutoCleanup": "Enable Auto Cleanup", + "enableAutoCleanupHelper": "Automatically remove orphaned files and clean up storage", + "enableCompression": "Enable Compression", + "enableCompressionHelper": "Compress stored documents to save disk space" + } + }, + "ocrSettings": { + "title": "OCR Image Processing Settings", + "enhancementControls": { + "title": "Enhancement Controls", + "skipEnhancement": "Skip All Image Enhancement (Use Original Images Only)", + "brightnessBoost": "Brightness Boost", + "brightnessBoostHelper": "Manual brightness adjustment (0 = auto, >0 = boost amount)", + "contrastMultiplier": "Contrast Multiplier", + "contrastMultiplierHelper": "Manual contrast adjustment (1.0 = auto, >1.0 = increase)", + "noiseReductionLevel": "Noise Reduction Level", + "noiseReductionNone": "None", + "noiseReductionLight": "Light", + "noiseReductionModerate": "Moderate", + "noiseReductionHeavy": "Heavy", + "sharpeningStrength": "Sharpening Strength", + "sharpeningStrengthHelper": "Image sharpening amount (0 = auto, >0 = manual)" + }, + "qualityThresholds": { + "title": "Quality Thresholds (when to apply enhancements)", + "brightnessThreshold": "Brightness Threshold", + "brightnessThresholdHelper": "Enhance if brightness below this value (0-255)", + "contrastThreshold": "Contrast Threshold", + "contrastThresholdHelper": "Enhance if contrast below this value (0-1)", + "noiseThreshold": "Noise Threshold", + "noiseThresholdHelper": "Enhance if noise above this value (0-1)", + "sharpnessThreshold": "Sharpness Threshold", + "sharpnessThresholdHelper": "Enhance if sharpness below this value (0-1)" + }, + "advancedProcessing": { + "title": "Advanced Processing Options", + "morphologicalOperations": "Morphological Operations (text cleanup)", + "histogramEqualization": "Histogram Equalization", + "saveProcessedImages": "Save Processed Images for Review", + "adaptiveThresholdWindowSize": "Adaptive Threshold Window Size", + "adaptiveThresholdWindowSizeHelper": "Window size for contrast enhancement (odd number)" + }, + "imageSizeScaling": { + "title": "Image Size and Scaling", + "maxImageWidth": "Max Image Width", + "maxImageWidthHelper": "Maximum image width in pixels", + "maxImageHeight": "Max Image Height", + "maxImageHeightHelper": "Maximum image height in pixels", + "upscaleFactor": "Upscale Factor", + "upscaleFactorHelper": "Image scaling factor (1.0 = no scaling)" + } + }, + "userManagement": { + "title": "User Management", + "addUser": "Add User", + "tableHeaders": { + "username": "Username", + "email": "Email", + "createdAt": "Created At", + "watchDirectory": "Watch Directory", + "actions": "Actions" + }, + "watchDirectory": { + "statusActive": "Active", + "statusDisabled": "Disabled", + "statusNotCreated": "Not Created", + "statusUnknown": "Unknown", + "loading": "Loading...", + "createDirectory": "Create watch directory", + "viewDirectory": "View watch directory", + "removeDirectory": "Remove watch directory (Admin only)", + "editUser": "Edit user", + "deleteUser": "Delete user" + }, + "dialogs": { + "createUser": "Create New User", + "editUser": "Edit User", + "username": "Username", + "email": "Email", + "password": "Password", + "newPassword": "New Password (leave empty to keep current)" + }, + "confirmRemoveDirectory": { + "title": "Remove Watch Directory", + "message": "Are you sure you want to remove the watch directory for user \"{{username}}\"? This action cannot be undone and will stop monitoring their directory for new files.", + "removeButton": "Remove Directory" + } + }, + "serverConfiguration": { + "title": "Server Configuration (Admin Only)", + "fileUpload": { + "title": "File Upload Configuration", + "maxFileSize": "Max File Size", + "uploadPath": "Upload Path", + "allowedFileTypes": "Allowed File Types", + "watchFolder": "Watch Folder" + }, + "ocrProcessing": { + "title": "OCR Processing Configuration", + "concurrentOcrJobs": "Concurrent OCR Jobs", + "ocrTimeout": "OCR Timeout", + "memoryLimit": "Memory Limit", + "ocrLanguage": "OCR Language", + "cpuPriority": "CPU Priority", + "backgroundOcr": "Background OCR", + "enabled": "Enabled", + "disabled": "Disabled" + }, + "serverInformation": { + "title": "Server Information", + "serverHost": "Server Host", + "serverPort": "Server Port", + "jwtSecret": "JWT Secret", + "configured": "Configured", + "notSet": "Not Set", + "version": "Version", + "buildInformation": "Build Information" + }, + "watchFolderConfiguration": { + "title": "Watch Folder Configuration", + "watchInterval": "Watch Interval", + "fileStabilityCheck": "File Stability Check", + "maxFileAge": "Max File Age" + }, + "refreshConfiguration": "Refresh Configuration", + "loadFailed": "Failed to load server configuration. Admin access may be required." + }, + "messages": { + "settingsUpdated": "Settings updated successfully", + "settingsUpdateFailed": "Failed to update settings", + "invalidLanguage": "Invalid language selected. Please choose from available languages.", + "valueOutOfRange": "{{message}}. {{suggestedAction}}", + "conflictingSettings": "Conflicting settings detected. Please review your configuration.", + "userCreated": "User created successfully", + "userUpdated": "User updated successfully", + "userDeleted": "User deleted successfully", + "cannotDeleteSelf": "You cannot delete your own account", + "confirmDeleteUser": "Are you sure you want to delete this user?", + "duplicateUsername": "This username is already taken. Please choose a different username.", + "duplicateEmail": "This email address is already in use. Please use a different email.", + "invalidPassword": "Password must be at least 8 characters with uppercase, lowercase, and numbers.", + "invalidEmail": "Please enter a valid email address.", + "invalidUsername": "Username contains invalid characters. Please use only letters, numbers, and underscores.", + "permissionDenied": "You do not have permission to perform this action.", + "cannotDeleteUser": "Cannot delete this user: They may have associated data or be the last admin.", + "userNotFound": "User not found. They may have already been deleted.", + "watchDirectoryCreated": "Watch directory created successfully", + "watchDirectoryCreatedFailed": "Failed to create watch directory", + "watchDirectoryAlreadyExists": "Watch directory already exists for this user", + "watchDirectoryPath": "Watch directory: {{path}}", + "watchDirectoryRemoved": "Watch directory removed successfully", + "watchDirectoryRemoveFailed": "Failed to remove watch directory", + "watchDirectoryNotFound": "Watch directory not found or already removed", + "ocrPaused": "OCR processing paused successfully", + "ocrPauseFailed": "Admin access required to pause OCR processing", + "ocrPauseFailedGeneric": "Failed to pause OCR processing", + "ocrResumed": "OCR processing resumed successfully", + "ocrResumeFailed": "Admin access required to resume OCR processing", + "ocrResumeFailedGeneric": "Failed to resume OCR processing", + "serverConfigLoadFailed": "Admin access required to view server configuration", + "serverConfigLoadFailedGeneric": "Failed to load server configuration" + } + }, + "labels": { + "title": "Label Management", + "loading": "Loading labels...", + "search": { + "placeholder": "Search labels..." + }, + "filters": { + "systemLabels": "System Labels" + }, + "sections": { + "systemLabels": "System Labels", + "myLabels": "My Labels" + }, + "badge": { + "system": "System" + }, + "stats": { + "documents": "Documents: {{count}}", + "sources": "Sources: {{count}}" + }, + "actions": { + "createLabel": "Create Label", + "editLabel": "Edit label", + "deleteLabel": "Delete label" + }, + "create": { + "title": "Create New Label", + "editTitle": "Edit Label", + "nameLabel": "Label Name", + "nameRequired": "Name is required", + "descriptionLabel": "Description (optional)", + "colorLabel": "Color", + "customColorLabel": "Custom Color (hex)", + "iconLabel": "Icon (optional)", + "iconNone": "None", + "previewLabel": "Preview", + "cancel": "Cancel", + "create": "Create", + "update": "Update", + "saving": "Saving..." + }, + "selector": { + "placeholder": "Search or create labels...", + "systemLabels": "System Labels", + "myLabels": "My Labels", + "createLabel": "Create label \"{{name}}\"", + "noLabelsFound": "No labels found", + "noLabelsMatch": "No labels match \"{{query}}\"", + "noLabelsAvailable": "No labels available" + }, + "empty": { + "title": "No labels found", + "noMatch": "No labels match \"{{query}}\"", + "noLabels": "You haven't created any labels yet", + "createFirst": "Create Your First Label" + }, + "dialogs": { + "delete": { + "title": "Delete Label", + "message": "Are you sure you want to delete the label \"{{name}}\"?", + "inUseWarning": " This label is currently used by {{count}} document(s)." + } + }, + "errors": { + "sessionExpired": "Your session has expired. Please log in again.", + "permissionDenied": "You do not have permission to view labels.", + "serverError": "Server error. Please try again later.", + "networkError": "Network error. Please check your connection and try again.", + "loadFailed": "Failed to load labels. Please check your connection.", + "notFound": "Label not found. It may have been deleted by another user.", + "duplicateName": "A label with this name already exists. Please choose a different name.", + "systemModification": "System labels cannot be modified. Only user-created labels can be edited.", + "alreadyDeleted": "Label not found. It may have already been deleted.", + "inUse": "Cannot delete label because it is currently assigned to documents. Please remove the label from all documents first.", + "systemDelete": "System labels cannot be deleted. Only user-created labels can be removed.", + "invalidName": "Label name contains invalid characters. Please use only letters, numbers, and basic punctuation.", + "invalidColor": "Invalid color format. Please use a valid hex color like #0969da.", + "maxLabelsReached": "Maximum number of labels reached. Please delete some labels before creating new ones." + } + }, + "notifications": { + "title": "Notifications", + "markAllAsRead": "Mark all as read", + "clearAll": "Clear all", + "noNotifications": "No notifications" + }, + "ocr": { + "languageSelector": { + "label": "OCR Language", + "loading": "Loading languages...", + "error": "Failed to load OCR languages", + "retry": "Retry", + "fallback": "English (Fallback)", + "current": "Current", + "languagesAvailable": "{{count}} language{{plural}} available", + "selectingWillUpdate": "Selecting \"{{language}}\" will update your default language" + } + }, + "register": { + "title": "Create your Readur account", + "fields": { + "username": "Username", + "email": "Email", + "password": "Password" + }, + "placeholders": { + "username": "Username", + "email": "Email", + "password": "Password" + }, + "actions": { + "signup": "Sign up", + "creating": "Creating account..." + }, + "links": { + "signin": "Already have an account? Sign in" + }, + "errors": { + "failed": "Failed to register" + } + }, + "debug": { + "title": "Document Processing Debug", + "subtitle": "Upload documents or analyze existing ones to troubleshoot OCR processing issues", + "errors": { + "enterDocumentId": "Please enter a document ID", + "documentNotFound": "Document {{documentId}} not found. It may still be processing or may have been moved to failed documents.", + "fetchFailed": "Failed to fetch debug information: {{message}}", + "debugError": "Debug Error" + }, + "upload": { + "title": "Upload Document for Debug Analysis", + "description": "Upload a PDF or image file to analyze the processing pipeline in real-time", + "selectFile": "Please select a file to upload", + "uploading": "Uploading file...", + "uploadedStartingOcr": "Document uploaded successfully. Starting OCR processing...", + "uploadFailed": "Failed to upload document", + "uploadFailedStatus": "Upload failed", + "selectFileButton": "Select File", + "uploadDebugButton": "Upload & Debug", + "uploadingButton": "Uploading...", + "uploadProgress": "Upload Progress: {{percent}}%", + "selected": "Selected:", + "documentId": "Document ID:" + }, + "monitoring": { + "processingComplete": "Processing {{status}}!", + "ocrInProgress": "OCR processing in progress...", + "queuedForOcr": "Document queued for OCR processing...", + "checkingStatus": "Checking processing status...", + "monitoringTimeout": "Monitoring stopped (timeout)" + }, + "search": { + "title": "Debug Existing Document", + "description": "Enter a document ID to analyze the processing pipeline for an existing document", + "documentIdLabel": "Document ID", + "documentIdPlaceholder": "e.g., 123e4567-e89b-12d3-a456-426614174000", + "debugButton": "Debug" + }, + "tabs": { + "uploadAndDebug": "Upload & Debug", + "searchExisting": "Search Existing", + "debugResults": "Debug Results" + }, + "actions": { + "debugAnalysis": "Debug Analysis", + "showDebugDetails": "Show Debug Details", + "refreshStatus": "Refresh Status", + "viewDocument": "View Document" + }, + "document": { + "title": "Document: {{filename}}", + "status": "Status: {{status}}", + "debugRunAt": "Debug run at: {{timestamp}}" + }, + "pipeline": { + "title": "Processing Pipeline" + }, + "steps": { + "fileInformation": { + "title": "File Information", + "filename": "Filename:", + "original": "Original:", + "size": "Size:", + "mimeType": "MIME Type:", + "fileExists": "File Exists:", + "yes": "Yes", + "no": "No" + }, + "fileMetadata": { + "title": "File Metadata", + "actualSize": "Actual Size:", + "isFile": "Is File:", + "modified": "Modified:", + "created": "Created:", + "unknown": "Unknown", + "notAvailable": "File metadata not available" + }, + "fileAnalysis": { + "title": "Detailed File Analysis", + "basicAnalysis": "Basic Analysis", + "fileType": "File Type:", + "size": "Size:", + "readable": "Readable:", + "fileError": "File Error:", + "pdfAnalysis": "PDF Analysis", + "validPdf": "Valid PDF:", + "pdfVersion": "PDF Version:", + "pages": "Pages:", + "hasText": "Has Text:", + "hasImages": "Has Images:", + "encrypted": "Encrypted:", + "fontCount": "Font Count:", + "textLength": "Text Length:", + "chars": "chars", + "pdfTextExtractionError": "PDF Text Extraction Error:", + "textPreview": "Text Preview", + "fileContent": "File Content", + "noPreview": "No preview available for this file type" + }, + "queueStatus": { + "title": "Queue Status", + "userOcrEnabled": "User OCR Enabled:", + "queueEntries": "Queue Entries:", + "queueHistory": "Queue History", + "status": "Status", + "priority": "Priority", + "created": "Created", + "started": "Started", + "completed": "Completed", + "attempts": "Attempts", + "worker": "Worker" + }, + "ocrResults": { + "title": "OCR Results", + "textLength": "Text Length:", + "characters": "characters", + "confidence": "Confidence:", + "wordCount": "Word Count:", + "processingTime": "Processing Time:", + "completedAt": "Completed:", + "notCompleted": "Not completed", + "processingDetails": "Processing Details", + "hasProcessedImage": "Has Processed Image:", + "imageSize": "Image Size:", + "fileSize": "File Size:", + "processingSteps": "Processing Steps:", + "none": "None", + "processingParameters": "Processing Parameters:" + }, + "qualityValidation": { + "title": "Quality Thresholds", + "minConfidence": "Min Confidence:", + "brightness": "Brightness:", + "contrast": "Contrast:", + "noise": "Noise:", + "sharpness": "Sharpness:", + "actualValues": "Actual Values", + "confidence": "Confidence:", + "wordCount": "Word Count:", + "processedImageAvailable": "Processed Image Available:", + "qualityChecks": "Quality Checks" + } + }, + "failedDocument": { + "title": "Failed Document Information", + "failureDetails": "Failure Details", + "failureReason": "Failure Reason:", + "failureStage": "Failure Stage:", + "retryCount": "Retry Count:", + "created": "Created:", + "lastRetry": "Last Retry:", + "failedOcrResults": "Failed OCR Results", + "ocrTextLength": "OCR Text Length:", + "ocrConfidence": "OCR Confidence:", + "wordCount": "Word Count:", + "processingTime": "Processing Time:", + "noOcrResults": "No OCR results available", + "errorMessage": "Error Message:", + "contentPreview": "Content Preview" + }, + "processingLogs": { + "title": "Detailed Processing Logs", + "description": "Complete history of all OCR processing attempts for this document", + "attempt": "Attempt", + "status": "Status", + "priority": "Priority", + "created": "Created", + "started": "Started", + "completed": "Completed", + "duration": "Duration", + "waitTime": "Wait Time", + "attempts": "Attempts", + "worker": "Worker", + "error": "Error" + }, + "fileAnalysisSummary": { + "title": "File Analysis Summary", + "fileProperties": "File Properties", + "fileType": "File Type:", + "size": "Size:", + "readable": "Readable:", + "pdfProperties": "PDF Properties", + "validPdf": "Valid PDF:", + "hasTextContent": "Has Text Content:", + "textLength": "Text Length:", + "pageCount": "Page Count:", + "encrypted": "Encrypted:", + "pdfTextExtractionIssue": "PDF Text Extraction Issue:" + }, + "processedImages": { + "title": "Processed Images", + "originalDocument": "Original Document", + "processedImage": "Processed Image (OCR Input)", + "notAvailable": "Processed image not available" + }, + "userSettings": { + "title": "User Settings", + "ocrSettings": "OCR Settings", + "backgroundOcr": "Background OCR:", + "enabled": "Enabled", + "disabled": "Disabled", + "minConfidence": "Min Confidence:", + "maxFileSize": "Max File Size:", + "qualityThresholds": "Quality Thresholds", + "brightness": "Brightness:", + "contrast": "Contrast:", + "noise": "Noise:", + "sharpness": "Sharpness:" + }, + "preview": "Preview" + }, + "sources": { + "title": "Document Sources", + "subtitle": "Connect and manage your document sources with intelligent syncing", + "empty": { + "title": "No Sources Configured", + "subtitle": "Connect your first document source to start automatically syncing and processing your files with AI-powered OCR.", + "addFirst": "Add Your First Source" + }, + "actions": { + "addSource": "Add Source", + "editSource": "Edit Source", + "deleteSource": "Delete Source", + "testConnection": "Test Connection", + "testing": "Testing...", + "saveSource": "Save Source", + "createSource": "Create Source", + "updateSource": "Update Source", + "triggerSync": "Trigger Sync", + "stopSync": "Stop Sync", + "viewIgnoredFiles": "View Ignored Files", + "runValidation": "Run Validation Check", + "quickSync": "Quick Sync", + "deepScan": "Deep Scan" + }, + "status": { + "autoRefreshing": "Auto-refreshing...", + "disabled": "Disabled", + "syncing": "Syncing", + "error": "Error", + "idle": "Idle" + }, + "ocr": { + "pause": "Pause OCR", + "resume": "Resume OCR", + "pausedSuccess": "OCR processing paused successfully", + "pauseFailed": "Failed to pause OCR processing", + "resumedSuccess": "OCR processing resumed successfully", + "resumeFailed": "Failed to resume OCR processing" + }, + "stats": { + "documentsStored": "Documents Stored", + "documentsStoredTooltip": "Total number of documents currently stored from this source", + "ocrProcessed": "OCR Processed", + "ocrProcessedTooltip": "Number of documents that have been successfully OCR'd", + "ocrCount": "{{count}} OCR'd", + "lastSync": "Last Sync", + "lastSyncTooltip": "When this source was last synchronized", + "never": "Never", + "filesPending": "Files Pending", + "filesPendingTooltip": "Files discovered but not yet processed during sync", + "totalSize": "Total Size", + "totalSizeTooltip": "Total size of files successfully downloaded from this source" + }, + "types": { + "webdav": { + "name": "WebDAV", + "description": "Nextcloud, ownCloud, and other WebDAV servers" + }, + "localFolder": { + "name": "Local Folder", + "description": "Monitor local filesystem directories" + }, + "s3": { + "name": "S3 Compatible", + "description": "AWS S3, MinIO, and other S3-compatible storage" + } + }, + "form": { + "sourceName": "Source Name", + "sourceType": "Source Type", + "sourceEnabled": "Source Enabled", + "sourceEnabledHelper": "Enable this source for syncing", + "sourceNamePlaceholder": "My Document Server" + }, + "webdav": { + "title": "WebDAV Configuration", + "serverUrl": "Server URL", + "username": "Username", + "password": "Password", + "serverType": "Server Type", + "serverTypes": { + "nextcloud": "Nextcloud", + "nextcloudDesc": "Optimized for Nextcloud servers", + "owncloud": "ownCloud", + "owncloudDesc": "Optimized for ownCloud servers", + "generic": "Generic WebDAV", + "genericDesc": "Any standard WebDAV server" + } + }, + "localFolder": { + "title": "Local Folder Configuration", + "description": "Monitor local filesystem directories for new documents. Ensure the application has read access to the specified paths.", + "recursive": "Recursive Scanning", + "recursiveDesc": "Scan subdirectories recursively", + "followSymlinks": "Follow Symbolic Links", + "followSymlinksDesc": "Follow symlinks when scanning directories" + }, + "s3": { + "title": "S3 Compatible Storage Configuration", + "description": "Connect to AWS S3, MinIO, or any S3-compatible storage service. For MinIO, provide the endpoint URL of your server.", + "bucketName": "Bucket Name", + "region": "Region", + "accessKeyId": "Access Key ID", + "secretAccessKey": "Secret Access Key", + "endpointUrl": "Endpoint URL (Optional)", + "endpointUrlHelper": "Leave empty for AWS S3, or provide custom endpoint for MinIO/other S3-compatible storage", + "objectPrefix": "Object Key Prefix (Optional)", + "objectPrefixHelper": "Optional prefix to limit scanning to specific object keys" + }, + "common": { + "folders": "Folders to Monitor", + "foldersDesc": "Specify the folders within your source to monitor for new documents", + "addFolder": "Add Folder Path", + "extensions": "File Extensions", + "extensionsDesc": "File types to sync and process with OCR.", + "addExtension": "Add Extension" + }, + "advanced": { + "title": "Advanced Settings", + "description": "Configure automatic sync and advanced options", + "enableAutoSync": "Enable Automatic Sync", + "autoSyncDesc": "Automatically sync files on a schedule", + "autoSyncDescLocal": "Automatically scan for new files on a schedule", + "autoSyncDescS3": "Automatically check for new objects on a schedule", + "syncInterval": "Sync Interval (minutes)", + "syncIntervalHelper": "How often to check for new files (15 min - 24 hours)", + "syncIntervalHelperLocal": "How often to scan for new files (15 min - 24 hours)", + "syncIntervalHelperS3": "How often to check for new objects (15 min - 24 hours)" + }, + "estimation": { + "title": "Crawl Estimation", + "description": "Estimate how many files will be processed and how long it will take.", + "estimate": "Estimate Crawl", + "estimating": "Estimating...", + "analyzing": "Analyzing folders and counting files...", + "results": "Estimation Results", + "files": "Estimated Files", + "time": "Estimated Time", + "size": "Estimated Size" + }, + "dialog": { + "editTitle": "Edit Source", + "createTitle": "Create New Source", + "editSubtitle": "Update your source configuration", + "createSubtitle": "Connect a new document source" + }, + "sync": { + "quickSyncDesc": "Fast incremental sync using ETags. Only processes new or changed files.", + "deepScanDesc": "Complete rescan that resets ETag expectations. Use for troubleshooting sync issues." + }, + "validation": { + "healthy": "Healthy", + "warning": "Warning", + "critical": "Critical", + "validating": "Validating", + "unknown": "Unknown", + "statusUnknown": "Validation status unknown", + "inProgress": "Validation check in progress", + "healthScore": "Health score: {{score}}", + "healthScoreIssues": "Health score: {{score}} - Issues detected", + "healthScoreCritical": "Health score: {{score}} - Critical issues" + }, + "delete": { + "title": "Delete Source", + "message": "Are you sure you want to delete this source?", + "warning": "This action cannot be undone. All sync history and settings will be lost.", + "deleting": "Deleting..." + }, + "messages": { + "createSuccess": "Source created successfully", + "updateSuccess": "Source updated successfully", + "deleteSuccess": "Source deleted successfully", + "syncStartSuccess": "Quick sync started successfully", + "deepScanSuccess": "Deep scan started successfully", + "syncStopSuccess": "Sync stopped successfully", + "connectionSuccess": "Connection successful!", + "estimationSuccess": "Crawl estimation completed" + }, + "errors": { + "loadFailed": "Failed to load sources", + "saveFailed": "Failed to save source", + "deleteFailed": "Failed to delete source", + "testConnectionFailed": "Failed to test connection", + "syncStartFailed": "Failed to start sync", + "syncStopFailed": "Failed to stop sync", + "deepScanFailed": "Failed to start deep scan", + "estimateFailed": "Failed to estimate crawl", + "connectionFailed": "Connection failed", + "duplicateName": "A source with this name already exists. Please choose a different name.", + "invalidConfig": "Source configuration is invalid. Please check your settings and try again.", + "authFailed": "Authentication failed. Please verify your credentials.", + "connectionError": "Cannot connect to the source. Please check your network and server settings.", + "invalidPath": "Invalid path specified. Please check your folder paths and try again.", + "notFound": "Source not found. It may have already been deleted.", + "syncInProgress": "Cannot delete source while sync is in progress. Please stop the sync first.", + "alreadySyncing": "Source is already syncing. Please wait for the current sync to complete.", + "cannotConnect": "Cannot connect to source. Please check your connection and try again.", + "authFailedSource": "Authentication failed. Please verify your source credentials.", + "sourceDeleted": "Source not found. It may have been deleted.", + "connectionFailedUrl": "Connection failed. Please check your server URL and network connectivity.", + "authFailedCredentials": "Authentication failed. Please verify your username and password.", + "invalidFolderPath": "Invalid path specified. Please check your folder paths.", + "invalidSettings": "Configuration is invalid. Please review your settings.", + "timeout": "Connection timed out. Please check your network and try again.", + "deepScanWebdavOnly": "Deep scan is only supported for WebDAV sources", + "notSyncing": "Source is not currently syncing" + }, + "labels": { + "recommended": "Recommended", + "notAvailable": "Not Available" + } + } +} \ No newline at end of file diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json new file mode 100644 index 0000000..32cbf55 --- /dev/null +++ b/frontend/public/locales/es/translation.json @@ -0,0 +1,1494 @@ +{ + "common": { + "appName": "Readur", + "appTagline": "Plataforma de Documentos IA", + "welcome": "Bienvenido a {{appName}}", + "welcomeBack": "¡Bienvenido de nuevo, {{username}}! 👋", + "or": "o", + "copyright": "© 2026 Readur. Impulsado por tecnología avanzada de OCR e IA.", + "actions": { + "save": "Guardar", + "cancel": "Cancelar", + "close": "Cerrar", + "delete": "Eliminar", + "edit": "Editar", + "view": "Ver", + "download": "Descargar", + "upload": "Subir", + "search": "Buscar", + "filter": "Filtrar", + "sort": "Ordenar", + "clear": "Limpiar", + "refresh": "Actualizar", + "retry": "Reintentar", + "create": "Crear", + "update": "Actualizar", + "viewDetails": "Ver Detalles", + "back": "Atrás" + }, + "status": { + "loading": "Cargando...", + "processing": "Procesando...", + "completed": "Completado", + "failed": "Fallido", + "pending": "Pendiente", + "success": "Éxito", + "error": "Error" + }, + "time": { + "seconds": "{{count}} segundos", + "minutes": "{{count}} minutos", + "hours": "{{count}} horas", + "days": "{{count}} días" + }, + "sizes": { + "bytes": "{{count}} Bytes", + "kb": "{{count}} KB", + "mb": "{{count}} MB", + "gb": "{{count}} GB" + }, + "moreCount": "+{{count}} más", + "of": "de", + "and": "y" + }, + "auth": { + "signIn": "Iniciar sesión", + "signingIn": "Iniciando sesión...", + "signInToAccount": "Inicia sesión en tu cuenta", + "signInWithOIDC": "Iniciar sesión con OIDC", + "redirecting": "Redirigiendo...", + "username": "Nombre de usuario", + "password": "Contraseña", + "usernameRequired": "El nombre de usuario es obligatorio", + "passwordRequired": "La contraseña es obligatoria", + "logout": "Cerrar sesión", + "profile": "Perfil", + "intelligentDocumentPlatform": "Tu plataforma inteligente de gestión de documentos", + "errors": { + "invalidCredentials": "Nombre de usuario o contraseña inválidos. Por favor verifica tus credenciales e intenta de nuevo.", + "accountDisabled": "Tu cuenta ha sido deshabilitada. Por favor contacta a un administrador para obtener ayuda.", + "userNotFound": "No se encontró ninguna cuenta con este nombre de usuario. Por favor verifica tu nombre de usuario o contacta a soporte.", + "sessionExpired": "Tu sesión ha expirado. Por favor intenta iniciar sesión nuevamente.", + "networkError": "Error de red. Por favor verifica tu conexión e intenta de nuevo.", + "serverError": "Error del servidor. Por favor intenta más tarde o contacta a soporte si el problema persiste.", + "oidcAuthFailed": "La autenticación OIDC falló. Por favor consulta con tu administrador.", + "oidcNotConfigured": "OIDC no está configurado en este servidor. Por favor usa inicio de sesión con nombre de usuario/contraseña.", + "oidcInitFailed": "Falló el inicio de sesión OIDC. Por favor intenta de nuevo.", + "loginFailed": "Falló el inicio de sesión. Por favor verifica tus credenciales." + } + }, + "navigation": { + "dashboard": "Panel", + "upload": "Subir", + "documents": "Documentos", + "search": "Buscar", + "labels": "Etiquetas", + "sources": "Fuentes", + "watchFolder": "Carpeta Vigilada", + "documentManagement": "Gestión de Documentos", + "ignoredFiles": "Archivos Ignorados" + }, + "dashboard": { + "greeting": "Esto es lo que está pasando con tus documentos hoy.", + "stats": { + "totalDocuments": { + "title": "Documentos Totales", + "subtitle": "Archivos en tu biblioteca", + "trend": "{{count}} total", + "trendEmpty": "Aún no hay documentos" + }, + "storageUsed": { + "title": "Almacenamiento Usado", + "subtitle": "Tamaño total de archivos", + "trend": "{{size}} usado", + "trendEmpty": "No se usa almacenamiento" + }, + "ocrProcessed": { + "title": "OCR Procesado", + "subtitle": "Documentos con texto extraído", + "trend": "{{percentage}}% completado", + "trendEmpty": "0% completado" + }, + "searchable": { + "title": "Buscable", + "subtitle": "Listo para búsqueda", + "trend": "{{count}} indexados", + "trendEmpty": "Nada indexado aún" + } + }, + "recentDocuments": { + "title": "Documentos Recientes", + "viewAll": "Ver Todos", + "noDocuments": "Aún no hay documentos", + "uploadFirst": "Sube tu primer documento para comenzar" + }, + "quickActions": { + "title": "Acciones Rápidas", + "upload": { + "title": "Subir Documentos", + "description": "Agrega nuevos archivos para procesamiento OCR" + }, + "search": { + "title": "Buscar en Biblioteca", + "description": "Encuentra documentos por contenido o metadatos" + }, + "browse": { + "title": "Explorar Documentos", + "description": "Ver y gestionar tu biblioteca de documentos" + } + } + }, + "search": { + "title": "Buscar Documentos", + "placeholder": "Buscar documentos por contenido, nombre de archivo o etiquetas... Prueba 'factura', 'contrato', o tag:importante", + "searchPlaceholder": "Buscar documentos...", + "noResults": { + "title": "No se encontraron resultados para \"{{query}}\"", + "subtitle": "Intenta ajustar tus términos de búsqueda o filtros", + "suggestions": { + "title": "Sugerencias:", + "simpler": "Intenta términos más simples o generales", + "spelling": "Verifica la ortografía y prueba diferentes palabras clave", + "removeFilters": "Elimina algunos filtros para ampliar tu búsqueda", + "useQuotes": "Usa comillas para frases exactas" + } + }, + "searching": "Buscando...", + "searchingAsYouType": "Buscando mientras escribes...", + "quickResults": "Resultados Rápidos", + "resultsCount": "{{count}} encontrado{{plural}}", + "viewAllResults": "Ver todos los resultados para \"{{query}}\"", + "recentSearches": "Búsquedas Recientes", + "startTyping": "Comienza a escribir para buscar documentos", + "popularSearches": "Búsquedas populares:", + "noDocumentsFound": "No se encontraron documentos para \"{{query}}\"", + "pressEnterAdvanced": "Presiona Enter para buscar con opciones avanzadas", + "trySuggestions": "Prueba estas sugerencias:", + "settings": { + "title": "Configuración de Búsqueda" + }, + "modes": { + "smart": "Inteligente", + "exactPhrase": "Frase exacta", + "similarWords": "Palabras similares", + "advanced": "Avanzado", + "enhanced": "Mejorado" + }, + "quickSuggestions": { + "title": "Sugerencias rápidas:" + }, + "relatedSearches": { + "title": "Búsquedas relacionadas:" + }, + "filters": { + "title": "Filtros", + "tags": "Etiquetas", + "selectTags": "Seleccionar Etiquetas", + "ocrStatus": "Estado OCR", + "ocrText": "Texto OCR", + "allDocuments": "Todos los Documentos", + "hasOcrText": "Tiene Texto OCR", + "noOcrText": "Sin Texto OCR", + "dateRange": "Rango de Fechas", + "daysAgo": "Días atrás: {{min}} - {{max}}", + "dateMarks": { + "today": "Hoy", + "30d": "30d", + "90d": "90d", + "1y": "1a" + }, + "fileSize": "Tamaño de Archivo", + "sizeRange": "Tamaño: {{min}}MB - {{max}}MB" + }, + "status": { + "searching": "Buscando...", + "resultsFound": "{{count}} resultados encontrados" + }, + "display": { + "settings": "Configuración de Visualización", + "textSettings": "Configuración de Texto", + "fontSizeLabel": "Tamaño de Fuente: {{size}}px", + "snippetsPerResult": "Fragmentos por resultado: {{count}}", + "contextLength": "Longitud de Contexto: {{length}} caracteres", + "viewMode": { + "label": "Modo de Vista", + "compact": "Compacto", + "detailed": "Detallado", + "contextFocus": "Enfoque en Contexto" + }, + "highlightStyle": { + "label": "Estilo de Resaltado", + "background": "Color de Fondo", + "underline": "Subrayado", + "bold": "Texto en Negrita" + } + }, + "results": { + "showing": "Mostrando:", + "snippetsCount": "{{count}} fragmentos", + "fontSize": "fuente de {{size}}px", + "hasOcr": " • OCR", + "tags": "Etiquetas:", + "pagination": "Mostrando {{start}}-{{end}} de {{total}} resultados" + }, + "empty": { + "title": "Comienza a buscar tus documentos", + "subtitle": "Usa la barra de búsqueda mejorada arriba para encontrar documentos por contenido, nombre de archivo o etiquetas" + }, + "tips": { + "title": "Consejos de Búsqueda:", + "exactPhrase": "Usa comillas para frases exactas: \"plan de proyecto\"", + "tags": "Busca por etiquetas: tag:importante o tag:factura", + "combine": "Combina términos: contrato AND pago", + "wildcards": "Usa comodines: proy* para proyecto, proyectos, etc." + }, + "examples": { + "invoice": "Prueba: factura", + "contract": "Prueba: contrato", + "tagImportant": "Prueba: tag:importante" + }, + "actions": { + "clearFilters": "Limpiar Filtros", + "newSearch": "Nueva Búsqueda" + } + }, + "upload": { + "title": "Subir Documentos", + "subtitle": "Transforma tus documentos con procesamiento OCR inteligente", + "features": { + "aiOcr": { + "title": "OCR con IA", + "description": "Extracción avanzada de texto de cualquier tipo de documento" + }, + "fullTextSearch": { + "title": "Búsqueda de Texto Completo", + "description": "Encuentra documentos instantáneamente por contenido o metadatos" + }, + "lightningFast": { + "title": "Ultra Rápido", + "description": "Procesa documentos en segundos, no en minutos" + }, + "secure": { + "title": "Seguro y Privado", + "description": "Tus documentos están encriptados y protegidos" + }, + "multiLanguage": { + "title": "Multilingüe", + "description": "Soporte para más de 100 idiomas y escrituras" + } + }, + "tips": { + "title": "📋 Consejos de Subida", + "highRes": "• Para mejores resultados de OCR, usa imágenes de alta resolución", + "pdfText": "• Los archivos PDF con capas de texto se procesan más rápido", + "clarity": "• Asegúrate de que los documentos estén bien iluminados y claramente legibles", + "maxSize": "• El tamaño máximo de archivo es 50MB por documento" + }, + "dropzone": { + "dragDrop": "Arrastra y suelta archivos aquí", + "dropHere": "Suelta archivos aquí", + "browse": "o haz clic para explorar tu computadora", + "chooseFiles": "Elegir Archivos", + "maxFileSize": "Tamaño máximo de archivo: 50MB por archivo", + "fileTypes": { + "pdf": "PDF", + "images": "Imágenes", + "text": "Texto", + "word": "Word" + } + }, + "languageSettings": { + "title": "🌐 Configuración de Idioma OCR", + "description": "Selecciona idiomas para reconocimiento óptimo de texto OCR" + }, + "labelAssignment": { + "title": "📋 Asignación de Etiquetas", + "description": "Selecciona etiquetas para asignar automáticamente a todos los documentos subidos", + "placeholder": "Elige etiquetas para tus documentos...", + "helperText": "Estas etiquetas se aplicarán a todos los documentos subidos" + }, + "fileList": { + "title": "Archivos ({{count}})", + "clearCompleted": "Limpiar Completados", + "uploading": "Subiendo... ({{completed}}/{{total}})", + "uploadingSimple": "Subiendo...", + "uploadAll": "Subir Todos" + }, + "errors": { + "sessionExpired": "Tu sesión ha expirado. Por favor actualiza la página e inicia sesión nuevamente.", + "labelPermissionDenied": "No tienes permiso para acceder a las etiquetas.", + "labelNetworkError": "Error de red al cargar etiquetas. Por favor verifica tu conexión.", + "fileTooLarge": "El archivo es demasiado grande. El tamaño máximo es 50MB.", + "unsupportedFormat": "Formato de archivo no soportado. Por favor usa PDF, imágenes, texto o documentos Word.", + "processingFailed": "Falló el procesamiento del documento. Por favor intenta de nuevo o contacta a soporte.", + "permissionDenied": "No tienes permiso para subir documentos.", + "networkError": "Error de red. Por favor verifica tu conexión e intenta de nuevo.", + "serverError": "Error del servidor. Por favor intenta más tarde." + } + }, + "documents": { + "title": "Documentos", + "subtitle": "Gestiona y explora tu biblioteca de documentos", + "viewDocument": "Ver documento", + "downloadDocument": "Descargar documento", + "deleteDocument": "Eliminar documento", + "unknownDocument": "Documento Desconocido", + "search": { + "placeholder": "Buscar documentos..." + }, + "selection": { + "select": "Seleccionar", + "cancel": "Cancelar", + "count": "{{count}} de {{total}} documentos seleccionados", + "selectAll": "Seleccionar Todos", + "deselectAll": "Deseleccionar Todos", + "deleteSelected": "Eliminar Seleccionados ({{count}})" + }, + "filters": { + "ocrStatus": "Estado OCR", + "all": "Todos", + "completed": "Completado", + "processing": "Procesando", + "failed": "Fallido", + "pending": "Pendiente" + }, + "sort": { + "label": "Ordenar", + "newestFirst": "Más Recientes Primero", + "oldestFirst": "Más Antiguos Primero", + "nameAZ": "Nombre A-Z", + "nameZA": "Nombre Z-A", + "largestFirst": "Más Grandes Primero", + "smallestFirst": "Más Pequeños Primero" + }, + "ocrStatus": { + "confidence": "OCR {{percent}}%", + "done": "OCR Completado", + "processing": "Procesando...", + "failed": "OCR Fallido", + "pending": "Pendiente" + }, + "actions": { + "editLabels": "Editar Etiquetas", + "retryOcr": "Reintentar OCR", + "retryingOcr": "Reintentando OCR...", + "retryHistory": "Historial de Reintentos" + }, + "empty": { + "title": "No se encontraron documentos", + "searchSubtitle": "Intenta ajustar tus términos de búsqueda", + "uploadSubtitle": "Sube tu primer documento para comenzar" + }, + "dialogs": { + "editLabels": { + "title": "Editar Etiquetas del Documento", + "placeholder": "Selecciona etiquetas para este documento..." + }, + "delete": { + "title": "Eliminar Documento", + "message": "¿Estás seguro de que quieres eliminar \"{{filename}}\"?", + "warning": "Esta acción no se puede deshacer. El archivo del documento y todos los datos asociados se eliminarán permanentemente.", + "deleting": "Eliminando...", + "delete": "Eliminar" + }, + "bulkDelete": { + "title": "Eliminar Múltiples Documentos", + "message": "¿Estás seguro de que quieres eliminar {{count}} documento{{plural}} seleccionado{{plural}}?", + "warning": "Esta acción no se puede deshacer. Todos los documentos seleccionados y sus datos asociados se eliminarán permanentemente.", + "listTitle": "Documentos a eliminar:", + "moreCount": "... y {{count}} más", + "deleteButton": "Eliminar {{count}} Documento{{plural}}" + } + }, + "pagination": { + "showing": "Mostrando {{start}}-{{end}} de {{total}} documentos", + "withOcrStatus": " con estado OCR: {{status}}", + "matching": " coincidiendo con \"{{query}}\"" + } + }, + "documentDetails": { + "subtitle": "Visor completo de análisis de documentos y metadatos", + "actions": { + "backToDocuments": "Volver a Documentos", + "download": "Descargar", + "viewDocument": "Ver Documento", + "viewOcrText": "Ver Texto OCR", + "viewProcessedImage": "Ver Imagen Procesada", + "retryOcr": "Reintentar OCR", + "retryHistory": "Historial de Reintentos", + "editLabels": "Editar Etiquetas", + "deleteDocument": "Eliminar Documento" + }, + "errors": { + "notFound": "Documento no encontrado" + }, + "metadata": { + "fileSize": "Tamaño de Archivo", + "uploadDate": "Fecha de Subida", + "sourceType": "Tipo de Fuente", + "originalPath": "Ruta Original", + "originalCreated": "Creación Original", + "originalModified": "Modificación Original", + "ocrStatus": "Estado OCR", + "textExtracted": "Texto Extraído" + }, + "ocr": { + "title": "🔍 Texto Extraído (OCR)", + "expandTooltip": "Expandir para ver texto completo con búsqueda", + "expand": "Expandir", + "loading": "Cargando análisis OCR...", + "confidence": "Confianza", + "words": "Palabras", + "processingTime": "Tiempo de Procesamiento", + "error": "Error de Procesamiento OCR", + "noText": "No hay texto OCR disponible para este documento.", + "completed": "✅ Procesamiento completado: {{date}}", + "loadFailed": "El texto OCR está disponible pero falló al cargar. Por favor intenta actualizar la página." + }, + "tagsLabels": { + "title": "🏷️ Etiquetas y Marcas", + "tags": "Etiquetas", + "labels": "Marcas", + "noLabels": "No hay etiquetas asignadas a este documento" + }, + "dialogs": { + "ocrText": { + "title": "Texto Extraído (OCR)", + "confidence": "{{percent}}% confianza", + "words": "{{count}} palabras", + "loading": "Cargando texto OCR...", + "error": "Error OCR: {{message}}", + "noText": "No hay texto OCR disponible para este documento.", + "processingTime": "Tiempo de procesamiento: {{time}}ms", + "completed": "Completado: {{date}}" + }, + "ocrExpanded": { + "title": "🔍 Texto Extraído (OCR) - Vista Completa", + "searchPlaceholder": "Buscar dentro del texto extraído...", + "matches": "{{count}} coincidencia{{plural}} encontrada{{plural}}", + "noMatches": "No se encontraron coincidencias", + "loading": "Cargando texto OCR...", + "error": "Error OCR: {{message}}", + "noText": "No hay texto OCR disponible para este documento." + }, + "processedImage": { + "title": "Imagen Procesada - Mejora OCR Aplicada", + "description": "Esta es la imagen mejorada que fue procesada por el motor OCR. Puedes ajustar la configuración de mejora OCR en la página de Configuración.", + "noImage": "No hay imagen procesada disponible" + }, + "editLabels": { + "title": "Editar Etiquetas del Documento", + "description": "Selecciona etiquetas para asignar a este documento", + "placeholder": "Elige etiquetas para este documento...", + "saveLabels": "Guardar Etiquetas" + }, + "delete": { + "title": "Eliminar Documento", + "warning": "Esta acción no se puede deshacer.", + "message": "¿Estás seguro de que quieres eliminar {{filename}}?", + "details": "Esto eliminará permanentemente el documento y todos los datos asociados incluyendo texto OCR, etiquetas e historial de procesamiento.", + "deleting": "Eliminando...", + "delete": "Eliminar Documento" + } + } + }, + "documentManagement": { + "title": "Gestión de Documentos", + "retryAll": "Reintentar Todos los Documentos", + "retrying": "Reintentando Todos...", + "retryFailedOnly": "Reintentar Solo Fallidos", + "tabs": { + "failedDocuments": "Documentos Fallidos{{showCount, select, true { ({{count}}) } other {}}", + "failedDocumentsTooltip": "Ver y gestionar documentos que fallaron durante el procesamiento (OCR, ingesta, validación, etc.)", + "cleanup": "Limpieza de Documentos{{showCount, select, true { ({{count}}) } other {}}", + "cleanupTooltip": "Gestionar y limpiar documentos con problemas de calidad - baja confianza de OCR o procesamiento fallido", + "duplicates": "Archivos Duplicados{{showCount, select, true { ({{count}}) } other {}}", + "duplicatesTooltip": "Ver y gestionar grupos de documentos duplicados - documentos con contenido idéntico", + "ignoredFiles": "Archivos Ignorados{{showCount, select, true { ({{count}}) } other {}}", + "ignoredFilesTooltip": "Gestionar archivos que han sido ignorados durante operaciones de sincronización" + }, + "stats": { + "totalFailed": "Total Fallidos", + "failureCategories": "Categorías de Fallos", + "noFailureData": "No hay datos de fallos disponibles" + }, + "advancedRetry": { + "title": "Opciones Avanzadas de Reintento", + "button": "Reintento Avanzado", + "description": "Usa opciones avanzadas de filtrado y selección para reintentar subconjuntos específicos de documentos fallidos basados en tipo de archivo, razón de fallo, tamaño y más." + }, + "filters": { + "title": "Opciones de Filtro", + "stage": "Filtrar por Etapa", + "reason": "Filtrar por Razón", + "allStages": "Todas las Etapas", + "allReasons": "Todas las Razones", + "clearFilters": "Limpiar Filtros", + "stages": { + "ocr": "Procesamiento OCR", + "ingestion": "Ingesta de Documentos", + "validation": "Validación", + "storage": "Almacenamiento de Archivos", + "processing": "Procesamiento", + "sync": "Sincronización" + }, + "reasons": { + "duplicateContent": "Contenido Duplicado", + "lowConfidence": "Baja Confianza de OCR", + "unsupportedFormat": "Formato No Soportado", + "fileTooLarge": "Archivo Demasiado Grande", + "fileCorrupted": "Archivo Corrupto", + "ocrTimeout": "Tiempo de Espera de OCR", + "pdfParsingError": "Error de Análisis de PDF", + "other": "Otro" + } + }, + "alerts": { + "noFailedTitle": "¡Buenas noticias!", + "noFailedMessage": "Ningún documento ha fallado el procesamiento OCR. Todos tus documentos se están procesando exitosamente.", + "overviewTitle": "Resumen de Documentos Fallidos", + "overviewMessage": "Estos documentos fallaron en varias etapas del procesamiento: ingesta, validación, OCR, almacenamiento, etc. Usa los filtros arriba para reducir por etapa de fallo o razón específica. Puedes reintentar el procesamiento para fallos recuperables." + }, + "table": { + "document": "Documento", + "failureType": "Tipo de Fallo", + "retryCount": "Conteo de Reintentos", + "lastFailed": "Último Fallo", + "actions": "Acciones", + "attempts": "{{count}} intentos", + "unknown": "Desconocido" + }, + "actions": { + "retryOcr": "Reintentar OCR", + "viewDetails": "Ver Detalles", + "retryHistory": "Historial de Reintentos", + "download": "Descargar Documento" + }, + "details": { + "errorDetails": "Detalles del Error", + "failureReason": "Razón del Fallo", + "notSpecified": "No especificado", + "ocrResults": "Resultados de OCR", + "confidencePercent": "{{percent}}% de confianza", + "wordsFound": "{{count}} palabras encontradas", + "errorMessage": "Mensaje de Error", + "noErrorMessage": "No hay mensaje de error disponible", + "lastAttempt": "Último Intento", + "noPreviousAttempts": "Sin intentos previos", + "fileCreated": "Archivo Creado" + }, + "retry": { + "queuedSuccess": "Reintento de OCR encolado para \"{{filename}}\". Tiempo de espera estimado: {{minutes}} minutos.", + "unknown": "Desconocido", + "failed": "Falló el reintento de OCR", + "processingFailed": "Falló el reintento del procesamiento OCR", + "bulkSuccess": "Se encolaron exitosamente {{count}} documentos para reintento de OCR. Tiempo de procesamiento estimado: {{minutes}} minutos.", + "noDocuments": "No se encontraron documentos para reintentar", + "bulkFailed": "Falló el reintento de documentos. Por favor intenta de nuevo.", + "requeuedSuccess": "Se encolaron exitosamente {{count}} documentos fallidos para reintento de OCR. Verifica las estadísticas de la cola para el progreso.", + "noFailedDocuments": "No se encontraron documentos fallidos para reintentar", + "requeuedFailed": "Falló el reintento de todos los documentos OCR fallidos", + "advancedSuccess": "Se encolaron exitosamente {{queued}} de {{matched}} documentos para reintento. Tiempo de procesamiento estimado: {{minutes}} minutos." + }, + "cleanup": { + "previewFailed": "Falló la vista previa de documentos de baja confianza", + "noDocuments": "No hay documentos para eliminar", + "deleteFailed": "Falló la eliminación de documentos de baja confianza", + "previewFailedDocs": "Falló la vista previa de documentos fallidos", + "deleteFailedDocs": "Falló la eliminación de documentos fallidos" + }, + "ignoredFiles": { + "removedSuccess": "Archivos removidos de la lista de ignorados", + "deleteFailed": "Falló la eliminación de archivos ignorados", + "fileRemovedSuccess": "Archivo removido de la lista de ignorados", + "fileDeleteFailed": "Falló la eliminación del archivo ignorado" + }, + "errors": { + "loadFailedDocuments": "Falló la carga de documentos fallidos", + "sessionExpired": "Tu sesión ha expirado. Por favor actualiza la página e inicia sesión nuevamente.", + "permissionDenied": "No tienes permiso para ver documentos fallidos.", + "noFailedDocumentsFound": "No se encontraron documentos fallidos o pueden haber sido procesados.", + "networkError": "Error de red. Por favor verifica tu conexión e intenta de nuevo.", + "serverError": "Error del servidor. Por favor intenta más tarde.", + "loadDuplicates": "Falló la carga de documentos duplicados", + "permissionDeniedDuplicates": "No tienes permiso para ver documentos duplicados.", + "documentNotFound": "Documento no encontrado. Puede haber sido eliminado o procesado ya.", + "cannotRetry": "El documento no puede ser reintentado debido a problemas de procesamiento. Por favor verifica el formato del documento.", + "permissionDeniedRetry": "No tienes permiso para reintentar el procesamiento OCR.", + "serverErrorSupport": "Error del servidor. Por favor intenta más tarde o contacta a soporte.", + "loadIgnoredFiles": "Falló la carga de archivos ignorados", + "permissionDeniedIgnored": "No tienes permiso para ver archivos ignorados." + } + }, + "settings": { + "title": "Configuración", + "apiDocumentation": "Documentación de API", + "debug": "Depuración", + "language": "Idioma", + "selectLanguage": "Seleccionar idioma", + "tabs": { + "general": "General", + "ocrSettings": "Configuración de OCR", + "userManagement": "Gestión de Usuarios", + "serverConfiguration": "Configuración del Servidor" + }, + "general": { + "title": "Configuración General", + "ocrConfiguration": { + "title": "Configuración de OCR", + "description": "Configura idiomas para la extracción de texto OCR. Múltiples idiomas ayudan con documentos en varios idiomas.", + "autoDetectLanguageCombination": "Auto-detectar combinaciones de idiomas", + "autoDetectLanguageCombinationHelper": "Sugerir automáticamente combinaciones óptimas de idiomas basadas en el análisis del contenido del documento", + "concurrentOcrJobs": "Trabajos OCR Concurrentes", + "concurrentOcrJobsHelper": "Número de trabajos OCR que pueden ejecutarse simultáneamente", + "ocrTimeout": "Tiempo de Espera OCR (segundos)", + "ocrTimeoutHelper": "Tiempo máximo para el procesamiento OCR por archivo", + "cpuPriority": "Prioridad de CPU", + "cpuPriorityLow": "Baja", + "cpuPriorityNormal": "Normal", + "cpuPriorityHigh": "Alta" + }, + "ocrControls": { + "title": "Controles de Procesamiento OCR (Solo Administrador)", + "description": "Controla el procesamiento OCR para gestionar el uso de CPU y permitir que los usuarios usen la aplicación sin impacto en el rendimiento.", + "pauseOcr": "Pausar Procesamiento OCR", + "resumeOcr": "Reanudar Procesamiento OCR", + "ocrStatusLabel": "Estado OCR: {{status}}", + "ocrPausedMessage": "El procesamiento OCR está pausado. No se procesarán nuevos trabajos.", + "ocrActiveMessage": "El procesamiento OCR está activo. Los documentos se procesarán automáticamente.", + "pausedAlertTitle": "Procesamiento OCR Pausado", + "pausedAlertMessage": "Los nuevos documentos no se procesarán para extracción de texto OCR hasta que se reanude el procesamiento. Los usuarios aún pueden subir y ver documentos, pero la funcionalidad de búsqueda puede estar limitada." + }, + "fileProcessing": { + "title": "Procesamiento de Archivos", + "maxFileSize": "Tamaño Máximo de Archivo (MB)", + "maxFileSizeHelper": "Tamaño máximo permitido de archivo para subidas", + "memoryLimit": "Límite de Memoria (MB)", + "memoryLimitHelper": "Límite de memoria por trabajo OCR", + "autoRotateImages": "Rotar Imágenes Automáticamente", + "autoRotateImagesHelper": "Detectar y corregir automáticamente la orientación de la imagen", + "enableImagePreprocessing": "Habilitar Preprocesamiento de Imágenes", + "enableImagePreprocessingHelper": "Mejorar imágenes para mejor precisión OCR (deskew, eliminar ruido, contraste)", + "preprocessingWarning": "⚠️ Advertencia: Habilitar el preprocesamiento puede alterar significativamente los resultados del texto OCR y puede reducir la precisión para algunos documentos", + "enableBackgroundOcr": "Habilitar OCR en Segundo Plano", + "enableBackgroundOcrHelper": "Procesar OCR en segundo plano después de subir el archivo" + }, + "searchConfiguration": { + "title": "Configuración de Búsqueda", + "resultsPerPage": "Resultados por Página", + "snippetLength": "Longitud del Fragmento", + "snippetLengthHelper": "Caracteres a mostrar en vistas previas de resultados de búsqueda", + "fuzzySearchThreshold": "Umbral de Búsqueda Difusa", + "fuzzySearchThresholdHelper": "Tolerancia para errores de ortografía (0.0-1.0)" + }, + "storageManagement": { + "title": "Gestión de Almacenamiento", + "retentionDays": "Días de Retención", + "retentionDaysHelper": "Eliminar automáticamente documentos después de X días (dejar vacío para deshabilitar)", + "enableAutoCleanup": "Habilitar Limpieza Automática", + "enableAutoCleanupHelper": "Eliminar automáticamente archivos huérfanos y limpiar almacenamiento", + "enableCompression": "Habilitar Compresión", + "enableCompressionHelper": "Comprimir documentos almacenados para ahorrar espacio en disco" + } + }, + "ocrSettings": { + "title": "Configuración de Procesamiento de Imágenes OCR", + "enhancementControls": { + "title": "Controles de Mejora", + "skipEnhancement": "Omitir Toda Mejora de Imagen (Usar Solo Imágenes Originales)", + "brightnessBoost": "Aumento de Brillo", + "brightnessBoostHelper": "Ajuste manual de brillo (0 = auto, >0 = cantidad de aumento)", + "contrastMultiplier": "Multiplicador de Contraste", + "contrastMultiplierHelper": "Ajuste manual de contraste (1.0 = auto, >1.0 = aumentar)", + "noiseReductionLevel": "Nivel de Reducción de Ruido", + "noiseReductionNone": "Ninguno", + "noiseReductionLight": "Ligero", + "noiseReductionModerate": "Moderado", + "noiseReductionHeavy": "Pesado", + "sharpeningStrength": "Fuerza de Nitidez", + "sharpeningStrengthHelper": "Cantidad de nitidez de imagen (0 = auto, >0 = manual)" + }, + "qualityThresholds": { + "title": "Umbrales de Calidad (cuándo aplicar mejoras)", + "brightnessThreshold": "Umbral de Brillo", + "brightnessThresholdHelper": "Mejorar si el brillo está por debajo de este valor (0-255)", + "contrastThreshold": "Umbral de Contraste", + "contrastThresholdHelper": "Mejorar si el contraste está por debajo de este valor (0-1)", + "noiseThreshold": "Umbral de Ruido", + "noiseThresholdHelper": "Mejorar si el ruido está por encima de este valor (0-1)", + "sharpnessThreshold": "Umbral de Nitidez", + "sharpnessThresholdHelper": "Mejorar si la nitidez está por debajo de este valor (0-1)" + }, + "advancedProcessing": { + "title": "Opciones Avanzadas de Procesamiento", + "morphologicalOperations": "Operaciones Morfológicas (limpieza de texto)", + "histogramEqualization": "Ecualización de Histograma", + "saveProcessedImages": "Guardar Imágenes Procesadas para Revisión", + "adaptiveThresholdWindowSize": "Tamaño de Ventana de Umbral Adaptativo", + "adaptiveThresholdWindowSizeHelper": "Tamaño de ventana para mejora de contraste (número impar)" + }, + "imageSizeScaling": { + "title": "Tamaño y Escalado de Imagen", + "maxImageWidth": "Ancho Máximo de Imagen", + "maxImageWidthHelper": "Ancho máximo de imagen en píxeles", + "maxImageHeight": "Altura Máxima de Imagen", + "maxImageHeightHelper": "Altura máxima de imagen en píxeles", + "upscaleFactor": "Factor de Escalado", + "upscaleFactorHelper": "Factor de escalado de imagen (1.0 = sin escalado)" + } + }, + "userManagement": { + "title": "Gestión de Usuarios", + "addUser": "Agregar Usuario", + "tableHeaders": { + "username": "Nombre de Usuario", + "email": "Correo Electrónico", + "createdAt": "Creado En", + "watchDirectory": "Directorio de Vigilancia", + "actions": "Acciones" + }, + "watchDirectory": { + "statusActive": "Activo", + "statusDisabled": "Deshabilitado", + "statusNotCreated": "No Creado", + "statusUnknown": "Desconocido", + "loading": "Cargando...", + "createDirectory": "Crear directorio de vigilancia", + "viewDirectory": "Ver directorio de vigilancia", + "removeDirectory": "Eliminar directorio de vigilancia (Solo administrador)", + "editUser": "Editar usuario", + "deleteUser": "Eliminar usuario" + }, + "dialogs": { + "createUser": "Crear Nuevo Usuario", + "editUser": "Editar Usuario", + "username": "Nombre de Usuario", + "email": "Correo Electrónico", + "password": "Contraseña", + "newPassword": "Nueva Contraseña (dejar vacío para mantener la actual)" + }, + "confirmRemoveDirectory": { + "title": "Eliminar Directorio de Vigilancia", + "message": "¿Estás seguro de que quieres eliminar el directorio de vigilancia para el usuario \"{{username}}\"? Esta acción no se puede deshacer y detendrá el monitoreo de su directorio para archivos nuevos.", + "removeButton": "Eliminar Directorio" + } + }, + "serverConfiguration": { + "title": "Configuración del Servidor (Solo Administrador)", + "fileUpload": { + "title": "Configuración de Subida de Archivos", + "maxFileSize": "Tamaño Máximo de Archivo", + "uploadPath": "Ruta de Subida", + "allowedFileTypes": "Tipos de Archivo Permitidos", + "watchFolder": "Carpeta de Vigilancia" + }, + "ocrProcessing": { + "title": "Configuración de Procesamiento OCR", + "concurrentOcrJobs": "Trabajos OCR Concurrentes", + "ocrTimeout": "Tiempo de Espera OCR", + "memoryLimit": "Límite de Memoria", + "ocrLanguage": "Idioma OCR", + "cpuPriority": "Prioridad de CPU", + "backgroundOcr": "OCR en Segundo Plano", + "enabled": "Habilitado", + "disabled": "Deshabilitado" + }, + "serverInformation": { + "title": "Información del Servidor", + "serverHost": "Host del Servidor", + "serverPort": "Puerto del Servidor", + "jwtSecret": "Secreto JWT", + "configured": "Configurado", + "notSet": "No Establecido", + "version": "Versión", + "buildInformation": "Información de Compilación" + }, + "watchFolderConfiguration": { + "title": "Configuración de Carpeta de Vigilancia", + "watchInterval": "Intervalo de Vigilancia", + "fileStabilityCheck": "Verificación de Estabilidad de Archivo", + "maxFileAge": "Edad Máxima de Archivo" + }, + "refreshConfiguration": "Actualizar Configuración", + "loadFailed": "Falló la carga de configuración del servidor. Puede requerir acceso de administrador." + }, + "messages": { + "settingsUpdated": "Configuración actualizada exitosamente", + "settingsUpdateFailed": "Falló la actualización de configuración", + "invalidLanguage": "Idioma inválido seleccionado. Por favor elige de los idiomas disponibles.", + "valueOutOfRange": "{{message}}. {{suggestedAction}}", + "conflictingSettings": "Configuración conflictiva detectada. Por favor revisa tu configuración.", + "userCreated": "Usuario creado exitosamente", + "userUpdated": "Usuario actualizado exitosamente", + "userDeleted": "Usuario eliminado exitosamente", + "cannotDeleteSelf": "No puedes eliminar tu propia cuenta", + "confirmDeleteUser": "¿Estás seguro de que quieres eliminar este usuario?", + "duplicateUsername": "Este nombre de usuario ya está en uso. Por favor elige un nombre de usuario diferente.", + "duplicateEmail": "Esta dirección de correo electrónico ya está en uso. Por favor usa un correo electrónico diferente.", + "invalidPassword": "La contraseña debe tener al menos 8 caracteres con mayúsculas, minúsculas y números.", + "invalidEmail": "Por favor ingresa una dirección de correo electrónico válida.", + "invalidUsername": "El nombre de usuario contiene caracteres inválidos. Por favor usa solo letras, números y guiones bajos.", + "permissionDenied": "No tienes permiso para realizar esta acción.", + "cannotDeleteUser": "No se puede eliminar este usuario: Pueden tener datos asociados o ser el último administrador.", + "userNotFound": "Usuario no encontrado. Puede haber sido eliminado ya.", + "watchDirectoryCreated": "Directorio de vigilancia creado exitosamente", + "watchDirectoryCreatedFailed": "Falló la creación del directorio de vigilancia", + "watchDirectoryAlreadyExists": "El directorio de vigilancia ya existe para este usuario", + "watchDirectoryPath": "Directorio de vigilancia: {{path}}", + "watchDirectoryRemoved": "Directorio de vigilancia eliminado exitosamente", + "watchDirectoryRemoveFailed": "Falló la eliminación del directorio de vigilancia", + "watchDirectoryNotFound": "Directorio de vigilancia no encontrado o ya eliminado", + "ocrPaused": "Procesamiento OCR pausado exitosamente", + "ocrPauseFailed": "Se requiere acceso de administrador para pausar el procesamiento OCR", + "ocrPauseFailedGeneric": "Falló pausar el procesamiento OCR", + "ocrResumed": "Procesamiento OCR reanudado exitosamente", + "ocrResumeFailed": "Se requiere acceso de administrador para reanudar el procesamiento OCR", + "ocrResumeFailedGeneric": "Falló reanudar el procesamiento OCR", + "serverConfigLoadFailed": "Se requiere acceso de administrador para ver la configuración del servidor", + "serverConfigLoadFailedGeneric": "Falló la carga de configuración del servidor" + } + }, + "labels": { + "title": "Gestión de Etiquetas", + "loading": "Cargando etiquetas...", + "search": { + "placeholder": "Buscar etiquetas..." + }, + "filters": { + "systemLabels": "Etiquetas del Sistema" + }, + "sections": { + "systemLabels": "Etiquetas del Sistema", + "myLabels": "Mis Etiquetas" + }, + "badge": { + "system": "Sistema" + }, + "stats": { + "documents": "Documentos: {{count}}", + "sources": "Fuentes: {{count}}" + }, + "actions": { + "createLabel": "Crear Etiqueta", + "editLabel": "Editar etiqueta", + "deleteLabel": "Eliminar etiqueta" + }, + "create": { + "title": "Crear Nueva Etiqueta", + "editTitle": "Editar Etiqueta", + "nameLabel": "Nombre de Etiqueta", + "nameRequired": "El nombre es obligatorio", + "descriptionLabel": "Descripción (opcional)", + "colorLabel": "Color", + "customColorLabel": "Color Personalizado (hex)", + "iconLabel": "Icono (opcional)", + "iconNone": "Ninguno", + "previewLabel": "Vista Previa", + "cancel": "Cancelar", + "create": "Crear", + "update": "Actualizar", + "saving": "Guardando..." + }, + "selector": { + "placeholder": "Buscar o crear etiquetas...", + "systemLabels": "Etiquetas del Sistema", + "myLabels": "Mis Etiquetas", + "createLabel": "Crear etiqueta \"{{name}}\"", + "noLabelsFound": "No se encontraron etiquetas", + "noLabelsMatch": "No hay etiquetas que coincidan con \"{{query}}\"", + "noLabelsAvailable": "No hay etiquetas disponibles" + }, + "empty": { + "title": "No se encontraron etiquetas", + "noMatch": "No hay etiquetas que coincidan con \"{{query}}\"", + "noLabels": "Aún no has creado ninguna etiqueta", + "createFirst": "Crea Tu Primera Etiqueta" + }, + "dialogs": { + "delete": { + "title": "Eliminar Etiqueta", + "message": "¿Estás seguro de que quieres eliminar la etiqueta \"{{name}}\"?", + "inUseWarning": " Esta etiqueta está actualmente en uso por {{count}} documento(s)." + } + }, + "errors": { + "sessionExpired": "Tu sesión ha expirado. Por favor inicia sesión nuevamente.", + "permissionDenied": "No tienes permiso para ver las etiquetas.", + "serverError": "Error del servidor. Por favor intenta más tarde.", + "networkError": "Error de red. Por favor verifica tu conexión e intenta de nuevo.", + "loadFailed": "Falló la carga de etiquetas. Por favor verifica tu conexión.", + "notFound": "Etiqueta no encontrada. Puede haber sido eliminada por otro usuario.", + "duplicateName": "Ya existe una etiqueta con este nombre. Por favor elige un nombre diferente.", + "systemModification": "Las etiquetas del sistema no se pueden modificar. Solo se pueden editar las etiquetas creadas por el usuario.", + "alreadyDeleted": "Etiqueta no encontrada. Puede haber sido eliminada ya.", + "inUse": "No se puede eliminar la etiqueta porque está actualmente asignada a documentos. Por favor elimina la etiqueta de todos los documentos primero.", + "systemDelete": "Las etiquetas del sistema no se pueden eliminar. Solo se pueden eliminar las etiquetas creadas por el usuario.", + "invalidName": "El nombre de la etiqueta contiene caracteres inválidos. Por favor usa solo letras, números y puntuación básica.", + "invalidColor": "Formato de color inválido. Por favor usa un color hexadecimal válido como #0969da.", + "maxLabelsReached": "Se alcanzó el número máximo de etiquetas. Por favor elimina algunas etiquetas antes de crear nuevas." + } + }, + "notifications": { + "title": "Notificaciones", + "markAllAsRead": "Marcar todas como leídas", + "clearAll": "Limpiar todas", + "noNotifications": "No hay notificaciones" + }, + "ocr": { + "languageSelector": { + "label": "Idioma OCR", + "loading": "Cargando idiomas...", + "error": "Falló la carga de idiomas OCR", + "retry": "Reintentar", + "fallback": "Inglés (Respaldo)", + "current": "Actual", + "languagesAvailable": "{{count}} idioma{{plural}} disponible{{plural}}", + "selectingWillUpdate": "Seleccionar \"{{language}}\" actualizará tu idioma predeterminado" + } + }, + "ignoredFiles": { + "title": "Archivos Ignorados", + "subtitle": "Ver y administrar archivos que fueron intencionalmente ignorados durante el procesamiento", + "filters": { + "searchPlaceholder": "Buscar por nombre o ruta de archivo...", + "reason": "Razón", + "allReasons": "Todas las Razones", + "duplicateHash": "Hash Duplicado", + "tooLarge": "Demasiado Grande", + "unsupportedFormat": "Formato No Soportado", + "excluded": "Excluido", + "permissionDenied": "Permiso Denegado", + "corrupted": "Corrupto", + "other": "Otro" + }, + "table": { + "filename": "Nombre de Archivo", + "path": "Ruta", + "reason": "Razón", + "size": "Tamaño", + "ignoredAt": "Ignorado En" + }, + "empty": { + "title": "Sin Archivos Ignorados", + "subtitle": "No se han ignorado archivos. Todos los archivos procesados fueron manejados exitosamente." + }, + "noResults": { + "title": "No Se Encontraron Resultados", + "subtitle": "Ningún archivo ignorado coincide con tus filtros actuales. Intenta ajustar tu búsqueda o criterios de filtro." + }, + "pagination": { + "showing": "Mostrando {{start}}-{{end}} de {{total}} archivos ignorados" + }, + "errors": { + "loadFailed": "Falló la carga de archivos ignorados", + "tryAgain": "Por favor intenta de nuevo más tarde" + }, + "reasons": { + "duplicate_hash": "Hash Duplicado - El archivo ya existe en el sistema", + "file_too_large": "Archivo Demasiado Grande - Excede el límite máximo de tamaño", + "unsupported_format": "Formato No Soportado - Tipo de archivo no compatible", + "excluded_by_pattern": "Excluido - El archivo coincide con el patrón de exclusión", + "permission_denied": "Permiso Denegado - No se puede acceder al archivo", + "file_corrupted": "Archivo Corrupto - No se puede leer o procesar el archivo", + "unknown": "Desconocido - Razón no especificada" + } + }, + "debug": { + "title": "Depuración de Procesamiento de Documentos", + "subtitle": "Sube documentos o analiza los existentes para solucionar problemas de procesamiento OCR", + "errors": { + "enterDocumentId": "Por favor ingresa un ID de documento", + "documentNotFound": "Documento {{documentId}} no encontrado. Puede estar aún procesándose o haber sido movido a documentos fallidos.", + "fetchFailed": "Falló la obtención de información de depuración: {{message}}", + "debugError": "Error de Depuración" + }, + "upload": { + "title": "Subir Documento para Análisis de Depuración", + "description": "Sube un archivo PDF o imagen para analizar el proceso de procesamiento en tiempo real", + "selectFile": "Por favor selecciona un archivo para subir", + "uploading": "Subiendo archivo...", + "uploadedStartingOcr": "Documento subido exitosamente. Iniciando procesamiento OCR...", + "uploadFailed": "Falló la subida del documento", + "uploadFailedStatus": "Subida fallida", + "selectFileButton": "Seleccionar Archivo", + "uploadDebugButton": "Subir y Depurar", + "uploadingButton": "Subiendo...", + "uploadProgress": "Progreso de Subida: {{percent}}%", + "selected": "Seleccionado:", + "documentId": "ID de Documento:" + }, + "monitoring": { + "processingComplete": "Procesamiento {{status}}!", + "ocrInProgress": "Procesamiento OCR en progreso...", + "queuedForOcr": "Documento encolado para procesamiento OCR...", + "checkingStatus": "Verificando estado de procesamiento...", + "monitoringTimeout": "Monitoreo detenido (tiempo agotado)" + }, + "search": { + "title": "Depurar Documento Existente", + "description": "Ingresa un ID de documento para analizar el proceso de procesamiento de un documento existente", + "documentIdLabel": "ID de Documento", + "documentIdPlaceholder": "ej., 123e4567-e89b-12d3-a456-426614174000", + "debugButton": "Depurar" + }, + "tabs": { + "uploadAndDebug": "Subir y Depurar", + "searchExisting": "Buscar Existente", + "debugResults": "Resultados de Depuración" + }, + "actions": { + "debugAnalysis": "Análisis de Depuración", + "showDebugDetails": "Mostrar Detalles de Depuración", + "refreshStatus": "Actualizar Estado", + "viewDocument": "Ver Documento" + }, + "document": { + "title": "Documento: {{filename}}", + "status": "Estado: {{status}}", + "debugRunAt": "Depuración ejecutada en: {{timestamp}}" + }, + "pipeline": { + "title": "Pipeline de Procesamiento" + }, + "steps": { + "fileInformation": { + "title": "Información del Archivo", + "filename": "Nombre de Archivo:", + "original": "Original:", + "size": "Tamaño:", + "mimeType": "Tipo MIME:", + "fileExists": "Archivo Existe:", + "yes": "Sí", + "no": "No" + }, + "fileMetadata": { + "title": "Metadatos del Archivo", + "actualSize": "Tamaño Real:", + "isFile": "Es Archivo:", + "modified": "Modificado:", + "created": "Creado:", + "unknown": "Desconocido", + "notAvailable": "Metadatos del archivo no disponibles" + }, + "fileAnalysis": { + "title": "Análisis Detallado del Archivo", + "basicAnalysis": "Análisis Básico", + "fileType": "Tipo de Archivo:", + "size": "Tamaño:", + "readable": "Legible:", + "fileError": "Error de Archivo:", + "pdfAnalysis": "Análisis de PDF", + "validPdf": "PDF Válido:", + "pdfVersion": "Versión de PDF:", + "pages": "Páginas:", + "hasText": "Tiene Texto:", + "hasImages": "Tiene Imágenes:", + "encrypted": "Encriptado:", + "fontCount": "Conteo de Fuentes:", + "textLength": "Longitud de Texto:", + "chars": "caracteres", + "pdfTextExtractionError": "Error de Extracción de Texto PDF:", + "textPreview": "Vista Previa de Texto", + "fileContent": "Contenido del Archivo", + "noPreview": "No hay vista previa disponible para este tipo de archivo" + }, + "queueStatus": { + "title": "Estado de la Cola", + "userOcrEnabled": "OCR de Usuario Habilitado:", + "queueEntries": "Entradas de Cola:", + "queueHistory": "Historial de Cola", + "status": "Estado", + "priority": "Prioridad", + "created": "Creado", + "started": "Iniciado", + "completed": "Completado", + "attempts": "Intentos", + "worker": "Trabajador" + }, + "ocrResults": { + "title": "Resultados de OCR", + "textLength": "Longitud de Texto:", + "characters": "caracteres", + "confidence": "Confianza:", + "wordCount": "Conteo de Palabras:", + "processingTime": "Tiempo de Procesamiento:", + "completedAt": "Completado:", + "notCompleted": "No completado", + "processingDetails": "Detalles de Procesamiento", + "hasProcessedImage": "Tiene Imagen Procesada:", + "imageSize": "Tamaño de Imagen:", + "fileSize": "Tamaño de Archivo:", + "processingSteps": "Pasos de Procesamiento:", + "none": "Ninguno", + "processingParameters": "Parámetros de Procesamiento:" + }, + "qualityValidation": { + "title": "Umbrales de Calidad", + "minConfidence": "Confianza Mínima:", + "brightness": "Brillo:", + "contrast": "Contraste:", + "noise": "Ruido:", + "sharpness": "Nitidez:", + "actualValues": "Valores Reales", + "confidence": "Confianza:", + "wordCount": "Conteo de Palabras:", + "processedImageAvailable": "Imagen Procesada Disponible:", + "qualityChecks": "Verificaciones de Calidad" + } + }, + "failedDocument": { + "title": "Información de Documento Fallido", + "failureDetails": "Detalles del Fallo", + "failureReason": "Razón del Fallo:", + "failureStage": "Etapa del Fallo:", + "retryCount": "Conteo de Reintentos:", + "created": "Creado:", + "lastRetry": "Último Reintento:", + "failedOcrResults": "Resultados de OCR Fallidos", + "ocrTextLength": "Longitud de Texto OCR:", + "ocrConfidence": "Confianza de OCR:", + "wordCount": "Conteo de Palabras:", + "processingTime": "Tiempo de Procesamiento:", + "noOcrResults": "No hay resultados de OCR disponibles", + "errorMessage": "Mensaje de Error:", + "contentPreview": "Vista Previa de Contenido" + }, + "processingLogs": { + "title": "Registros Detallados de Procesamiento", + "description": "Historial completo de todos los intentos de procesamiento OCR para este documento", + "attempt": "Intento", + "status": "Estado", + "priority": "Prioridad", + "created": "Creado", + "started": "Iniciado", + "completed": "Completado", + "duration": "Duración", + "waitTime": "Tiempo de Espera", + "attempts": "Intentos", + "worker": "Trabajador", + "error": "Error" + }, + "fileAnalysisSummary": { + "title": "Resumen de Análisis de Archivo", + "fileProperties": "Propiedades del Archivo", + "fileType": "Tipo de Archivo:", + "size": "Tamaño:", + "readable": "Legible:", + "pdfProperties": "Propiedades de PDF", + "validPdf": "PDF Válido:", + "hasTextContent": "Tiene Contenido de Texto:", + "textLength": "Longitud de Texto:", + "pageCount": "Conteo de Páginas:", + "encrypted": "Encriptado:", + "pdfTextExtractionIssue": "Problema de Extracción de Texto PDF:" + }, + "processedImages": { + "title": "Imágenes Procesadas", + "originalDocument": "Documento Original", + "processedImage": "Imagen Procesada (Entrada OCR)", + "notAvailable": "Imagen procesada no disponible" + }, + "userSettings": { + "title": "Configuración de Usuario", + "ocrSettings": "Configuración de OCR", + "backgroundOcr": "OCR en Segundo Plano:", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "minConfidence": "Confianza Mínima:", + "maxFileSize": "Tamaño Máximo de Archivo:", + "qualityThresholds": "Umbrales de Calidad", + "brightness": "Brillo:", + "contrast": "Contraste:", + "noise": "Ruido:", + "sharpness": "Nitidez:" + }, + "preview": "Vista Previa" + }, + "watchFolder": { + "title": "Carpeta Vigilada", + "refreshAll": "Actualizar Todo", + "retryFailedJobs": "Reintentar {{count}} Trabajos Fallidos", + "requeuing": "Reintentando...", + "personalWatchDirectory": "Directorio de Vigilancia Personal", + "admin": "Admin", + "directoryStatus": "Estado del Directorio", + "directoryExists": "Directorio Existe", + "directoryMissing": "Directorio Faltante", + "watchStatus": "Estado de Vigilancia", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "yourPersonalWatchDirectory": "Tu Directorio de Vigilancia Personal", + "directoryNotExist": "Tu directorio de vigilancia personal aún no existe. Créalo para comenzar a subir archivos a tu carpeta dedicada.", + "creatingDirectory": "Creando Directorio...", + "createPersonalDirectory": "Crear Directorio Personal", + "unableToLoad": "No se puede cargar la información del directorio de vigilancia personal. Por favor intenta actualizar la página.", + "systemConfiguration": "Configuración del Sistema", + "globalWatchFolderConfiguration": "Configuración Global de Carpeta Vigilada", + "adminOnly": "Solo Admin", + "systemWideInfo": "Esta es la configuración de carpeta vigilada a nivel de sistema. Todos los usuarios pueden ver esta información.", + "watchedDirectory": "Directorio Vigilado", + "status": "Estado", + "active": "Activo", + "inactive": "Inactivo", + "watchStrategy": "Estrategia de Vigilancia", + "scanInterval": "Intervalo de Escaneo", + "seconds": "{{count}} segundos", + "maxFileAge": "Edad Máxima de Archivo", + "hours": "{{count}} horas", + "supportedFileTypes": "Tipos de Archivo Soportados", + "processingQueue": "Cola de Procesamiento", + "pending": "Pendiente", + "processing": "Procesando", + "failed": "Fallido", + "completedToday": "Completado Hoy", + "averageWaitTime": "Tiempo de Espera Promedio", + "oldestPendingItem": "Elemento Pendiente Más Antiguo", + "lastUpdated": "Última actualización: {{time}}", + "howWatchFolderWorks": "Cómo Funciona la Carpeta Vigilada", + "watchFolderDescription": "El sistema de carpeta vigilada monitorea automáticamente el directorio configurado para archivos nuevos y los procesa para OCR.", + "processingPipeline": "Proceso de Procesamiento:", + "pipelineSteps": { + "fileDetection": "Detección de Archivos: Los nuevos archivos se detectan usando vigilancia híbrida (inotify + sondeo)", + "validation": "Validación: Los archivos se verifican para formato compatible y límites de tamaño", + "deduplication": "Deduplicación: El sistema previene el procesamiento de archivos duplicados", + "storage": "Almacenamiento: Los archivos se mueven al sistema de almacenamiento de documentos", + "ocrQueue": "Cola OCR: Los documentos se encolan para procesamiento OCR con prioridad" + }, + "hybridStrategyInfo": "El sistema usa una estrategia de vigilancia híbrida que detecta automáticamente el tipo de sistema de archivos y elige el enfoque de monitoreo óptimo (inotify para sistemas de archivos locales, sondeo para montajes de red)." + }, + "register": { + "title": "Crea tu cuenta de Readur", + "fields": { + "username": "Nombre de usuario", + "email": "Correo electrónico", + "password": "Contraseña" + }, + "placeholders": { + "username": "Nombre de usuario", + "email": "Correo electrónico", + "password": "Contraseña" + }, + "actions": { + "signup": "Registrarse", + "creating": "Creando cuenta..." + }, + "links": { + "signin": "¿Ya tienes una cuenta? Inicia sesión" + }, + "errors": { + "failed": "Falló el registro" + } + }, + "sources": { + "title": "Fuentes de Documentos", + "subtitle": "Conecta y gestiona tus fuentes de documentos con sincronización inteligente", + "empty": { + "title": "No Hay Fuentes Configuradas", + "subtitle": "Conecta tu primera fuente de documentos para comenzar a sincronizar y procesar automáticamente tus archivos con OCR impulsado por IA.", + "addFirst": "Agregar Tu Primera Fuente" + }, + "actions": { + "addSource": "Agregar Fuente", + "editSource": "Editar Fuente", + "deleteSource": "Eliminar Fuente", + "testConnection": "Probar Conexión", + "testing": "Probando...", + "saveSource": "Guardar Fuente", + "createSource": "Crear Fuente", + "updateSource": "Actualizar Fuente", + "triggerSync": "Activar Sincronización", + "stopSync": "Detener Sincronización", + "viewIgnoredFiles": "Ver Archivos Ignorados", + "runValidation": "Ejecutar Verificación de Validación", + "quickSync": "Sincronización Rápida", + "deepScan": "Escaneo Profundo" + }, + "status": { + "autoRefreshing": "Actualizando automáticamente...", + "disabled": "Deshabilitado", + "syncing": "Sincronizando", + "error": "Error", + "idle": "Inactivo" + }, + "ocr": { + "pause": "Pausar OCR", + "resume": "Reanudar OCR", + "pausedSuccess": "Procesamiento OCR pausado exitosamente", + "pauseFailed": "Falló pausar el procesamiento OCR", + "resumedSuccess": "Procesamiento OCR reanudado exitosamente", + "resumeFailed": "Falló reanudar el procesamiento OCR" + }, + "stats": { + "documentsStored": "Documentos Almacenados", + "documentsStoredTooltip": "Número total de documentos actualmente almacenados de esta fuente", + "ocrProcessed": "OCR Procesado", + "ocrProcessedTooltip": "Número de documentos que han sido procesados exitosamente con OCR", + "ocrCount": "{{count}} con OCR", + "lastSync": "Última Sincronización", + "lastSyncTooltip": "Cuándo se sincronizó por última vez esta fuente", + "never": "Nunca", + "filesPending": "Archivos Pendientes", + "filesPendingTooltip": "Archivos descubiertos pero aún no procesados durante la sincronización", + "totalSize": "Tamaño Total", + "totalSizeTooltip": "Tamaño total de archivos descargados exitosamente de esta fuente" + }, + "types": { + "webdav": { + "name": "WebDAV", + "description": "Nextcloud, ownCloud y otros servidores WebDAV" + }, + "localFolder": { + "name": "Carpeta Local", + "description": "Monitorear directorios del sistema de archivos local" + }, + "s3": { + "name": "Compatible con S3", + "description": "AWS S3, MinIO y otro almacenamiento compatible con S3" + } + }, + "form": { + "sourceName": "Nombre de la Fuente", + "sourceType": "Tipo de Fuente", + "sourceEnabled": "Fuente Habilitada", + "sourceEnabledHelper": "Habilitar esta fuente para sincronización", + "sourceNamePlaceholder": "Mi Servidor de Documentos" + }, + "webdav": { + "title": "Configuración WebDAV", + "serverUrl": "URL del Servidor", + "username": "Nombre de Usuario", + "password": "Contraseña", + "serverType": "Tipo de Servidor", + "serverTypes": { + "nextcloud": "Nextcloud", + "nextcloudDesc": "Optimizado para servidores Nextcloud", + "owncloud": "ownCloud", + "owncloudDesc": "Optimizado para servidores ownCloud", + "generic": "WebDAV Genérico", + "genericDesc": "Cualquier servidor WebDAV estándar" + } + }, + "localFolder": { + "title": "Configuración de Carpeta Local", + "description": "Monitorear directorios del sistema de archivos local para nuevos documentos. Asegúrate de que la aplicación tenga acceso de lectura a las rutas especificadas.", + "recursive": "Escaneo Recursivo", + "recursiveDesc": "Escanear subdirectorios recursivamente", + "followSymlinks": "Seguir Enlaces Simbólicos", + "followSymlinksDesc": "Seguir enlaces simbólicos al escanear directorios" + }, + "s3": { + "title": "Configuración de Almacenamiento Compatible con S3", + "description": "Conecta a AWS S3, MinIO o cualquier servicio de almacenamiento compatible con S3. Para MinIO, proporciona la URL del endpoint de tu servidor.", + "bucketName": "Nombre del Bucket", + "region": "Región", + "accessKeyId": "ID de Clave de Acceso", + "secretAccessKey": "Clave de Acceso Secreta", + "endpointUrl": "URL del Endpoint (Opcional)", + "endpointUrlHelper": "Deja vacío para AWS S3, o proporciona un endpoint personalizado para MinIO/otro almacenamiento compatible con S3", + "objectPrefix": "Prefijo de Clave de Objeto (Opcional)", + "objectPrefixHelper": "Prefijo opcional para limitar el escaneo a claves de objeto específicas" + }, + "common": { + "folders": "Carpetas a Monitorear", + "foldersDesc": "Especifica las carpetas dentro de tu fuente para monitorear nuevos documentos", + "addFolder": "Agregar Ruta de Carpeta", + "extensions": "Extensiones de Archivo", + "extensionsDesc": "Tipos de archivo para sincronizar y procesar con OCR.", + "addExtension": "Agregar Extensión" + }, + "advanced": { + "title": "Configuración Avanzada", + "description": "Configurar sincronización automática y opciones avanzadas", + "enableAutoSync": "Habilitar Sincronización Automática", + "autoSyncDesc": "Sincronizar archivos automáticamente según un horario", + "autoSyncDescLocal": "Escanear automáticamente archivos nuevos según un horario", + "autoSyncDescS3": "Verificar automáticamente objetos nuevos según un horario", + "syncInterval": "Intervalo de Sincronización (minutos)", + "syncIntervalHelper": "Con qué frecuencia verificar archivos nuevos (15 min - 24 horas)", + "syncIntervalHelperLocal": "Con qué frecuencia escanear archivos nuevos (15 min - 24 horas)", + "syncIntervalHelperS3": "Con qué frecuencia verificar objetos nuevos (15 min - 24 horas)" + }, + "estimation": { + "title": "Estimación de Rastreo", + "description": "Estima cuántos archivos se procesarán y cuánto tiempo tomará.", + "estimate": "Estimar Rastreo", + "estimating": "Estimando...", + "analyzing": "Analizando carpetas y contando archivos...", + "results": "Resultados de Estimación", + "files": "Archivos Estimados", + "time": "Tiempo Estimado", + "size": "Tamaño Estimado" + }, + "dialog": { + "editTitle": "Editar Fuente", + "createTitle": "Crear Nueva Fuente", + "editSubtitle": "Actualiza la configuración de tu fuente", + "createSubtitle": "Conecta una nueva fuente de documentos" + }, + "sync": { + "quickSyncDesc": "Sincronización incremental rápida usando ETags. Solo procesa archivos nuevos o modificados.", + "deepScanDesc": "Reescaneo completo que reinicia las expectativas de ETag. Úsalo para solucionar problemas de sincronización." + }, + "validation": { + "healthy": "Saludable", + "warning": "Advertencia", + "critical": "Crítico", + "validating": "Validando", + "unknown": "Desconocido", + "statusUnknown": "Estado de validación desconocido", + "inProgress": "Verificación de validación en progreso", + "healthScore": "Puntuación de salud: {{score}}", + "healthScoreIssues": "Puntuación de salud: {{score}} - Problemas detectados", + "healthScoreCritical": "Puntuación de salud: {{score}} - Problemas críticos" + }, + "delete": { + "title": "Eliminar Fuente", + "message": "¿Estás seguro de que quieres eliminar esta fuente?", + "warning": "Esta acción no se puede deshacer. Todo el historial de sincronización y la configuración se perderán.", + "deleting": "Eliminando..." + }, + "messages": { + "createSuccess": "Fuente creada exitosamente", + "updateSuccess": "Fuente actualizada exitosamente", + "deleteSuccess": "Fuente eliminada exitosamente", + "syncStartSuccess": "Sincronización rápida iniciada exitosamente", + "deepScanSuccess": "Escaneo profundo iniciado exitosamente", + "syncStopSuccess": "Sincronización detenida exitosamente", + "connectionSuccess": "¡Conexión exitosa!", + "estimationSuccess": "Estimación de rastreo completada" + }, + "errors": { + "loadFailed": "Falló la carga de fuentes", + "saveFailed": "Falló guardar la fuente", + "deleteFailed": "Falló eliminar la fuente", + "testConnectionFailed": "Falló probar la conexión", + "syncStartFailed": "Falló iniciar la sincronización", + "syncStopFailed": "Falló detener la sincronización", + "deepScanFailed": "Falló iniciar el escaneo profundo", + "estimateFailed": "Falló estimar el rastreo", + "connectionFailed": "Conexión fallida", + "duplicateName": "Ya existe una fuente con este nombre. Por favor elige un nombre diferente.", + "invalidConfig": "La configuración de la fuente es inválida. Por favor verifica tus ajustes e intenta de nuevo.", + "authFailed": "Autenticación fallida. Por favor verifica tus credenciales.", + "connectionError": "No se puede conectar a la fuente. Por favor verifica tu red y configuración del servidor.", + "invalidPath": "Ruta inválida especificada. Por favor verifica las rutas de tus carpetas e intenta de nuevo.", + "notFound": "Fuente no encontrada. Puede haber sido eliminada ya.", + "syncInProgress": "No se puede eliminar la fuente mientras la sincronización está en progreso. Por favor detén la sincronización primero.", + "alreadySyncing": "La fuente ya está sincronizando. Por favor espera a que se complete la sincronización actual.", + "cannotConnect": "No se puede conectar a la fuente. Por favor verifica tu conexión e intenta de nuevo.", + "authFailedSource": "Autenticación fallida. Por favor verifica las credenciales de tu fuente.", + "sourceDeleted": "Fuente no encontrada. Puede haber sido eliminada.", + "connectionFailedUrl": "Conexión fallida. Por favor verifica la URL de tu servidor y la conectividad de red.", + "authFailedCredentials": "Autenticación fallida. Por favor verifica tu nombre de usuario y contraseña.", + "invalidFolderPath": "Ruta inválida especificada. Por favor verifica las rutas de tus carpetas.", + "invalidSettings": "La configuración es inválida. Por favor revisa tus ajustes.", + "timeout": "Tiempo de conexión agotado. Por favor verifica tu red e intenta de nuevo.", + "deepScanWebdavOnly": "El escaneo profundo solo está soportado para fuentes WebDAV", + "notSyncing": "La fuente no está actualmente sincronizando" + }, + "labels": { + "recommended": "Recomendado", + "notAvailable": "No Disponible" + } + } +} \ No newline at end of file diff --git a/frontend/src/components/Auth/Login.tsx b/frontend/src/components/Auth/Login.tsx index 65255ab..dff0736 100644 --- a/frontend/src/components/Auth/Login.tsx +++ b/frontend/src/components/Auth/Login.tsx @@ -27,6 +27,7 @@ import { useNavigate } from 'react-router-dom'; import { useTheme } from '../../contexts/ThemeContext'; import { useTheme as useMuiTheme } from '@mui/material/styles'; import { api, ErrorHelper, ErrorCodes } from '../../services/api'; +import { useTranslation } from 'react-i18next'; interface LoginFormData { username: string; @@ -42,7 +43,8 @@ const Login: React.FC = () => { const navigate = useNavigate(); const { mode } = useTheme(); const theme = useMuiTheme(); - + const { t } = useTranslation(); + const { register, handleSubmit, @@ -62,20 +64,20 @@ const Login: React.FC = () => { // Handle specific login errors if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_INVALID_CREDENTIALS)) { - setError('Invalid username or password. Please check your credentials and try again.'); + setError(t('auth.errors.invalidCredentials')); } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_ACCOUNT_DISABLED)) { - setError('Your account has been disabled. Please contact an administrator for assistance.'); + setError(t('auth.errors.accountDisabled')); } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_NOT_FOUND)) { - setError('No account found with this username. Please check your username or contact support.'); - } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) || + setError(t('auth.errors.userNotFound')); + } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(err, ErrorCodes.USER_TOKEN_EXPIRED)) { - setError('Your session has expired. Please try logging in again.'); + setError(t('auth.errors.sessionExpired')); } else if (errorInfo.category === 'network') { - setError('Network error. Please check your connection and try again.'); + setError(t('auth.errors.networkError')); } else if (errorInfo.category === 'server') { - setError('Server error. Please try again later or contact support if the problem persists.'); + setError(t('auth.errors.serverError')); } else { - setError(errorInfo.message || 'Failed to log in. Please check your credentials.'); + setError(errorInfo.message || t('auth.errors.loginFailed')); } } finally { setLoading(false); @@ -99,13 +101,13 @@ const Login: React.FC = () => { // Handle specific OIDC errors if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_OIDC_AUTH_FAILED)) { - setError('OIDC authentication failed. Please check with your administrator.'); + setError(t('auth.errors.oidcAuthFailed')); } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_AUTH_PROVIDER_NOT_CONFIGURED)) { - setError('OIDC is not configured on this server. Please use username/password login.'); + setError(t('auth.errors.oidcNotConfigured')); } else if (errorInfo.category === 'network') { - setError('Network error. Please check your connection and try again.'); + setError(t('auth.errors.networkError')); } else { - setError(errorInfo.message || 'Failed to initiate OIDC login. Please try again.'); + setError(errorInfo.message || t('auth.errors.oidcInitFailed')); } setOidcLoading(false); } @@ -161,18 +163,18 @@ const Login: React.FC = () => { : '0 4px 12px rgba(0, 0, 0, 0.5)', }} > - Welcome to Readur + {t('common.welcome', { appName: t('common.appName') })} - Your intelligent document management platform + {t('auth.intelligentDocumentPlatform')} @@ -204,7 +206,7 @@ const Login: React.FC = () => { color: 'text.primary', }} > - Sign in to your account + {t('auth.signInToAccount')} {error && ( @@ -216,10 +218,10 @@ const Login: React.FC = () => { { { }, }} > - {loading ? 'Signing in...' : 'Sign in'} + {loading ? t('auth.signingIn') : t('auth.signIn')} { }, }} > - - or + {t('common.or')} @@ -348,7 +350,7 @@ const Login: React.FC = () => { }, }} > - {oidcLoading ? 'Redirecting...' : 'Sign in with OIDC'} + {oidcLoading ? t('auth.redirecting') : t('auth.signInWithOIDC')} @@ -363,12 +365,12 @@ const Login: React.FC = () => { - © 2026 Readur. Powered by advanced OCR and AI technology. + {t('common.copyright')} diff --git a/frontend/src/components/Dashboard/Dashboard.tsx b/frontend/src/components/Dashboard/Dashboard.tsx index 0a95df9..e2fd8e5 100644 --- a/frontend/src/components/Dashboard/Dashboard.tsx +++ b/frontend/src/components/Dashboard/Dashboard.tsx @@ -39,6 +39,7 @@ import { import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import api, { documentService } from '../../services/api'; +import { useTranslation } from 'react-i18next'; interface Document { id: string; @@ -214,7 +215,8 @@ const StatsCard: React.FC = ({ title, value, subtitle, icon: Ico const RecentDocuments: React.FC = ({ documents = [] }) => { const navigate = useNavigate(); const theme = useTheme(); - + const { t } = useTranslation(); + // Ensure documents is always an array const safeDocuments = Array.isArray(documents) ? documents : []; @@ -256,7 +258,7 @@ const RecentDocuments: React.FC = ({ documents = [] }) => }}> - = ({ documents = [] }) => WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', }}> - Recent Documents + {t('dashboard.recentDocuments.title')} navigate('/documents')} sx={{ cursor: 'pointer', @@ -302,15 +304,15 @@ const RecentDocuments: React.FC = ({ documents = [] }) => }}> - - No documents yet + {t('dashboard.recentDocuments.noDocuments')} - Upload your first document to get started + {t('dashboard.recentDocuments.uploadFirst')} ) : ( @@ -403,25 +405,26 @@ const RecentDocuments: React.FC = ({ documents = [] }) => const QuickActions: React.FC = () => { const navigate = useNavigate(); const theme = useTheme(); - + const { t } = useTranslation(); + const actions: QuickAction[] = [ { - title: 'Upload Documents', - description: 'Add new files for OCR processing', + title: t('dashboard.quickActions.upload.title'), + description: t('dashboard.quickActions.upload.description'), icon: UploadIcon, color: '#6366f1', path: '/upload', }, { - title: 'Search Library', - description: 'Find documents by content or metadata', + title: t('dashboard.quickActions.search.title'), + description: t('dashboard.quickActions.search.description'), icon: SearchIcon, color: '#10b981', path: '/search', }, { - title: 'Browse Documents', - description: 'View and manage your document library', + title: t('dashboard.quickActions.browse.title'), + description: t('dashboard.quickActions.browse.description'), icon: SearchableIcon, color: '#f59e0b', path: '/documents', @@ -440,7 +443,7 @@ const QuickActions: React.FC = () => { borderRadius: 3, }}> - { WebkitTextFillColor: 'transparent', mb: 3, }}> - Quick Actions + {t('dashboard.quickActions.title')} {actions.map((action) => ( @@ -523,6 +526,7 @@ const Dashboard: React.FC = () => { const theme = useTheme(); const navigate = useNavigate(); const { user } = useAuth(); + const { t } = useTranslation(); const [documents, setDocuments] = useState([]); const [stats, setStats] = useState({ totalDocuments: 0, @@ -621,8 +625,8 @@ const Dashboard: React.FC = () => { {/* Welcome Header */} - { WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', }}> - Welcome back, {user?.username}! 👋 + {t('common.welcomeBack', { username: user?.username })} - Here's what's happening with your documents today. + {t('dashboard.greeting')} @@ -647,42 +651,52 @@ const Dashboard: React.FC = () => { 0 ? `${stats.totalDocuments} total` : 'No documents yet'} + trend={stats.totalDocuments > 0 + ? t('dashboard.stats.totalDocuments.trend', { count: stats.totalDocuments }) + : t('dashboard.stats.totalDocuments.trendEmpty')} /> 0 ? `${formatBytes(stats.totalSize)} used` : 'No storage used'} + trend={stats.totalSize > 0 + ? t('dashboard.stats.storageUsed.trend', { size: formatBytes(stats.totalSize) }) + : t('dashboard.stats.storageUsed.trendEmpty')} /> 0 ? `${Math.round((stats.ocrProcessed / stats.totalDocuments) * 100)}% completion` : '0% completion'} + trend={stats.totalDocuments > 0 + ? t('dashboard.stats.ocrProcessed.trend', { + percentage: Math.round((stats.ocrProcessed / stats.totalDocuments) * 100) + }) + : t('dashboard.stats.ocrProcessed.trendEmpty')} /> 0 ? `${stats.searchablePages} indexed` : 'Nothing indexed yet'} + trend={stats.searchablePages > 0 + ? t('dashboard.stats.searchable.trend', { count: stats.searchablePages }) + : t('dashboard.stats.searchable.trendEmpty')} /> diff --git a/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx b/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx index fb77792..509d2a8 100644 --- a/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx +++ b/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx @@ -33,6 +33,7 @@ import { AccessTime as TimeIcon, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { documentService, SearchRequest, EnhancedDocument, SearchResponse } from '../../services/api'; interface GlobalSearchBarProps { @@ -43,6 +44,7 @@ interface GlobalSearchBarProps { const GlobalSearchBar: React.FC = ({ sx, ...props }) => { const navigate = useNavigate(); const theme = useTheme(); + const { t } = useTranslation(); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); @@ -333,7 +335,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { = ({ sx, ...props }) => { }}> - - {isTyping ? 'Searching as you type...' : 'Searching...'} + {isTyping ? t('search.searchingAsYouType') : t('search.searching')} @@ -510,8 +512,8 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { )} {!loading && !isTyping && query && results.length === 0 && ( - @@ -521,7 +523,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { letterSpacing: '0.025em', mb: 1, }}> - No documents found for "{query}" + {t('search.noDocumentsFound', { query })} = ({ sx, ...props }) => { mb: 2, display: 'block', }}> - Press Enter to search with advanced options + {t('search.pressEnterAdvanced')} {/* Smart suggestions for no results */} @@ -544,7 +546,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { mb: 1.5, display: 'block', }}> - Try these suggestions: + {t('search.trySuggestions')} {suggestions.map((suggestion, index) => ( @@ -593,7 +595,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { textTransform: 'uppercase', fontSize: '0.7rem', }}> - Quick Results + {t('search.quickResults')} = ({ sx, ...props }) => { fontWeight: 600, fontSize: '0.7rem', }}> - {results.length} found + {t('search.resultsCount', { count: results.length })} @@ -763,7 +765,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { transition: 'color 0.2s ease-in-out', }} > - View all results for "{query}" + {t('search.viewAllResults', { query })} @@ -785,7 +787,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { textTransform: 'uppercase', fontSize: '0.7rem', }}> - Recent Searches + {t('search.recentSearches')} @@ -846,7 +848,7 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { letterSpacing: '0.025em', mb: 1, }}> - Start typing to search documents + {t('search.startTyping')} = ({ sx, ...props }) => { mb: 2, display: 'block', }}> - Popular searches: + {t('search.popularSearches')} {popularSearches.slice(0, 3).map((search, index) => ( diff --git a/frontend/src/components/Labels/LabelCreateDialog.tsx b/frontend/src/components/Labels/LabelCreateDialog.tsx index 07086b1..e7e0466 100644 --- a/frontend/src/components/Labels/LabelCreateDialog.tsx +++ b/frontend/src/components/Labels/LabelCreateDialog.tsx @@ -31,6 +31,7 @@ import { Assignment as AssignmentIcon, Schedule as ScheduleIcon, } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import Label, { type LabelData } from './Label'; interface LabelCreateDialogProps { @@ -80,6 +81,7 @@ const LabelCreateDialog: React.FC = ({ prefilledName = '', editingLabel }) => { + const { t } = useTranslation(); const [formData, setFormData] = useState({ name: '', description: '', @@ -113,9 +115,9 @@ const LabelCreateDialog: React.FC = ({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + if (!formData.name.trim()) { - setNameError('Name is required'); + setNameError(t('labels.errors.invalidName')); return; } @@ -169,15 +171,15 @@ const LabelCreateDialog: React.FC = ({ }} > - {editingLabel ? 'Edit Label' : 'Create New Label'} + {editingLabel ? t('labels.create.editTitle') : t('labels.create.title')} - + {/* Name Field */} { setFormData({ ...formData, name: e.target.value }); @@ -195,7 +197,7 @@ const LabelCreateDialog: React.FC = ({ {/* Description Field */} setFormData({ ...formData, description: e.target.value })} fullWidth @@ -208,7 +210,7 @@ const LabelCreateDialog: React.FC = ({ {/* Color Selection */} - Color + {t('labels.create.colorLabel')} {predefinedColors.map((color) => ( @@ -231,7 +233,7 @@ const LabelCreateDialog: React.FC = ({ ))} setFormData({ ...formData, color: e.target.value })} size="small" @@ -257,7 +259,7 @@ const LabelCreateDialog: React.FC = ({ {/* Icon Selection */} - Icon (optional) + {t('labels.create.iconLabel')} = ({ backgroundColor: !formData.icon ? 'action.selected' : 'transparent', }} > - None + {t('labels.create.iconNone')} {availableIcons.map((iconData) => { const IconComponent = iconData.icon; @@ -295,7 +297,7 @@ const LabelCreateDialog: React.FC = ({ {/* Preview */} - Preview + {t('labels.create.previewLabel')} @@ -309,14 +311,14 @@ const LabelCreateDialog: React.FC = ({ diff --git a/frontend/src/components/Labels/LabelSelector.tsx b/frontend/src/components/Labels/LabelSelector.tsx index f93b729..46286fc 100644 --- a/frontend/src/components/Labels/LabelSelector.tsx +++ b/frontend/src/components/Labels/LabelSelector.tsx @@ -16,6 +16,7 @@ import { Button, } from '@mui/material'; import { Add as AddIcon, Edit as EditIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import Label, { type LabelData } from './Label'; import LabelCreateDialog from './LabelCreateDialog'; @@ -44,6 +45,7 @@ const LabelSelector: React.FC = ({ showCreateButton = true, maxTags }) => { + const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [prefilledName, setPrefilledName] = useState(''); @@ -116,7 +118,7 @@ const LabelSelector: React.FC = ({ inputValue={inputValue} onInputChange={(event, newInputValue) => setInputValue(newInputValue)} options={filteredOptions} - groupBy={(option: LabelData) => option.is_system ? 'System Labels' : 'My Labels'} + groupBy={(option: LabelData) => option.is_system ? t('labels.selector.systemLabels') : t('labels.selector.myLabels')} getOptionLabel={(option: LabelData) => option.name} isOptionEqualToValue={(option: LabelData, value: LabelData) => option.id === value.id} disabled={disabled} @@ -130,7 +132,7 @@ const LabelSelector: React.FC = ({ endAdornment: ( <> {canCreateNew && ( - + = ({ - Create "{inputValue}" + {t('labels.selector.createLabel', { name: inputValue })} @@ -227,7 +229,7 @@ const LabelSelector: React.FC = ({ canCreateNew ? ( - No labels found + {t('labels.selector.noLabelsFound')} ) : ( - `No labels match "${inputValue}"` + t('labels.selector.noLabelsMatch', { query: inputValue }) ) - ) : 'No labels available' + ) : t('labels.selector.noLabelsAvailable') } filterOptions={(options, { inputValue }) => { if (!inputValue) return options; diff --git a/frontend/src/components/LanguageSwitcher/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher/LanguageSwitcher.tsx new file mode 100644 index 0000000..792a921 --- /dev/null +++ b/frontend/src/components/LanguageSwitcher/LanguageSwitcher.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import { + IconButton, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Box, + useTheme, +} from '@mui/material'; +import { + Language as LanguageIcon, + Check as CheckIcon, +} from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import { supportedLanguages, SupportedLanguage } from '../../i18n/types'; + +interface LanguageSwitcherProps { + size?: 'small' | 'medium' | 'large'; + color?: 'inherit' | 'default' | 'primary' | 'secondary'; +} + +const LanguageSwitcher: React.FC = ({ + size = 'medium', + color = 'inherit' +}) => { + const { i18n } = useTranslation(); + const theme = useTheme(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLanguageChange = (language: SupportedLanguage) => { + i18n.changeLanguage(language); + handleClose(); + }; + + const currentLanguage = i18n.language as SupportedLanguage; + + return ( + <> + + + + + {Object.entries(supportedLanguages).map(([code, name]) => ( + handleLanguageChange(code as SupportedLanguage)} + selected={currentLanguage === code} + sx={{ + borderRadius: 2, + mx: 1, + my: 0.5, + transition: 'all 0.2s ease-in-out', + '&:hover': { + background: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)', + }, + '&.Mui-selected': { + background: 'linear-gradient(135deg, rgba(99,102,241,0.15) 0%, rgba(139,92,246,0.15) 100%)', + '&:hover': { + background: 'linear-gradient(135deg, rgba(99,102,241,0.2) 0%, rgba(139,92,246,0.2) 100%)', + }, + }, + }} + > + + + {currentLanguage === code && ( + + + + )} + + + ))} + + + ); +}; + +export default LanguageSwitcher; diff --git a/frontend/src/components/LanguageSwitcher/index.ts b/frontend/src/components/LanguageSwitcher/index.ts new file mode 100644 index 0000000..31505c0 --- /dev/null +++ b/frontend/src/components/LanguageSwitcher/index.ts @@ -0,0 +1 @@ +export { default } from './LanguageSwitcher'; diff --git a/frontend/src/components/Layout/AppLayout.tsx b/frontend/src/components/Layout/AppLayout.tsx index f3b35d6..03a2a55 100644 --- a/frontend/src/components/Layout/AppLayout.tsx +++ b/frontend/src/components/Layout/AppLayout.tsx @@ -45,11 +45,13 @@ import { useNotifications } from '../../contexts/NotificationContext'; import GlobalSearchBar from '../GlobalSearchBar'; import ThemeToggle from '../ThemeToggle/ThemeToggle'; import NotificationPanel from '../Notifications/NotificationPanel'; +import LanguageSwitcher from '../LanguageSwitcher'; +import { useTranslation } from 'react-i18next'; const drawerWidth = 280; interface NavigationItem { - text: string; + textKey: string; icon: React.ComponentType; path: string; } @@ -63,16 +65,16 @@ interface User { email?: string; } -const navigationItems: NavigationItem[] = [ - { text: 'Dashboard', icon: DashboardIcon, path: '/dashboard' }, - { text: 'Upload', icon: UploadIcon, path: '/upload' }, - { text: 'Documents', icon: DocumentIcon, path: '/documents' }, - { text: 'Search', icon: SearchIcon, path: '/search' }, - { text: 'Labels', icon: LabelIcon, path: '/labels' }, - { text: 'Sources', icon: StorageIcon, path: '/sources' }, - { text: 'Watch Folder', icon: FolderIcon, path: '/watch' }, - { text: 'Document Management', icon: ManageIcon, path: '/documents/management' }, - { text: 'Ignored Files', icon: BlockIcon, path: '/ignored-files' }, +const getNavigationItems = (t: (key: string) => string): NavigationItem[] => [ + { textKey: 'navigation.dashboard', icon: DashboardIcon, path: '/dashboard' }, + { textKey: 'navigation.upload', icon: UploadIcon, path: '/upload' }, + { textKey: 'navigation.documents', icon: DocumentIcon, path: '/documents' }, + { textKey: 'navigation.search', icon: SearchIcon, path: '/search' }, + { textKey: 'navigation.labels', icon: LabelIcon, path: '/labels' }, + { textKey: 'navigation.sources', icon: StorageIcon, path: '/sources' }, + { textKey: 'navigation.watchFolder', icon: FolderIcon, path: '/watch' }, + { textKey: 'navigation.documentManagement', icon: ManageIcon, path: '/documents/management' }, + { textKey: 'navigation.ignoredFiles', icon: BlockIcon, path: '/ignored-files' }, ]; const AppLayout: React.FC = ({ children }) => { @@ -85,6 +87,9 @@ const AppLayout: React.FC = ({ children }) => { const location = useLocation(); const { user, logout } = useAuth(); const { unreadCount } = useNotifications(); + const { t } = useTranslation(); + + const navigationItems = getNavigationItems(t); const handleDrawerToggle = (): void => { setMobileOpen(!mobileOpen); @@ -182,8 +187,8 @@ const AppLayout: React.FC = ({ children }) => { - = ({ children }) => { WebkitTextFillColor: 'transparent', letterSpacing: '-0.025em', }}> - Readur + {t('common.appName')} - - AI Document Platform + {t('common.appTagline')} @@ -258,7 +263,7 @@ const AppLayout: React.FC = ({ children }) => { const Icon = item.icon; return ( - + navigate(item.path)} sx={{ @@ -315,8 +320,8 @@ const AppLayout: React.FC = ({ children }) => { - = ({ children }) => { - = ({ children }) => { WebkitTextFillColor: 'transparent', letterSpacing: '-0.025em', }}> - {navigationItems.find(item => item.path === location.pathname)?.text || 'Dashboard'} + {navigationItems.find(item => item.path === location.pathname)?.textKey + ? t(navigationItems.find(item => item.path === location.pathname)!.textKey) + : t('navigation.dashboard')} {/* Global Search Bar */} @@ -488,8 +495,29 @@ const AppLayout: React.FC = ({ children }) => { + {/* Language Switcher */} + + + + {/* Theme Toggle */} - = ({ children }) => { anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} > navigate('/profile')}> - Profile + {t('auth.profile')} navigate('/settings')}> - Settings + {t('settings.title')} navigate('/debug')}> - Debug + {t('settings.debug')} window.open('/swagger-ui', '_blank')}> - API Documentation + {t('settings.apiDocumentation')} - Logout + {t('auth.logout')} diff --git a/frontend/src/components/Notifications/NotificationPanel.tsx b/frontend/src/components/Notifications/NotificationPanel.tsx index 5b8958b..46ec41d 100644 --- a/frontend/src/components/Notifications/NotificationPanel.tsx +++ b/frontend/src/components/Notifications/NotificationPanel.tsx @@ -23,6 +23,7 @@ import { Delete as DeleteIcon, DoneAll as DoneAllIcon, } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import { useNotifications } from '../../contexts/NotificationContext'; import { NotificationType } from '../../types/notification'; import { formatDistanceToNow } from 'date-fns'; @@ -34,6 +35,7 @@ interface NotificationPanelProps { const NotificationPanel: React.FC = ({ anchorEl, onClose }) => { const theme = useTheme(); + const { t } = useTranslation(); const { notifications, unreadCount, markAsRead, markAllAsRead, clearNotification, clearAll } = useNotifications(); const getIcon = (type: NotificationType) => { @@ -88,7 +90,7 @@ const NotificationPanel: React.FC = ({ anchorEl, onClose > - Notifications + {t('notifications.title')} {unreadCount > 0 && ( = ({ anchorEl, onClose @@ -116,7 +118,7 @@ const NotificationPanel: React.FC = ({ anchorEl, onClose @@ -139,7 +141,7 @@ const NotificationPanel: React.FC = ({ anchorEl, onClose color: 'text.secondary', }} > - No notifications + {t('notifications.noNotifications')} ) : ( diff --git a/frontend/src/components/OcrLanguageSelector/OcrLanguageSelector.tsx b/frontend/src/components/OcrLanguageSelector/OcrLanguageSelector.tsx index c90da84..a3e4333 100644 --- a/frontend/src/components/OcrLanguageSelector/OcrLanguageSelector.tsx +++ b/frontend/src/components/OcrLanguageSelector/OcrLanguageSelector.tsx @@ -12,6 +12,7 @@ import { SelectChangeEvent, } from '@mui/material'; import { Language as LanguageIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import { ocrService, LanguageInfo } from '../../services/api'; interface OcrLanguageSelectorProps { @@ -37,6 +38,7 @@ const OcrLanguageSelector: React.FC = ({ required = false, helperText, }) => { + const { t } = useTranslation(); const [languages, setLanguages] = useState([]); const [currentUserLanguage, setCurrentUserLanguage] = useState('eng'); const [loading, setLoading] = useState(true); @@ -88,7 +90,7 @@ const OcrLanguageSelector: React.FC = ({ - Loading languages... + {t('ocr.languageSelector.loading')} @@ -98,16 +100,16 @@ const OcrLanguageSelector: React.FC = ({ if (error) { return ( - - Retry + {t('ocr.languageSelector.retry')} } > @@ -116,7 +118,7 @@ const OcrLanguageSelector: React.FC = ({ {label} @@ -143,10 +145,10 @@ const OcrLanguageSelector: React.FC = ({ {language.code} {showCurrentIndicator && language.code === currentUserLanguage && ( - @@ -162,12 +164,16 @@ const OcrLanguageSelector: React.FC = ({ )} - + + {showCurrentIndicator && languages.length > 0 && ( - {languages.length} language{languages.length !== 1 ? 's' : ''} available + {t('ocr.languageSelector.languagesAvailable', { + count: languages.length, + plural: languages.length !== 1 ? 's' : '' + })} {value && value !== currentUserLanguage && ( - • Selecting "{getLanguageDisplay(value)}" will update your default language + • {t('ocr.languageSelector.selectingWillUpdate', { language: getLanguageDisplay(value) })} )} )} diff --git a/frontend/src/components/Register.tsx b/frontend/src/components/Register.tsx index c965826..c9cf729 100644 --- a/frontend/src/components/Register.tsx +++ b/frontend/src/components/Register.tsx @@ -1,8 +1,10 @@ import React, { useState } from 'react' import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { useAuth } from '../contexts/AuthContext' function Register() { + const { t } = useTranslation() const [username, setUsername] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') @@ -18,7 +20,7 @@ function Register() { try { await register(username, email, password) } catch (err: any) { - setError(err.response?.data?.message || 'Failed to register') + setError(err.response?.data?.message || t('register.errors.failed')) } finally { setLoading(false) } @@ -29,7 +31,7 @@ function Register() {

- Create your Readur account + {t('register.title')}

@@ -40,7 +42,7 @@ function Register() { )}
setUsername(e.target.value)} />
setEmail(e.target.value)} />
setPassword(e.target.value)} /> @@ -89,12 +91,12 @@ function Register() { disabled={loading} className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50" > - {loading ? 'Creating account...' : 'Sign up'} + {loading ? t('register.actions.creating') : t('register.actions.signup')}
- Already have an account? Sign in + {t('register.links.signin')}
diff --git a/frontend/src/components/Upload/UploadZone.tsx b/frontend/src/components/Upload/UploadZone.tsx index 99ef4fe..20384ef 100644 --- a/frontend/src/components/Upload/UploadZone.tsx +++ b/frontend/src/components/Upload/UploadZone.tsx @@ -29,6 +29,7 @@ import { } from '@mui/icons-material'; import { useDropzone, FileRejection, DropzoneOptions } from 'react-dropzone'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { api, ErrorHelper, ErrorCodes } from '../../services/api'; import { useNotifications } from '../../contexts/NotificationContext'; import LabelSelector from '../Labels/LabelSelector'; @@ -65,6 +66,7 @@ type FileStatus = 'pending' | 'uploading' | 'success' | 'error' | 'timeout' | 'c const UploadZone: React.FC = ({ onUploadComplete }) => { const theme = useTheme(); const navigate = useNavigate(); + const { t } = useTranslation(); const { addBatchNotification } = useNotifications(); const [files, setFiles] = useState([]); const [uploading, setUploading] = useState(false); @@ -97,13 +99,13 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); // Handle specific label fetch errors - if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - setError('Your session has expired. Please refresh the page and log in again.'); + setError(t('upload.errors.sessionExpired')); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - setError('You do not have permission to access labels.'); + setError(t('upload.errors.labelPermissionDenied')); } else if (errorInfo.category === 'network') { - setError('Network error loading labels. Please check your connection.'); + setError(t('upload.errors.labelNetworkError')); } else { // Don't show error for label loading failures as it's not critical console.warn('Label loading failed:', errorInfo.message); @@ -126,15 +128,15 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { // Handle specific label creation errors if (ErrorHelper.isErrorCode(error, ErrorCodes.LABEL_DUPLICATE_NAME)) { - throw new Error('A label with this name already exists. Please choose a different name.'); + throw new Error(t('labels.errors.duplicateName')); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.LABEL_INVALID_NAME)) { - throw new Error('Label name contains invalid characters. Please use only letters, numbers, and basic punctuation.'); + throw new Error(t('labels.errors.invalidName')); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.LABEL_INVALID_COLOR)) { - throw new Error('Invalid color format. Please use a valid hex color like #0969da.'); + throw new Error(t('labels.errors.invalidColor')); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.LABEL_MAX_LABELS_REACHED)) { - throw new Error('Maximum number of labels reached. Please delete some labels before creating new ones.'); + throw new Error(t('labels.errors.maxLabelsReached')); } else { - throw new Error(errorInfo.message || 'Failed to create label'); + throw new Error(errorInfo.message || t('labels.errors.loadFailed')); } } }; @@ -261,22 +263,22 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { // Handle specific document upload errors if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_TOO_LARGE)) { - errorMessage = 'File is too large. Maximum size is 50MB.'; + errorMessage = t('upload.errors.fileTooLarge'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_INVALID_FORMAT)) { - errorMessage = 'Unsupported file format. Please use PDF, images, text, or Word documents.'; + errorMessage = t('upload.errors.unsupportedFormat'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_OCR_FAILED)) { - errorMessage = 'Failed to process document. Please try again or contact support.'; - } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + errorMessage = t('upload.errors.processingFailed'); + } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - errorMessage = 'Session expired. Please refresh and log in again.'; + errorMessage = t('upload.errors.sessionExpired'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - errorMessage = 'You do not have permission to upload documents.'; + errorMessage = t('upload.errors.permissionDenied'); } else if (errorInfo.category === 'network') { - errorMessage = 'Network error. Please check your connection and try again.'; + errorMessage = t('upload.errors.networkError'); } else if (errorInfo.category === 'server') { - errorMessage = 'Server error. Please try again later.'; + errorMessage = t('upload.errors.serverError'); } else { - errorMessage = errorInfo.message || 'Upload failed'; + errorMessage = errorInfo.message || t('common.status.failed'); } setFiles(prev => prev.map(f => @@ -471,33 +473,33 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { /> - {isDragActive ? 'Drop files here' : 'Drag & drop files here'} + {isDragActive ? t('upload.dropzone.dropHere') : t('upload.dropzone.dragDrop')} - + - or click to browse your computer + {t('upload.dropzone.browse')} - - - + - - - - + + + + - + - Maximum file size: 50MB per file + {t('upload.dropzone.maxFileSize')} @@ -514,10 +516,10 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { - 🌐 OCR Language Settings + {t('upload.languageSettings.title')} - Select languages for optimal OCR text recognition + {t('upload.languageSettings.description')} div': { width: '100%' } }}> = ({ onUploadComplete }) => { - 📋 Label Assignment + {t('upload.labelAssignment.title')} - Select labels to automatically assign to all uploaded documents + {t('upload.labelAssignment.description')} {selectedLabels.length > 0 && ( - These labels will be applied to all uploaded documents + {t('upload.labelAssignment.helperText')} )} @@ -562,7 +564,7 @@ const UploadZone: React.FC = ({ onUploadComplete }) => { - Files ({files.length}) + {t('upload.fileList.title', { count: files.length })} diff --git a/frontend/src/i18n/config.ts b/frontend/src/i18n/config.ts new file mode 100644 index 0000000..2f5f612 --- /dev/null +++ b/frontend/src/i18n/config.ts @@ -0,0 +1,33 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: import.meta.env.DEV, + + interpolation: { + escapeValue: false, + }, + + backend: { + loadPath: '/locales/{{lng}}/translation.json', + }, + + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + lookupLocalStorage: 'i18nextLng', + }, + + react: { + useSuspense: true, + }, + }); + +export default i18n; diff --git a/frontend/src/i18n/types.ts b/frontend/src/i18n/types.ts new file mode 100644 index 0000000..164054f --- /dev/null +++ b/frontend/src/i18n/types.ts @@ -0,0 +1,8 @@ +export const supportedLanguages = { + en: 'English', + es: 'Español', +} as const; + +export type SupportedLanguage = keyof typeof supportedLanguages; + +export const defaultLanguage: SupportedLanguage = 'en'; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 13f1c7c..67b5573 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,16 +1,19 @@ -import React from 'react' +import React, { Suspense } from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './App' import './index.css' import { AuthProvider } from './contexts/AuthContext' +import './i18n/config' ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + Loading...
}> + + + + + + , ) \ No newline at end of file diff --git a/frontend/src/pages/DebugPage.tsx b/frontend/src/pages/DebugPage.tsx index bb79870..ae9beca 100644 --- a/frontend/src/pages/DebugPage.tsx +++ b/frontend/src/pages/DebugPage.tsx @@ -43,6 +43,7 @@ import { Refresh as RefreshIcon, Visibility as PreviewIcon, } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import { api } from '../services/api'; interface DebugStep { @@ -77,12 +78,13 @@ interface DebugInfo { } const DebugPage: React.FC = () => { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(0); const [documentId, setDocumentId] = useState(''); const [debugInfo, setDebugInfo] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - + // Upload functionality const [selectedFile, setSelectedFile] = useState(null); const [uploading, setUploading] = useState(false); @@ -126,7 +128,7 @@ const DebugPage: React.FC = () => { const fetchDebugInfo = useCallback(async (docId?: string, retryCount = 0) => { const targetDocId = docId || documentId; if (!targetDocId.trim()) { - setError('Please enter a document ID'); + setError(t('debug.errors.enterDocumentId')); return; } @@ -134,14 +136,14 @@ const DebugPage: React.FC = () => { if (retryCount === 0) { setError(''); // Only clear error on first attempt } - + try { const response = await api.get(`/documents/${targetDocId}/debug`); setDebugInfo(response.data); setError(''); // Clear any previous errors } catch (err: any) { console.error('Debug fetch error:', err); - + // If it's a 404 and we haven't retried much, try again after a short delay if (err.response?.status === 404 && retryCount < 3) { console.log(`Document not found, retrying in ${(retryCount + 1) * 1000}ms... (attempt ${retryCount + 1})`); @@ -150,10 +152,10 @@ const DebugPage: React.FC = () => { }, (retryCount + 1) * 1000); return; } - - const errorMessage = err.response?.status === 404 - ? `Document ${targetDocId} not found. It may still be processing or may have been moved to failed documents.` - : err.response?.data?.message || `Failed to fetch debug information: ${err.message}`; + + const errorMessage = err.response?.status === 404 + ? t('debug.errors.documentNotFound', { documentId: targetDocId }) + : err.response?.data?.message || t('debug.errors.fetchFailed', { message: err.message }); setError(errorMessage); setDebugInfo(null); } finally { @@ -161,7 +163,7 @@ const DebugPage: React.FC = () => { setLoading(false); } } - }, [documentId]); + }, [documentId, t]); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -173,14 +175,14 @@ const DebugPage: React.FC = () => { const uploadDocument = useCallback(async () => { if (!selectedFile) { - setError('Please select a file to upload'); + setError(t('debug.upload.selectFile')); return; } setUploading(true); setUploadProgress(0); setError(''); - setProcessingStatus('Uploading file...'); + setProcessingStatus(t('debug.upload.uploading')); try { const formData = new FormData(); @@ -191,7 +193,7 @@ const DebugPage: React.FC = () => { 'Content-Type': 'multipart/form-data', }, onUploadProgress: (progressEvent) => { - const progress = progressEvent.total + const progress = progressEvent.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0; setUploadProgress(progress); @@ -201,18 +203,18 @@ const DebugPage: React.FC = () => { const uploadedDoc = response.data; setUploadedDocumentId(uploadedDoc.id); setDocumentId(uploadedDoc.id); - setProcessingStatus('Document uploaded successfully. Starting OCR processing...'); - + setProcessingStatus(t('debug.upload.uploadedStartingOcr')); + // Start monitoring the processing startProcessingMonitor(uploadedDoc.id); } catch (err: any) { - setError(err.response?.data?.message || 'Failed to upload document'); - setProcessingStatus('Upload failed'); + setError(err.response?.data?.message || t('debug.upload.uploadFailed')); + setProcessingStatus(t('debug.upload.uploadFailedStatus')); } finally { setUploading(false); setUploadProgress(0); } - }, [selectedFile]); + }, [selectedFile, t]); const startProcessingMonitor = useCallback((docId: string) => { // Clear any existing interval @@ -224,23 +226,23 @@ const DebugPage: React.FC = () => { try { const response = await api.get(`/documents/${docId}`); const doc = response.data; - + if (doc.ocr_status === 'completed' || doc.ocr_status === 'failed') { - setProcessingStatus(`Processing ${doc.ocr_status}!`); + setProcessingStatus(t('debug.monitoring.processingComplete', { status: doc.ocr_status })); clearInterval(interval); setMonitoringInterval(null); - + // Auto-fetch debug info when processing is complete OR failed (but don't switch tabs) setTimeout(() => { fetchDebugInfo(docId); // Don't auto-switch tabs - let user decide when to view debug info }, 2000); // Give it a bit more time to ensure document is saved } else if (doc.ocr_status === 'processing') { - setProcessingStatus('OCR processing in progress...'); + setProcessingStatus(t('debug.monitoring.ocrInProgress')); } else if (doc.ocr_status === 'pending') { - setProcessingStatus('Document queued for OCR processing...'); + setProcessingStatus(t('debug.monitoring.queuedForOcr')); } else { - setProcessingStatus('Checking processing status...'); + setProcessingStatus(t('debug.monitoring.checkingStatus')); } } catch (err) { console.error('Error monitoring processing:', err); @@ -248,14 +250,14 @@ const DebugPage: React.FC = () => { }, 2000); // Check every 2 seconds setMonitoringInterval(interval); - + // Auto-clear monitoring after 5 minutes setTimeout(() => { clearInterval(interval); setMonitoringInterval(null); - setProcessingStatus('Monitoring stopped (timeout)'); + setProcessingStatus(t('debug.monitoring.monitoringTimeout')); }, 300000); - }, [monitoringInterval, fetchDebugInfo]); + }, [monitoringInterval, fetchDebugInfo, t]); // Cleanup interval on unmount useEffect(() => { @@ -282,31 +284,31 @@ const DebugPage: React.FC = () => { - File Information - Filename: {details.filename} - Original: {details.original_filename} - Size: {(details.file_size / 1024 / 1024).toFixed(2)} MB - MIME Type: {details.mime_type} - File Exists: {t('debug.steps.fileInformation.title')} + {t('debug.steps.fileInformation.filename')} {details.filename} + {t('debug.steps.fileInformation.original')} {details.original_filename} + {t('debug.steps.fileInformation.size')} {(details.file_size / 1024 / 1024).toFixed(2)} MB + {t('debug.steps.fileInformation.mimeType')} {details.mime_type} + {t('debug.steps.fileInformation.fileExists')} - File Metadata + {t('debug.steps.fileMetadata.title')} {details.file_metadata ? ( <> - Actual Size: {(details.file_metadata.size / 1024 / 1024).toFixed(2)} MB - Is File: {details.file_metadata.is_file ? 'Yes' : 'No'} - Modified: {details.file_metadata.modified ? new Date(details.file_metadata.modified.secs_since_epoch * 1000).toLocaleString() : 'Unknown'} + {t('debug.steps.fileMetadata.actualSize')} {(details.file_metadata.size / 1024 / 1024).toFixed(2)} MB + {t('debug.steps.fileMetadata.isFile')} {details.file_metadata.is_file ? t('debug.steps.fileInformation.yes') : t('debug.steps.fileInformation.no')} + {t('debug.steps.fileMetadata.modified')} {details.file_metadata.modified ? new Date(details.file_metadata.modified.secs_since_epoch * 1000).toLocaleString() : t('debug.steps.fileMetadata.unknown')} ) : ( - File metadata not available + {t('debug.steps.fileMetadata.notAvailable')} )} - Created: {new Date(details.created_at).toLocaleString()} + {t('debug.steps.fileMetadata.created')} {new Date(details.created_at).toLocaleString()} @@ -541,12 +543,12 @@ const DebugPage: React.FC = () => { - Upload Document for Debug Analysis + {t('debug.upload.title')} - Upload a PDF or image file to analyze the processing pipeline in real-time. + {t('debug.upload.description')} - + { disabled={uploading} sx={{ mr: 2 }} > - Select File + {t('debug.upload.selectFileButton')} - + {selectedFile && ( - Selected: {selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) + {t('debug.upload.selected')} {selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) )} - + {selectedFile && ( )} - + {uploading && uploadProgress > 0 && ( - Upload Progress: {uploadProgress}% + {t('debug.upload.uploadProgress', { percent: uploadProgress })} @@ -615,7 +617,7 @@ const DebugPage: React.FC = () => { {uploadedDocumentId && ( - Document ID: {uploadedDocumentId} + {t('debug.upload.documentId')} {uploadedDocumentId} @@ -654,8 +656,8 @@ const DebugPage: React.FC = () => { {selectedFile && selectedFile.type.startsWith('image/') && ( - Preview - {t('debug.preview')} + { - Debug Existing Document + {t('debug.search.title')} - Enter a document ID to analyze the processing pipeline for an existing document. + {t('debug.search.description')} - + setDocumentId(e.target.value)} - placeholder="e.g., 123e4567-e89b-12d3-a456-426614174000" + placeholder={t('debug.search.documentIdPlaceholder')} fullWidth size="small" /> @@ -701,10 +703,10 @@ const DebugPage: React.FC = () => { disabled={loading || !documentId.trim()} startIcon={loading ? : } > - Debug + {t('debug.search.debugButton')} - + {error && ( {error} @@ -720,36 +722,36 @@ const DebugPage: React.FC = () => { - Document Processing Debug + {t('debug.title')} - Upload documents or analyze existing ones to troubleshoot OCR processing issues. + {t('debug.subtitle')} setActiveTab(newValue)}> - } + } iconPosition="start" /> - } + } iconPosition="start" /> {debugInfo && ( - } + } iconPosition="start" /> )} - + {activeTab === 0 && renderUploadTab()} {activeTab === 1 && renderSearchTab()} @@ -758,7 +760,7 @@ const DebugPage: React.FC = () => { {error && ( - Debug Error + {t('debug.errors.debugError')} {error} )} @@ -768,15 +770,15 @@ const DebugPage: React.FC = () => { - Document: {debugInfo.filename} + {t('debug.document.title', { filename: debugInfo.filename })} - - Debug run at: {new Date(debugInfo.debug_timestamp).toLocaleString()} + {t('debug.document.debugRunAt', { timestamp: new Date(debugInfo.debug_timestamp).toLocaleString() })} @@ -785,7 +787,7 @@ const DebugPage: React.FC = () => { - Processing Pipeline + {t('debug.pipeline.title')} {(debugInfo.pipeline_steps || []).map((step) => ( diff --git a/frontend/src/pages/DocumentDetailsPage.tsx b/frontend/src/pages/DocumentDetailsPage.tsx index 68ab680..0f89134 100644 --- a/frontend/src/pages/DocumentDetailsPage.tsx +++ b/frontend/src/pages/DocumentDetailsPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Box, Typography, @@ -65,6 +66,7 @@ import { useTheme as useMuiTheme } from '@mui/material/styles'; import api from '../services/api'; const DocumentDetailsPage: React.FC = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { mode, modernTokens, glassEffect } = useTheme(); @@ -134,7 +136,7 @@ const DocumentDetailsPage: React.FC = () => { } catch (error) { console.error('Failed to delete document:', error); // Show error message to user - alert('Failed to delete document. Please try again.'); + alert(t('common.status.error')); } finally { setDeleting(false); setDeleteConfirmOpen(false); @@ -170,7 +172,7 @@ const DocumentDetailsPage: React.FC = () => { const fetchDocumentDetails = async (): Promise => { if (!id) { - setError('No document ID provided'); + setError(t('documentDetails.errors.notFound')); setLoading(false); return; } @@ -178,11 +180,11 @@ const DocumentDetailsPage: React.FC = () => { try { setLoading(true); setError(null); - + const response = await documentService.getById(id); setDocument(response.data); } catch (err: any) { - const errorMessage = err.message || 'Failed to load document details'; + const errorMessage = err.message || t('common.status.error'); setError(errorMessage); console.error('Failed to fetch document details:', err); } finally { @@ -215,10 +217,10 @@ const DocumentDetailsPage: React.FC = () => { setOcrLoading(true); const response = await documentService.getOcrText(document.id); setOcrData(response.data); - setOcrText(response.data.ocr_text || 'No OCR text available'); + setOcrText(response.data.ocr_text || t('documentDetails.ocr.noText')); } catch (err) { console.error('Failed to fetch OCR text:', err); - setOcrText('Failed to load OCR text. Please try again.'); + setOcrText(t('documentDetails.ocr.loadFailed')); } finally { setOcrLoading(false); } @@ -242,7 +244,7 @@ const DocumentDetailsPage: React.FC = () => { setShowProcessedImageDialog(true); } catch (err: any) { console.log('Processed image not available:', err); - alert('No processed image available for this document. This feature requires "Save Processed Images" to be enabled in OCR settings.'); + alert(t('documentDetails.dialogs.processedImage.noImage')); } finally { setProcessedImageLoading(false); } @@ -357,10 +359,10 @@ const DocumentDetailsPage: React.FC = () => { onClick={() => navigate('/documents')} sx={{ mb: 3 }} > - Back to Documents + {t('documentDetails.actions.backToDocuments')} - {error || 'Document not found'} + {error || t('documentDetails.errors.notFound')} ); @@ -380,7 +382,7 @@ const DocumentDetailsPage: React.FC = () => { @@ -403,12 +405,12 @@ const DocumentDetailsPage: React.FC = () => { letterSpacing: '-0.02em', }} > - {document?.original_filename || 'Document Details'} + {document?.original_filename || t('navigation.documents')} - + {/* Floating Action Menu */} - + { - - + + { - + {document?.has_ocr_text && ( - + { )} - - + + { - + - Comprehensive document analysis and metadata viewer + {t('documentDetails.subtitle')} @@ -586,16 +588,16 @@ const DocumentDetailsPage: React.FC = () => { - File Size + {t('documentDetails.metadata.fileSize')} {formatFileSize(document.file_size)} - + - Upload Date + {t('documentDetails.metadata.uploadDate')} {formatDate(document.created_at)} @@ -605,7 +607,7 @@ const DocumentDetailsPage: React.FC = () => { {document.source_type && ( - Source Type + {t('documentDetails.metadata.sourceType')} { {document.source_path && ( - Original Path + {t('documentDetails.metadata.originalPath')} { {document.original_created_at && ( - Original Created + {t('documentDetails.metadata.originalCreated')} {formatDate(document.original_created_at)} @@ -654,7 +656,7 @@ const DocumentDetailsPage: React.FC = () => { {document.original_modified_at && ( - Original Modified + {t('documentDetails.metadata.originalModified')} {formatDate(document.original_modified_at)} @@ -665,10 +667,10 @@ const DocumentDetailsPage: React.FC = () => { {document.has_ocr_text && ( - OCR Status + {t('documentDetails.metadata.ocrStatus')} - } @@ -680,7 +682,7 @@ const DocumentDetailsPage: React.FC = () => { {/* Action Buttons */} {document.mime_type?.includes('image') && ( - + { )} - - + + { )} - - + + { - 🔍 Extracted Text (OCR) + {t('documentDetails.ocr.title')} {ocrData?.ocr_text && ( - + setExpandedOcrText(true)} sx={{ @@ -797,18 +799,18 @@ const DocumentDetailsPage: React.FC = () => { > - Expand + {t('documentDetails.ocr.expand')} )} - + {ocrLoading ? ( - Loading OCR analysis... + {t('documentDetails.ocr.loading')} ) : ocrData ? ( @@ -816,9 +818,9 @@ const DocumentDetailsPage: React.FC = () => { {/* Enhanced OCR Stats */} {ocrData.ocr_confidence && ( - { {Math.round(ocrData.ocr_confidence)}% - Confidence + {t('documentDetails.ocr.confidence')} )} {ocrData.ocr_word_count && ( - { {ocrData.ocr_word_count.toLocaleString()} - Words + {t('documentDetails.ocr.words')} )} {ocrData.ocr_processing_time_ms && ( - { {ocrData.ocr_processing_time_ms}ms - Processing Time + {t('documentDetails.ocr.processingTime')} )} @@ -876,15 +878,15 @@ const DocumentDetailsPage: React.FC = () => { {/* OCR Error Display */} {ocrData.ocr_error && ( - - OCR Processing Error + {t('documentDetails.ocr.error')} {ocrData.ocr_error} @@ -934,7 +936,7 @@ const DocumentDetailsPage: React.FC = () => { ) : ( - No OCR text available for this document. + {t('documentDetails.ocr.noText')} )} @@ -943,19 +945,19 @@ const DocumentDetailsPage: React.FC = () => { {ocrData.ocr_completed_at && ( - ✅ Processing completed: {new Date(ocrData.ocr_completed_at).toLocaleString()} + {t('documentDetails.ocr.completed', { date: new Date(ocrData.ocr_completed_at).toLocaleString() })} )} ) : ( - - OCR text is available but failed to load. Please try refreshing the page. + {t('documentDetails.ocr.loadFailed')} )} @@ -985,7 +987,7 @@ const DocumentDetailsPage: React.FC = () => { - 🏷️ Tags & Labels + {t('documentDetails.tagsLabels.title')} - + {/* Tags */} {document.tags && document.tags.length > 0 && ( - Tags + {t('documentDetails.tagsLabels.tags')} {document.tags.map((tag, index) => ( @@ -1028,7 +1030,7 @@ const DocumentDetailsPage: React.FC = () => { {/* Labels */} - Labels + {t('documentDetails.tagsLabels.labels')} {documentLabels.length > 0 ? ( @@ -1047,7 +1049,7 @@ const DocumentDetailsPage: React.FC = () => { ) : ( - No labels assigned to this document + {t('documentDetails.tagsLabels.noLabels')} )} @@ -1070,22 +1072,22 @@ const DocumentDetailsPage: React.FC = () => { - Extracted Text (OCR) + {t('documentDetails.dialogs.ocrText.title')} {ocrData && ( {ocrData.ocr_confidence && ( - )} {ocrData.ocr_word_count && ( - )} @@ -1097,14 +1099,14 @@ const DocumentDetailsPage: React.FC = () => { - Loading OCR text... + {t('documentDetails.dialogs.ocrText.loading')} ) : ( <> {ocrData && ocrData.ocr_error && ( - OCR Error: {ocrData.ocr_error} + {t('documentDetails.dialogs.ocrText.error', { message: ocrData.ocr_error })} )} { lineHeight: 1.6, }} > - {ocrText || 'No OCR text available for this document.'} + {ocrText || t('documentDetails.dialogs.ocrText.noText')} {ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && ( - {ocrData.ocr_processing_time_ms && `Processing time: ${ocrData.ocr_processing_time_ms}ms`} + {ocrData.ocr_processing_time_ms && t('documentDetails.dialogs.ocrText.processingTime', { time: ocrData.ocr_processing_time_ms })} {ocrData.ocr_processing_time_ms && ocrData.ocr_completed_at && ' • '} - {ocrData.ocr_completed_at && `Completed: ${new Date(ocrData.ocr_completed_at).toLocaleString()}`} + {ocrData.ocr_completed_at && t('documentDetails.dialogs.ocrText.completed', { date: new Date(ocrData.ocr_completed_at).toLocaleString() })} )} @@ -1143,7 +1145,7 @@ const DocumentDetailsPage: React.FC = () => {
@@ -1158,7 +1160,7 @@ const DocumentDetailsPage: React.FC = () => { maxWidth="lg" fullWidth PaperProps={{ - sx: { + sx: { height: '90vh', backgroundColor: theme.palette.background.paper, } @@ -1167,23 +1169,23 @@ const DocumentDetailsPage: React.FC = () => { - 🔍 Extracted Text (OCR) - Full View + {t('documentDetails.dialogs.ocrExpanded.title')} {ocrData && ( {ocrData.ocr_confidence && ( - )} {ocrData.ocr_word_count && ( - )} @@ -1211,7 +1213,7 @@ const DocumentDetailsPage: React.FC = () => { setOcrSearchTerm(e.target.value)} InputProps={{ @@ -1242,7 +1244,9 @@ const DocumentDetailsPage: React.FC = () => { {(() => { const text = ocrData?.ocr_text || ''; const matches = text.toLowerCase().split(ocrSearchTerm.toLowerCase()).length - 1; - return matches > 0 ? `${matches} match${matches === 1 ? '' : 'es'} found` : 'No matches found'; + return matches > 0 + ? t('documentDetails.dialogs.ocrExpanded.matches', { count: matches, plural: matches === 1 ? '' : 'es' }) + : t('documentDetails.dialogs.ocrExpanded.noMatches'); })()} )} @@ -1254,14 +1258,14 @@ const DocumentDetailsPage: React.FC = () => { - Loading OCR text... + {t('documentDetails.dialogs.ocrExpanded.loading')} ) : ( <> {ocrData && ocrData.ocr_error && ( - OCR Error: {ocrData.ocr_error} + {t('documentDetails.dialogs.ocrExpanded.error', { message: ocrData.ocr_error })} )} { '$1' ) : ocrData.ocr_text - ) : 'No OCR text available for this document.' + ) : t('documentDetails.dialogs.ocrExpanded.noText') }} /> {ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && ( - {ocrData.ocr_processing_time_ms && `Processing time: ${ocrData.ocr_processing_time_ms}ms`} + {ocrData.ocr_processing_time_ms && t('documentDetails.dialogs.ocrText.processingTime', { time: ocrData.ocr_processing_time_ms })} {ocrData.ocr_processing_time_ms && ocrData.ocr_completed_at && ' • '} - {ocrData.ocr_completed_at && `Completed: ${new Date(ocrData.ocr_completed_at).toLocaleString()}`} + {ocrData.ocr_completed_at && t('documentDetails.dialogs.ocrText.completed', { date: new Date(ocrData.ocr_completed_at).toLocaleString() })} )} @@ -1331,7 +1335,7 @@ const DocumentDetailsPage: React.FC = () => { size="small" sx={{ mr: 1 }} > - Download + {t('common.actions.download')} @@ -1347,7 +1351,7 @@ const DocumentDetailsPage: React.FC = () => { @@ -1360,36 +1364,35 @@ const DocumentDetailsPage: React.FC = () => { fullWidth > - Processed Image - OCR Enhancement Applied + {t('documentDetails.dialogs.processedImage.title')} {processedImageUrl ? ( - Processed image that was fed to OCR - This is the enhanced image that was actually processed by the OCR engine. - You can adjust OCR enhancement settings in the Settings page. + {t('documentDetails.dialogs.processedImage.description')} ) : ( - No processed image available + {t('documentDetails.dialogs.processedImage.noImage')} )} @@ -1402,19 +1405,19 @@ const DocumentDetailsPage: React.FC = () => { fullWidth > - Edit Document Labels + {t('documentDetails.dialogs.editLabels.title')} - Select labels to assign to this document + {t('documentDetails.dialogs.editLabels.description')} @@ -1422,14 +1425,14 @@ const DocumentDetailsPage: React.FC = () => { - @@ -1455,37 +1458,35 @@ const DocumentDetailsPage: React.FC = () => { - Delete Document + {t('documentDetails.dialogs.delete.title')} - This action cannot be undone. + {t('documentDetails.dialogs.delete.warning')} - - Are you sure you want to delete {document?.original_filename}? - + - This will permanently remove the document and all associated data including OCR text, labels, and processing history. + {t('documentDetails.dialogs.delete.details')} - - diff --git a/frontend/src/pages/DocumentManagementPage.tsx b/frontend/src/pages/DocumentManagementPage.tsx index dcb9c9b..05c6cf9 100644 --- a/frontend/src/pages/DocumentManagementPage.tsx +++ b/frontend/src/pages/DocumentManagementPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Box, Typography, @@ -179,6 +180,7 @@ interface IgnoredFilesStats { } const DocumentManagementPage: React.FC = () => { + const { t } = useTranslation(); const theme = useTheme(); const navigate = useNavigate(); const [currentTab, setCurrentTab] = useState(0); @@ -259,22 +261,22 @@ const DocumentManagementPage: React.FC = () => { console.error('Failed to fetch failed documents:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - let errorMessage = 'Failed to load failed documents'; - + let errorMessage = t('documentManagement.errors.loadFailedDocuments'); + // Handle specific document management errors - if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - errorMessage = 'Your session has expired. Please refresh the page and log in again.'; + errorMessage = t('documentManagement.errors.sessionExpired'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - errorMessage = 'You do not have permission to view failed documents.'; + errorMessage = t('documentManagement.errors.permissionDenied'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_NOT_FOUND)) { - errorMessage = 'No failed documents found or they may have been processed.'; + errorMessage = t('documentManagement.errors.noFailedDocumentsFound'); } else if (errorInfo.category === 'network') { - errorMessage = 'Network error. Please check your connection and try again.'; + errorMessage = t('documentManagement.errors.networkError'); } else if (errorInfo.category === 'server') { - errorMessage = 'Server error. Please try again later.'; + errorMessage = t('documentManagement.errors.serverError'); } else { - errorMessage = errorInfo.message || 'Failed to load failed documents'; + errorMessage = errorInfo.message || t('documentManagement.errors.loadFailedDocuments'); } setSnackbar({ @@ -304,20 +306,20 @@ const DocumentManagementPage: React.FC = () => { console.error('Failed to fetch duplicates:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - let errorMessage = 'Failed to load duplicate documents'; - + let errorMessage = t('documentManagement.errors.loadDuplicates'); + // Handle specific duplicate fetch errors - if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - errorMessage = 'Your session has expired. Please refresh the page and log in again.'; + errorMessage = t('documentManagement.errors.sessionExpired'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - errorMessage = 'You do not have permission to view duplicate documents.'; + errorMessage = t('documentManagement.errors.permissionDeniedDuplicates'); } else if (errorInfo.category === 'network') { - errorMessage = 'Network error. Please check your connection and try again.'; + errorMessage = t('documentManagement.errors.networkError'); } else if (errorInfo.category === 'server') { - errorMessage = 'Server error. Please try again later.'; + errorMessage = t('documentManagement.errors.serverError'); } else { - errorMessage = errorInfo.message || 'Failed to load duplicate documents'; + errorMessage = errorInfo.message || t('documentManagement.errors.loadDuplicates'); } setSnackbar({ @@ -373,7 +375,10 @@ const DocumentManagementPage: React.FC = () => { if (response.data.success) { setSnackbar({ open: true, - message: `OCR retry queued for "${document.filename}". Estimated wait time: ${response.data.estimated_wait_minutes || 'Unknown'} minutes.`, + message: t('documentManagement.retry.queuedSuccess', { + filename: document.filename, + minutes: response.data.estimated_wait_minutes || t('documentManagement.retry.unknown') + }), severity: 'success' }); @@ -382,7 +387,7 @@ const DocumentManagementPage: React.FC = () => { } else { setSnackbar({ open: true, - message: response.data.message || 'Failed to retry OCR', + message: response.data.message || t('documentManagement.retry.failed'), severity: 'error' }); } @@ -390,24 +395,24 @@ const DocumentManagementPage: React.FC = () => { console.error('Failed to retry OCR:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - let errorMessage = 'Failed to retry OCR processing'; - + let errorMessage = t('documentManagement.retry.processingFailed'); + // Handle specific OCR retry errors if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_NOT_FOUND)) { - errorMessage = 'Document not found. It may have been deleted or processed already.'; + errorMessage = t('documentManagement.errors.documentNotFound'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.DOCUMENT_OCR_FAILED)) { - errorMessage = 'Document cannot be retried due to processing issues. Please check the document format.'; - } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + errorMessage = t('documentManagement.errors.cannotRetry'); + } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - errorMessage = 'Your session has expired. Please refresh the page and log in again.'; + errorMessage = t('documentManagement.errors.sessionExpired'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - errorMessage = 'You do not have permission to retry OCR processing.'; + errorMessage = t('documentManagement.errors.permissionDeniedRetry'); } else if (errorInfo.category === 'server') { - errorMessage = 'Server error. Please try again later or contact support.'; + errorMessage = t('documentManagement.errors.serverErrorSupport'); } else if (errorInfo.category === 'network') { - errorMessage = 'Network error. Please check your connection and try again.'; + errorMessage = t('documentManagement.errors.networkError'); } else { - errorMessage = errorInfo.message || 'Failed to retry OCR processing'; + errorMessage = errorInfo.message || t('documentManagement.retry.processingFailed'); } setSnackbar({ @@ -431,16 +436,19 @@ const DocumentManagementPage: React.FC = () => { if (response.data.queued_count > 0) { setSnackbar({ open: true, - message: `Successfully queued ${response.data.queued_count} documents for OCR retry. Estimated processing time: ${Math.ceil(response.data.estimated_total_time_minutes)} minutes.`, + message: t('documentManagement.retry.bulkSuccess', { + count: response.data.queued_count, + minutes: Math.ceil(response.data.estimated_total_time_minutes) + }), severity: 'success' }); - + // Refresh all tabs since we're retrying all documents await refreshCurrentTab(); } else { setSnackbar({ open: true, - message: 'No documents found to retry', + message: t('documentManagement.retry.noDocuments'), severity: 'info' }); } @@ -448,7 +456,7 @@ const DocumentManagementPage: React.FC = () => { console.error('Error retrying all documents:', error); setSnackbar({ open: true, - message: 'Failed to retry documents. Please try again.', + message: t('documentManagement.retry.bulkFailed'), severity: 'error' }); } finally { @@ -464,16 +472,18 @@ const DocumentManagementPage: React.FC = () => { if (response.data.requeued_count > 0) { setSnackbar({ open: true, - message: `Successfully queued ${response.data.requeued_count} failed documents for OCR retry. Check the queue stats for progress.`, + message: t('documentManagement.retry.requeuedSuccess', { + count: response.data.requeued_count + }), severity: 'success' }); - + // Refresh the list to update status await fetchFailedDocuments(); } else { setSnackbar({ open: true, - message: 'No failed documents found to retry', + message: t('documentManagement.retry.noFailedDocuments'), severity: 'info' }); } @@ -481,7 +491,7 @@ const DocumentManagementPage: React.FC = () => { console.error('Failed to retry all failed OCR:', error); setSnackbar({ open: true, - message: 'Failed to retry all failed OCR documents', + message: t('documentManagement.retry.requeuedFailed'), severity: 'error' }); } finally { @@ -493,7 +503,11 @@ const DocumentManagementPage: React.FC = () => { const handleBulkRetrySuccess = (result: BulkOcrRetryResponse) => { setSnackbar({ open: true, - message: `Successfully queued ${result.queued_count} of ${result.matched_count} documents for retry. Estimated processing time: ${Math.round(result.estimated_total_time_minutes)} minutes.`, + message: t('documentManagement.retry.advancedSuccess', { + queued: result.queued_count, + matched: result.matched_count, + minutes: Math.round(result.estimated_total_time_minutes) + }), severity: 'success' }); fetchFailedDocuments(); // Refresh the list @@ -574,20 +588,20 @@ const DocumentManagementPage: React.FC = () => { console.error('Failed to fetch ignored files:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - let errorMessage = 'Failed to load ignored files'; - + let errorMessage = t('documentManagement.errors.loadIgnoredFiles'); + // Handle specific ignored files errors - if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || + if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(error, ErrorCodes.USER_TOKEN_EXPIRED)) { - errorMessage = 'Your session has expired. Please refresh the page and log in again.'; + errorMessage = t('documentManagement.errors.sessionExpired'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - errorMessage = 'You do not have permission to view ignored files.'; + errorMessage = t('documentManagement.errors.permissionDeniedIgnored'); } else if (errorInfo.category === 'network') { - errorMessage = 'Network error. Please check your connection and try again.'; + errorMessage = t('documentManagement.errors.networkError'); } else if (errorInfo.category === 'server') { - errorMessage = 'Server error. Please try again later.'; + errorMessage = t('documentManagement.errors.serverError'); } else { - errorMessage = errorInfo.message || 'Failed to load ignored files'; + errorMessage = errorInfo.message || t('documentManagement.errors.loadIgnoredFiles'); } setSnackbar({ @@ -642,7 +656,7 @@ const DocumentManagementPage: React.FC = () => { setSnackbar({ open: true, - message: response.data.message || 'Files removed from ignored list', + message: response.data.message || t('documentManagement.ignoredFiles.removedSuccess'), severity: 'success' }); setSelectedIgnoredFiles(new Set()); @@ -652,7 +666,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error: any) { setSnackbar({ open: true, - message: error.response?.data?.message || 'Failed to delete ignored files', + message: error.response?.data?.message || t('documentManagement.ignoredFiles.deleteFailed'), severity: 'error' }); } finally { @@ -665,7 +679,7 @@ const DocumentManagementPage: React.FC = () => { const response = await api.delete(`/ignored-files/${fileId}`); setSnackbar({ open: true, - message: response.data.message || 'File removed from ignored list', + message: response.data.message || t('documentManagement.ignoredFiles.fileRemovedSuccess'), severity: 'success' }); fetchIgnoredFiles(); @@ -673,7 +687,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error: any) { setSnackbar({ open: true, - message: error.response?.data?.message || 'Failed to delete ignored file', + message: error.response?.data?.message || t('documentManagement.ignoredFiles.fileDeleteFailed'), severity: 'error' }); } @@ -748,7 +762,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error) { setSnackbar({ open: true, - message: 'Failed to preview low confidence documents', + message: t('documentManagement.cleanup.previewFailed'), severity: 'error' }); } finally { @@ -760,7 +774,7 @@ const DocumentManagementPage: React.FC = () => { if (!previewData || previewData.matched_count === 0) { setSnackbar({ open: true, - message: 'No documents to delete', + message: t('documentManagement.cleanup.noDocuments'), severity: 'warning' }); return; @@ -784,7 +798,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error) { setSnackbar({ open: true, - message: 'Failed to delete low confidence documents', + message: t('documentManagement.cleanup.deleteFailed'), severity: 'error' }); } finally { @@ -801,7 +815,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error) { setSnackbar({ open: true, - message: 'Failed to preview failed documents', + message: t('documentManagement.cleanup.previewFailedDocs'), severity: 'error' }); } finally { @@ -829,7 +843,7 @@ const DocumentManagementPage: React.FC = () => { } catch (error) { setSnackbar({ open: true, - message: 'Failed to delete failed documents', + message: t('documentManagement.cleanup.deleteFailedDocs'), severity: 'error' }); } finally { @@ -849,7 +863,7 @@ const DocumentManagementPage: React.FC = () => { - Document Management + {t('documentManagement.title')} @@ -903,31 +917,43 @@ const DocumentManagementPage: React.FC = () => { }, }} > - + } - label={`Failed Documents${statistics ? ` (${statistics.total_failed})` : ''}`} + label={t('documentManagement.tabs.failedDocuments', { + count: statistics ? statistics.total_failed : 0, + showCount: statistics ? true : false + })} iconPosition="start" /> - + } - label={`Document Cleanup${(previewData?.matched_count || 0) + (failedPreviewData?.matched_count || 0) > 0 ? ` (${(previewData?.matched_count || 0) + (failedPreviewData?.matched_count || 0)})` : ''}`} + label={t('documentManagement.tabs.cleanup', { + count: (previewData?.matched_count || 0) + (failedPreviewData?.matched_count || 0), + showCount: (previewData?.matched_count || 0) + (failedPreviewData?.matched_count || 0) > 0 + })} iconPosition="start" /> - + } - label={`Duplicate Files${duplicateStatistics ? ` (${duplicateStatistics.total_duplicate_groups})` : ''}`} + label={t('documentManagement.tabs.duplicates', { + count: duplicateStatistics ? duplicateStatistics.total_duplicate_groups : 0, + showCount: duplicateStatistics ? true : false + })} iconPosition="start" /> - + } - label={`Ignored Files${ignoredFilesStats ? ` (${ignoredFilesStats.total_ignored_files})` : ''}`} + label={t('documentManagement.tabs.ignoredFiles', { + count: ignoredFilesStats ? ignoredFilesStats.total_ignored_files : 0, + showCount: ignoredFilesStats ? true : false + })} iconPosition="start" /> @@ -945,7 +971,7 @@ const DocumentManagementPage: React.FC = () => { - Total Failed + {t('documentManagement.stats.totalFailed')} {statistics.total_failed} @@ -960,7 +986,7 @@ const DocumentManagementPage: React.FC = () => { size="small" fullWidth > - {retryingAll ? 'Retrying...' : 'Retry Failed Only'} + {retryingAll ? t('documentManagement.retrying') : t('documentManagement.retryFailedOnly')}
@@ -970,7 +996,7 @@ const DocumentManagementPage: React.FC = () => { - Failure Categories + {t('documentManagement.stats.failureCategories')} {statistics?.by_reason ? Object.entries(statistics.by_reason).map(([reason, count]) => ( @@ -983,7 +1009,7 @@ const DocumentManagementPage: React.FC = () => { /> )) : ( - No failure data available + {t('documentManagement.stats.noFailureData')} )} @@ -999,18 +1025,18 @@ const DocumentManagementPage: React.FC = () => { - Advanced Retry Options + {t('documentManagement.advancedRetry.title')} - Use advanced filtering and selection options to retry specific subsets of failed documents based on file type, failure reason, size, and more. + {t('documentManagement.advancedRetry.description')} @@ -1023,42 +1049,42 @@ const DocumentManagementPage: React.FC = () => { {/* Filter Controls */} - Filter Options + {t('documentManagement.filters.title')} setFailedDocumentsFilters(prev => ({ ...prev, stage: e.target.value || undefined }))} fullWidth > - All Stages - OCR Processing - Document Ingestion - Validation - File Storage - Processing - Synchronization + {t('documentManagement.filters.allStages')} + {t('documentManagement.filters.stages.ocr')} + {t('documentManagement.filters.stages.ingestion')} + {t('documentManagement.filters.stages.validation')} + {t('documentManagement.filters.stages.storage')} + {t('documentManagement.filters.stages.processing')} + {t('documentManagement.filters.stages.sync')} setFailedDocumentsFilters(prev => ({ ...prev, reason: e.target.value || undefined }))} fullWidth > - All Reasons - Duplicate Content - Low OCR Confidence - Unsupported Format - File Too Large - File Corrupted - OCR Timeout - PDF Parsing Error - Other + {t('documentManagement.filters.allReasons')} + {t('documentManagement.filters.reasons.duplicateContent')} + {t('documentManagement.filters.reasons.lowConfidence')} + {t('documentManagement.filters.reasons.unsupportedFormat')} + {t('documentManagement.filters.reasons.fileTooLarge')} + {t('documentManagement.filters.reasons.fileCorrupted')} + {t('documentManagement.filters.reasons.ocrTimeout')} + {t('documentManagement.filters.reasons.pdfParsingError')} + {t('documentManagement.filters.reasons.other')} @@ -1068,7 +1094,7 @@ const DocumentManagementPage: React.FC = () => { disabled={!failedDocumentsFilters.stage && !failedDocumentsFilters.reason} fullWidth > - Clear Filters + {t('documentManagement.filters.clearFilters')} @@ -1077,15 +1103,14 @@ const DocumentManagementPage: React.FC = () => { {(!documents || documents.length === 0) ? ( - Great news! - No documents have failed OCR processing. All your documents are processing successfully. + {t('documentManagement.alerts.noFailedTitle')} + {t('documentManagement.alerts.noFailedMessage')} ) : ( <> - Failed Documents Overview - These documents failed at various stages of processing: ingestion, validation, OCR, storage, etc. - Use the filters above to narrow down by failure stage or specific reason. You can retry processing for recoverable failures. + {t('documentManagement.alerts.overviewTitle')} + {t('documentManagement.alerts.overviewMessage')} @@ -1093,11 +1118,11 @@ const DocumentManagementPage: React.FC = () => { - Document - Failure Type - Retry Count - Last Failed - Actions + {t('documentManagement.table.document')} + {t('documentManagement.table.failureType')} + {t('documentManagement.table.retryCount')} + {t('documentManagement.table.lastFailed')} + {t('documentManagement.table.actions')} @@ -1131,17 +1156,17 @@ const DocumentManagementPage: React.FC = () => { - {document.retry_count} attempts + {t('documentManagement.table.attempts', { count: document.retry_count })} - {document.updated_at ? format(new Date(document.updated_at), 'MMM dd, yyyy HH:mm') : 'Unknown'} + {document.updated_at ? format(new Date(document.updated_at), 'MMM dd, yyyy HH:mm') : t('documentManagement.table.unknown')} - + handleRetryOcr(document)} @@ -1154,7 +1179,7 @@ const DocumentManagementPage: React.FC = () => { )} - + showDocumentDetails(document)} @@ -1162,7 +1187,7 @@ const DocumentManagementPage: React.FC = () => { - + handleShowRetryHistory(document.id)} @@ -1170,7 +1195,7 @@ const DocumentManagementPage: React.FC = () => { - + { @@ -1197,29 +1222,29 @@ const DocumentManagementPage: React.FC = () => { borderRadius: 1 }}> - Error Details + {t('documentManagement.details.errorDetails')} - Failure Reason: + {t('documentManagement.details.failureReason')}: - {document.failure_reason || document.ocr_failure_reason || 'Not specified'} + {document.failure_reason || document.ocr_failure_reason || t('documentManagement.details.notSpecified')} {/* Show OCR confidence and word count for low confidence failures */} {(document.failure_reason === 'low_ocr_confidence' || document.ocr_failure_reason === 'low_ocr_confidence') && ( <> - OCR Results: + {t('documentManagement.details.ocrResults')}: {document.ocr_confidence !== undefined && document.ocr_confidence !== null && ( } - label={`${document.ocr_confidence.toFixed(1)}% confidence`} + label={t('documentManagement.details.confidencePercent', { percent: document.ocr_confidence.toFixed(1) })} color="warning" variant="outlined" /> @@ -1228,7 +1253,7 @@ const DocumentManagementPage: React.FC = () => { } - label={`${document.ocr_word_count} words found`} + label={t('documentManagement.details.wordsFound', { count: document.ocr_word_count })} color="info" variant="outlined" /> @@ -1238,7 +1263,7 @@ const DocumentManagementPage: React.FC = () => { )} - Error Message: + {t('documentManagement.details.errorMessage')}: { wordBreak: 'break-word' }} > - {document.error_message || document.ocr_error || 'No error message available'} + {document.error_message || document.ocr_error || t('documentManagement.details.noErrorMessage')} - Last Attempt: + {t('documentManagement.details.lastAttempt')}: {document.last_attempt_at ? format(new Date(document.last_attempt_at), 'PPpp') - : 'No previous attempts'} + : t('documentManagement.details.noPreviousAttempts')} - + - File Created: + {t('documentManagement.details.fileCreated')}: {format(new Date(document.created_at), 'PPpp')} diff --git a/frontend/src/pages/DocumentsPage.tsx b/frontend/src/pages/DocumentsPage.tsx index 63d594e..e50746d 100644 --- a/frontend/src/pages/DocumentsPage.tsx +++ b/frontend/src/pages/DocumentsPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Box, Typography, @@ -98,6 +99,7 @@ type SortField = 'created_at' | 'original_filename' | 'file_size'; type SortOrder = 'asc' | 'desc'; const DocumentsPage: React.FC = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const api = useApi(); const [documents, setDocuments] = useState([]); @@ -157,7 +159,7 @@ const DocumentsPage: React.FC = () => { setDocuments(response.data.documents || []); setPagination(response.data.pagination || { total: 0, limit: 20, offset: 0, has_more: false }); } catch (err) { - setError('Failed to load documents'); + setError(t('common.status.error')); console.error(err); } finally { setLoading(false); @@ -438,12 +440,12 @@ const DocumentsPage: React.FC = () => { const getOcrStatusChip = (doc: Document) => { if (!doc.ocr_status) return null; - + const statusConfig = { - 'completed': { color: 'success' as const, label: doc.ocr_confidence ? `OCR ${Math.round(doc.ocr_confidence)}%` : 'OCR Done' }, - 'processing': { color: 'warning' as const, label: 'Processing...' }, - 'failed': { color: 'error' as const, label: 'OCR Failed' }, - 'pending': { color: 'default' as const, label: 'Pending' }, + 'completed': { color: 'success' as const, label: doc.ocr_confidence ? t('documents.ocrStatus.confidence', { percent: Math.round(doc.ocr_confidence) }) : t('documents.ocrStatus.done') }, + 'processing': { color: 'warning' as const, label: t('documents.ocrStatus.processing') }, + 'failed': { color: 'error' as const, label: t('documents.ocrStatus.failed') }, + 'pending': { color: 'default' as const, label: t('documents.ocrStatus.pending') }, }; const config = statusConfig[doc.ocr_status as keyof typeof statusConfig]; @@ -490,10 +492,10 @@ const DocumentsPage: React.FC = () => { mb: 1, }} > - Documents + {t('documents.title')} - Manage and explore your document library + {t('documents.subtitle')} @@ -507,7 +509,7 @@ const DocumentsPage: React.FC = () => { }}> {/* Search */} { size="small" color={selectionMode ? "secondary" : "primary"} > - {selectionMode ? 'Cancel' : 'Select'} + {selectionMode ? t('documents.selection.cancel') : t('documents.selection.select')} {/* OCR Filter */} - OCR Status + {t('documents.filters.ocrStatus')} @@ -571,7 +573,7 @@ const DocumentsPage: React.FC = () => { onClick={handleSortMenuClick} size="small" > - Sort + {t('documents.sort.label')} @@ -588,7 +590,10 @@ const DocumentsPage: React.FC = () => { color: 'primary.contrastText' }}> - {selectedDocuments.size > 999 ? `${Math.floor(selectedDocuments.size/1000)}K` : selectedDocuments.size} of {sortedDocuments.length > 999 ? `${Math.floor(sortedDocuments.length/1000)}K` : sortedDocuments.length} documents selected + {t('documents.selection.count', { + count: selectedDocuments.size > 999 ? `${Math.floor(selectedDocuments.size/1000)}K` : selectedDocuments.size, + total: sortedDocuments.length > 999 ? `${Math.floor(sortedDocuments.length/1000)}K` : sortedDocuments.length + })} )} @@ -620,27 +625,27 @@ const DocumentsPage: React.FC = () => { > handleSortChange('created_at', 'desc')}> - Newest First + {t('documents.sort.newestFirst')} handleSortChange('created_at', 'asc')}> - Oldest First + {t('documents.sort.oldestFirst')} handleSortChange('original_filename', 'asc')}> - Name A-Z + {t('documents.sort.nameAZ')} handleSortChange('original_filename', 'desc')}> - Name Z-A + {t('documents.sort.nameZA')} handleSortChange('file_size', 'desc')}> - Largest First + {t('documents.sort.largestFirst')} handleSortChange('file_size', 'asc')}> - Smallest First + {t('documents.sort.smallestFirst')} @@ -655,25 +660,25 @@ const DocumentsPage: React.FC = () => { handleDocMenuClose(); }}> - Download + {t('common.actions.download')} - { - if (selectedDoc) navigate(`/documents/${selectedDoc.id}`); - handleDocMenuClose(); + { + if (selectedDoc) navigate(`/documents/${selectedDoc.id}`); + handleDocMenuClose(); }}> - View Details + {t('common.actions.viewDetails')} - { - if (selectedDoc) handleEditDocumentLabels(selectedDoc); - handleDocMenuClose(); + { + if (selectedDoc) handleEditDocumentLabels(selectedDoc); + handleDocMenuClose(); }}> - Edit Labels + {t('documents.actions.editLabels')} - { - if (selectedDoc) handleRetryOcr(selectedDoc); + { + if (selectedDoc) handleRetryOcr(selectedDoc); }} disabled={retryingDocument === selectedDoc?.id}> {retryingDocument === selectedDoc?.id ? ( @@ -683,21 +688,21 @@ const DocumentsPage: React.FC = () => { )} - {retryingDocument === selectedDoc?.id ? 'Retrying OCR...' : 'Retry OCR'} + {retryingDocument === selectedDoc?.id ? t('documents.actions.retryingOcr') : t('documents.actions.retryOcr')} - { - if (selectedDoc) handleShowRetryHistory(selectedDoc.id); + { + if (selectedDoc) handleShowRetryHistory(selectedDoc.id); }}> - Retry History + {t('documents.actions.retryHistory')} - { - if (selectedDoc) handleDeleteClick(selectedDoc); + { + if (selectedDoc) handleDeleteClick(selectedDoc); }}> - Delete + {t('common.actions.delete')} @@ -714,10 +719,10 @@ const DocumentsPage: React.FC = () => { }} > - No documents found + {t('documents.empty.title')} - {searchQuery ? 'Try adjusting your search terms' : 'Upload your first document to get started'} + {searchQuery ? t('documents.empty.searchSubtitle') : t('documents.empty.uploadSubtitle')} ) : ( @@ -920,7 +925,7 @@ const DocumentsPage: React.FC = () => { }} fullWidth > - Download + {t('common.actions.download')} )} @@ -932,7 +937,7 @@ const DocumentsPage: React.FC = () => { {/* Label Edit Dialog */} setLabelEditDialogOpen(false)} maxWidth="sm" fullWidth> - Edit Document Labels + {t('documents.dialogs.editLabels.title')} { availableLabels={availableLabels} onLabelsChange={setEditingDocumentLabels} onCreateLabel={handleCreateLabel} - placeholder="Select labels for this document..." + placeholder={t('documents.dialogs.editLabels.placeholder')} size="medium" disabled={labelsLoading} /> - - + + {/* Delete Confirmation Dialog */} - Delete Document + {t('documents.dialogs.delete.title')} - Are you sure you want to delete "{documentToDelete?.original_filename}"? + {t('documents.dialogs.delete.message', { filename: documentToDelete?.original_filename })} - This action cannot be undone. The document file and all associated data will be permanently removed. + {t('documents.dialogs.delete.warning')} - {/* Bulk Delete Confirmation Dialog */} - Delete Multiple Documents + {t('documents.dialogs.bulkDelete.title')} - Are you sure you want to delete {selectedDocuments.size} selected document{selectedDocuments.size !== 1 ? 's' : ''}? + {t('documents.dialogs.bulkDelete.message', { + count: selectedDocuments.size, + plural: selectedDocuments.size !== 1 ? 's' : '' + })} - This action cannot be undone. All selected documents and their associated data will be permanently removed. + {t('documents.dialogs.bulkDelete.warning')} {selectedDocuments.size > 0 && ( - Documents to be deleted: + {t('documents.dialogs.bulkDelete.listTitle')} {Array.from(selectedDocuments).slice(0, 10).map(docId => { const doc = documents.find(d => d.id === docId); @@ -1004,7 +1012,7 @@ const DocumentsPage: React.FC = () => { })} {selectedDocuments.size > 10 && ( - ... and {selectedDocuments.size - 10} more + {t('documents.dialogs.bulkDelete.moreCount', { count: selectedDocuments.size - 10 })} )} @@ -1012,16 +1020,19 @@ const DocumentsPage: React.FC = () => { - @@ -1030,9 +1041,13 @@ const DocumentsPage: React.FC = () => { - Showing {pagination.offset + 1}-{Math.min(pagination.offset + pagination.limit, pagination.total)} of {pagination.total} documents - {ocrFilter && ` with OCR status: ${ocrFilter}`} - {searchQuery && ` matching "${searchQuery}"`} + {t('documents.pagination.showing', { + start: pagination.offset + 1, + end: Math.min(pagination.offset + pagination.limit, pagination.total), + total: pagination.total + })} + {ocrFilter && t('documents.pagination.withOcrStatus', { status: ocrFilter })} + {searchQuery && t('documents.pagination.matching', { query: searchQuery })} diff --git a/frontend/src/pages/IgnoredFilesPage.tsx b/frontend/src/pages/IgnoredFilesPage.tsx index 69f377d..6f1468a 100644 --- a/frontend/src/pages/IgnoredFilesPage.tsx +++ b/frontend/src/pages/IgnoredFilesPage.tsx @@ -50,6 +50,7 @@ import { import { format, formatDistanceToNow } from 'date-fns'; import { useNotifications } from '../contexts/NotificationContext'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; interface IgnoredFile { id: string; @@ -81,6 +82,7 @@ interface IgnoredFilesStats { } const IgnoredFilesPage: React.FC = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [ignoredFiles, setIgnoredFiles] = useState([]); @@ -381,11 +383,11 @@ const IgnoredFilesPage: React.FC = () => { sx={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }} > - Sources + {t('navigation.sources')} - Ignored Files + {t('ignoredFiles.title')} {(sourceTypeParam || sourceNameParam) && ( { - Ignored Files + {t('ignoredFiles.title')} {(sourceTypeParam || sourceNameParam || sourceIdParam) && ( )} - {sourceTypeParam || sourceNameParam || sourceIdParam - ? `Files from ${sourceNameParam || getSourceTypeDisplay(sourceTypeParam)} sources that have been deleted and will be ignored during future syncs.` - : 'Files that have been deleted and will be ignored during future syncs from their sources.' - } + {t('ignoredFiles.subtitle')} {/* Statistics Cards */} @@ -466,7 +465,7 @@ const IgnoredFilesPage: React.FC = () => { { }} sx={{ flexGrow: 1 }} /> - + - Source Type + {t('ignoredFiles.filters.reason')} - All Documents - Has OCR Text - No OCR Text + {t('search.filters.allDocuments')} + {t('search.filters.hasOcrText')} + {t('search.filters.noOcrText')} @@ -906,11 +908,11 @@ const SearchPage: React.FC = () => { {/* Date Range Filter */} }> - Date Range + {t('search.filters.dateRange')} - Days ago: {dateRange[0]} - {dateRange[1]} + {t('search.filters.daysAgo', { min: dateRange[0], max: dateRange[1] })} { min={0} max={365} marks={[ - { value: 0, label: 'Today' }, - { value: 30, label: '30d' }, - { value: 90, label: '90d' }, - { value: 365, label: '1y' }, + { value: 0, label: t('search.filters.dateMarks.today') }, + { value: 30, label: t('search.filters.dateMarks.30d') }, + { value: 90, label: t('search.filters.dateMarks.90d') }, + { value: 365, label: t('search.filters.dateMarks.1y') }, ]} /> @@ -931,11 +933,11 @@ const SearchPage: React.FC = () => { {/* File Size Filter */} }> - File Size + {t('search.filters.fileSize')} - Size: {fileSizeRange[0]}MB - {fileSizeRange[1]}MB + {t('search.filters.sizeRange', { min: fileSizeRange[0], max: fileSizeRange[1] })} { - {loading ? 'Searching...' : `${searchResults.length} results found`} + {loading ? t('search.status.searching') : t('search.status.resultsFound', { count: searchResults.length })} {/* Snippet Settings Button */} @@ -974,12 +976,12 @@ const SearchPage: React.FC = () => { size="small" startIcon={} onClick={(e) => setSnippetSettingsAnchor(e.currentTarget)} - sx={{ + sx={{ flexShrink: 0, position: 'relative', }} > - Display Settings + {t('search.display.settings')} {/* Show indicator if settings are customized */} {(snippetSettings.viewMode !== 'detailed' || snippetSettings.highlightStyle !== 'background' || @@ -1004,16 +1006,16 @@ const SearchPage: React.FC = () => { {!loading && searchResults.length > 0 && ( - Showing: + {t('search.results.showing')} - - { }} > - No results found for "{searchQuery}" + {t('search.noResults.title', { query: searchQuery })} - Try adjusting your search terms or filters + {t('search.noResults.subtitle')} - + {/* Helpful suggestions for no results */} - Suggestions: + {t('search.noResults.suggestions.title')} - • Try simpler or more general terms - • Check spelling and try different keywords - • Remove some filters to broaden your search - • Use quotes for exact phrases + • {t('search.noResults.suggestions.simpler')} + • {t('search.noResults.suggestions.spelling')} + • {t('search.noResults.suggestions.removeFilters')} + • {t('search.noResults.suggestions.useQuotes')} - - @@ -1104,16 +1106,16 @@ const SearchPage: React.FC = () => { > - Start searching your documents + {t('search.empty.title')} - Use the enhanced search bar above to find documents by content, filename, or tags + {t('search.empty.subtitle')} - + {/* Search Tips */} - Search Tips: + {t('search.tips.title')} {searchTips.map((tip, index) => ( @@ -1125,8 +1127,8 @@ const SearchPage: React.FC = () => { - { setCurrentPage(1); }} /> - { setCurrentPage(1); }} /> - { }}> {formatFileSize(doc.file_size)} • {formatDate(doc.created_at)} - {doc.has_ocr_text && ' • OCR'} + {doc.has_ocr_text && t('search.results.hasOcr')} @@ -1242,7 +1244,7 @@ const SearchPage: React.FC = () => { alignItems: 'center', }}> - Tags: + {t('search.results.tags')} {doc.tags.slice(0, 3).map((tag, index) => ( { ))} {doc.tags.length > 3 && ( - +{doc.tags.length - 3} more + {t('common.moreCount', { count: doc.tags.length - 3 })} )} @@ -1307,7 +1309,7 @@ const SearchPage: React.FC = () => { justifyContent: 'flex-start', pt: 0.5, }}> - + { - + { {/* Results Summary */} - Showing {((currentPage - 1) * resultsPerPage) + 1}-{Math.min(currentPage * resultsPerPage, totalResults)} of {totalResults} results + {t('search.results.pagination', { + start: ((currentPage - 1) * resultsPerPage) + 1, + end: Math.min(currentPage * resultsPerPage, totalResults), + total: totalResults + })} @@ -1394,12 +1400,12 @@ const SearchPage: React.FC = () => { PaperProps={{ sx: { width: 320, p: 2 } }} > - Text Display Settings + {t('search.display.textSettings')} - View Mode + {t('search.display.viewMode.label')} { } - label="Compact" + label={t('search.display.viewMode.compact')} /> } - label="Detailed" + label={t('search.display.viewMode.detailed')} /> } - label="Context Focus" + label={t('search.display.viewMode.contextFocus')} /> @@ -1427,7 +1433,7 @@ const SearchPage: React.FC = () => { - Highlight Style + {t('search.display.highlightStyle.label')} { } - label="Background Color" + label={t('search.display.highlightStyle.background')} /> } - label="Underline" + label={t('search.display.highlightStyle.underline')} /> } - label="Bold Text" + label={t('search.display.highlightStyle.bold')} /> @@ -1455,7 +1461,7 @@ const SearchPage: React.FC = () => { - Font Size: {snippetSettings.fontSize}px + {t('search.display.fontSizeLabel', { size: snippetSettings.fontSize })} { - Snippets per result: {snippetSettings.maxSnippetsToShow} + {t('search.display.snippetsPerResult', { count: snippetSettings.maxSnippetsToShow })} { - Context Length: {snippetSettings.contextLength} characters + {t('search.display.contextLength', { length: snippetSettings.contextLength })} any>(func: T, delay: number): const SettingsPage: React.FC = () => { + const { t } = useTranslation(); const { user: currentUser } = useAuth(); const [tabValue, setTabValue] = useState(0); const [settings, setSettings] = useState({ @@ -347,7 +349,7 @@ const SettingsPage: React.FC = () => { } catch (error: any) { console.error('Error fetching settings:', error); if (error.response?.status !== 404) { - showSnackbar('Failed to load settings', 'error'); + showSnackbar(t('settings.messages.settingsUpdateFailed'), 'error'); } } }; @@ -359,7 +361,7 @@ const SettingsPage: React.FC = () => { } catch (error: any) { console.error('Error fetching users:', error); if (error.response?.status !== 404) { - showSnackbar('Failed to load users', 'error'); + showSnackbar(t('settings.messages.settingsUpdateFailed'), 'error'); } } }; @@ -380,22 +382,22 @@ const SettingsPage: React.FC = () => { // Only show success message for non-text inputs to reduce noise if (typeof value !== 'string') { - showSnackbar('Settings updated successfully', 'success'); + showSnackbar(t('settings.messages.settingsUpdated'), 'success'); } } catch (error) { console.error('Error updating settings:', error); - + const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - + // Handle specific settings errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_INVALID_LANGUAGE)) { - showSnackbar('Invalid language selected. Please choose from available languages.', 'error'); + showSnackbar(t('settings.messages.invalidLanguage'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_VALUE_OUT_OF_RANGE)) { - showSnackbar(`${errorInfo.message}. ${errorInfo.suggestedAction || ''}`, 'error'); + showSnackbar(t('settings.messages.valueOutOfRange', { message: errorInfo.message, suggestedAction: errorInfo.suggestedAction || '' }), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_CONFLICTING_SETTINGS)) { - showSnackbar('Conflicting settings detected. Please review your configuration.', 'warning'); + showSnackbar(t('settings.messages.conflictingSettings'), 'warning'); } else { - showSnackbar(errorInfo.message || 'Failed to update settings', 'error'); + showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } }; @@ -405,7 +407,7 @@ const SettingsPage: React.FC = () => { try { if (userDialog.mode === 'create') { await api.post('/users', userForm); - showSnackbar('User created successfully', 'success'); + showSnackbar(t('settings.messages.userCreated'), 'success'); } else { const { password, ...updateData } = userForm; const payload: any = updateData; @@ -413,30 +415,30 @@ const SettingsPage: React.FC = () => { payload.password = password; } await api.put(`/users/${userDialog.user?.id}`, payload); - showSnackbar('User updated successfully', 'success'); + showSnackbar(t('settings.messages.userUpdated'), 'success'); } fetchUsers(); handleCloseUserDialog(); } catch (error: any) { console.error('Error saving user:', error); - + const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - + // Handle specific user errors with better messages if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DUPLICATE_USERNAME)) { - showSnackbar('This username is already taken. Please choose a different username.', 'error'); + showSnackbar(t('settings.messages.duplicateUsername'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DUPLICATE_EMAIL)) { - showSnackbar('This email address is already in use. Please use a different email.', 'error'); + showSnackbar(t('settings.messages.duplicateEmail'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_PASSWORD)) { - showSnackbar('Password must be at least 8 characters with uppercase, lowercase, and numbers.', 'error'); + showSnackbar(t('settings.messages.invalidPassword'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_EMAIL)) { - showSnackbar('Please enter a valid email address.', 'error'); + showSnackbar(t('settings.messages.invalidEmail'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_USERNAME)) { - showSnackbar('Username contains invalid characters. Please use only letters, numbers, and underscores.', 'error'); + showSnackbar(t('settings.messages.invalidUsername'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - showSnackbar('You do not have permission to perform this action.', 'error'); + showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else { - showSnackbar(errorInfo.message || 'Failed to save user', 'error'); + showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } finally { setLoading(false); @@ -445,31 +447,31 @@ const SettingsPage: React.FC = () => { const handleDeleteUser = async (userId: string): Promise => { if (userId === currentUser?.id) { - showSnackbar('You cannot delete your own account', 'error'); + showSnackbar(t('settings.messages.cannotDeleteSelf'), 'error'); return; } - if (window.confirm('Are you sure you want to delete this user?')) { + if (window.confirm(t('settings.messages.confirmDeleteUser'))) { setLoading(true); try { await api.delete(`/users/${userId}`); - showSnackbar('User deleted successfully', 'success'); + showSnackbar(t('settings.messages.userDeleted'), 'success'); fetchUsers(); } catch (error) { console.error('Error deleting user:', error); - + const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); - + // Handle specific delete errors if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DELETE_RESTRICTED)) { - showSnackbar('Cannot delete this user: They may have associated data or be the last admin.', 'error'); + showSnackbar(t('settings.messages.cannotDeleteUser'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_NOT_FOUND)) { - showSnackbar('User not found. They may have already been deleted.', 'warning'); + showSnackbar(t('settings.messages.userNotFound'), 'warning'); fetchUsers(); // Refresh the list } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { - showSnackbar('You do not have permission to delete users.', 'error'); + showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else { - showSnackbar(errorInfo.message || 'Failed to delete user', 'error'); + showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } finally { setLoading(false); @@ -536,14 +538,14 @@ const SettingsPage: React.FC = () => { setOcrActionLoading(true); try { await queueService.pauseOcr(); - showSnackbar('OCR processing paused successfully', 'success'); + showSnackbar(t('settings.messages.ocrPaused'), 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error pausing OCR:', error); if (error.response?.status === 403) { - showSnackbar('Admin access required to pause OCR processing', 'error'); + showSnackbar(t('settings.messages.ocrPauseFailed'), 'error'); } else { - showSnackbar('Failed to pause OCR processing', 'error'); + showSnackbar(t('settings.messages.ocrPauseFailedGeneric'), 'error'); } } finally { setOcrActionLoading(false); @@ -554,14 +556,14 @@ const SettingsPage: React.FC = () => { setOcrActionLoading(true); try { await queueService.resumeOcr(); - showSnackbar('OCR processing resumed successfully', 'success'); + showSnackbar(t('settings.messages.ocrResumed'), 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error resuming OCR:', error); if (error.response?.status === 403) { - showSnackbar('Admin access required to resume OCR processing', 'error'); + showSnackbar(t('settings.messages.ocrResumeFailed'), 'error'); } else { - showSnackbar('Failed to resume OCR processing', 'error'); + showSnackbar(t('settings.messages.ocrResumeFailedGeneric'), 'error'); } } finally { setOcrActionLoading(false); @@ -576,9 +578,9 @@ const SettingsPage: React.FC = () => { } catch (error: any) { console.error('Error fetching server configuration:', error); if (error.response?.status === 403) { - showSnackbar('Admin access required to view server configuration', 'error'); + showSnackbar(t('settings.messages.serverConfigLoadFailed'), 'error'); } else if (error.response?.status !== 404) { - showSnackbar('Failed to load server configuration', 'error'); + showSnackbar(t('settings.messages.serverConfigLoadFailedGeneric'), 'error'); } } finally { setConfigLoading(false); @@ -637,7 +639,7 @@ const SettingsPage: React.FC = () => { try { const response = await userWatchService.createUserWatchDirectory(userId); if (response.data.success) { - showSnackbar('Watch directory created successfully', 'success'); + showSnackbar(t('settings.messages.watchDirectoryCreated'), 'success'); // Refresh the watch directory info for this user try { const updatedResponse = await userWatchService.getUserWatchDirectory(userId); @@ -650,18 +652,18 @@ const SettingsPage: React.FC = () => { console.error('Error refreshing watch directory info:', fetchError); } } else { - showSnackbar(response.data.message || 'Failed to create watch directory', 'error'); + showSnackbar(response.data.message || t('settings.messages.watchDirectoryCreatedFailed'), 'error'); } } catch (error: any) { console.error('Error creating watch directory:', error); - + const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); if (error.response?.status === 403) { - showSnackbar('Admin access required to create watch directories', 'error'); + showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else if (error.response?.status === 409) { - showSnackbar('Watch directory already exists for this user', 'warning'); + showSnackbar(t('settings.messages.watchDirectoryAlreadyExists'), 'warning'); } else { - showSnackbar(errorInfo.message || 'Failed to create watch directory', 'error'); + showSnackbar(errorInfo.message || t('settings.messages.watchDirectoryCreatedFailed'), 'error'); } } finally { setUserWatchDirLoading(userId, false); @@ -671,14 +673,14 @@ const SettingsPage: React.FC = () => { const handleViewWatchDirectory = (directoryPath: string): void => { // For now, just show the path in a snackbar // In a real implementation, this could open a file explorer or navigate to a directory view - showSnackbar(`Watch directory: ${directoryPath}`, 'info'); + showSnackbar(t('settings.messages.watchDirectoryPath', { path: directoryPath }), 'info'); }; const handleRemoveWatchDirectory = (userId: string, username: string): void => { setConfirmDialog({ open: true, - title: 'Remove Watch Directory', - message: `Are you sure you want to remove the watch directory for user "${username}"? This action cannot be undone and will stop monitoring their directory for new files.`, + title: t('settings.userManagement.confirmRemoveDirectory.title'), + message: t('settings.userManagement.confirmRemoveDirectory.message', { username }), onConfirm: () => confirmRemoveWatchDirectory(userId), }); }; @@ -688,7 +690,7 @@ const SettingsPage: React.FC = () => { try { const response = await userWatchService.deleteUserWatchDirectory(userId); if (response.data.success) { - showSnackbar('Watch directory removed successfully', 'success'); + showSnackbar(t('settings.messages.watchDirectoryRemoved'), 'success'); // Update the watch directory info to reflect removal setUserWatchDirectories(prev => { const newMap = new Map(prev); @@ -703,16 +705,16 @@ const SettingsPage: React.FC = () => { return newMap; }); } else { - showSnackbar(response.data.message || 'Failed to remove watch directory', 'error'); + showSnackbar(response.data.message || t('settings.messages.watchDirectoryRemoveFailed'), 'error'); } } catch (error: any) { console.error('Error removing watch directory:', error); - + const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); if (error.response?.status === 403) { - showSnackbar('Admin access required to remove watch directories', 'error'); + showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else if (error.response?.status === 404) { - showSnackbar('Watch directory not found or already removed', 'warning'); + showSnackbar(t('settings.messages.watchDirectoryNotFound'), 'warning'); // Update state to reflect that it doesn't exist setUserWatchDirectories(prev => { const newMap = new Map(prev); @@ -727,7 +729,7 @@ const SettingsPage: React.FC = () => { return newMap; }); } else { - showSnackbar(errorInfo.message || 'Failed to remove watch directory', 'error'); + showSnackbar(errorInfo.message || t('settings.messages.watchDirectoryRemoveFailed'), 'error'); } } finally { setUserWatchDirLoading(userId, false); @@ -749,7 +751,7 @@ const SettingsPage: React.FC = () => { - Loading... + {t('settings.userManagement.watchDirectory.loading')} ); @@ -758,7 +760,7 @@ const SettingsPage: React.FC = () => { if (!watchDirInfo) { return ( - Unknown + {t('settings.userManagement.watchDirectory.statusUnknown')} ); } @@ -775,11 +777,11 @@ const SettingsPage: React.FC = () => { const getStatusText = () => { if (watchDirInfo.exists && watchDirInfo.enabled) { - return 'Active'; + return t('settings.userManagement.watchDirectory.statusActive'); } else if (watchDirInfo.exists && !watchDirInfo.enabled) { - return 'Disabled'; + return t('settings.userManagement.watchDirectory.statusDisabled'); } else { - return 'Not Created'; + return t('settings.userManagement.watchDirectory.statusNotCreated'); } }; @@ -837,34 +839,34 @@ const SettingsPage: React.FC = () => { return ( - Settings + {t('settings.title')} - - - - + + + + {tabValue === 0 && ( - General Settings + {t('settings.general.title')} - OCR Configuration + {t('settings.general.ocrConfiguration.title')} - Configure languages for OCR text extraction. Multiple languages help with mixed-language documents. + {t('settings.general.ocrConfiguration.description')} div': { width: '100%' } }}> { disabled={loading} /> } - label="Auto-detect language combinations" + label={t('settings.general.ocrConfiguration.autoDetectLanguageCombination')} /> - Automatically suggest optimal language combinations based on document content analysis + {t('settings.general.ocrConfiguration.autoDetectLanguageCombinationHelper')} handleSettingsChange('concurrentOcrJobs', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 16 }} - helperText="Number of OCR jobs that can run simultaneously" + helperText={t('settings.general.ocrConfiguration.concurrentOcrJobsHelper')} /> handleSettingsChange('ocrTimeoutSeconds', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 30, max: 3600 }} - helperText="Maximum time for OCR processing per file" + helperText={t('settings.general.ocrConfiguration.ocrTimeoutHelper')} /> - CPU Priority + {t('settings.general.ocrConfiguration.cpuPriority')} @@ -939,12 +941,12 @@ const SettingsPage: React.FC = () => { - OCR Processing Controls (Admin Only) + {t('settings.general.ocrControls.title')} - + - Control OCR processing to manage CPU usage and allow users to use the application without performance impact. + {t('settings.general.ocrControls.description')} @@ -953,14 +955,14 @@ const SettingsPage: React.FC = () => { @@ -969,16 +971,16 @@ const SettingsPage: React.FC = () => { {ocrStatus && ( : } size="medium" /> - {ocrStatus.is_paused - ? 'OCR processing is paused. No new jobs will be processed.' - : 'OCR processing is active. Documents will be processed automatically.'} + {ocrStatus.is_paused + ? t('settings.general.ocrControls.ocrPausedMessage') + : t('settings.general.ocrControls.ocrActiveMessage')} )} @@ -988,9 +990,8 @@ const SettingsPage: React.FC = () => { {ocrStatus?.is_paused && ( - OCR Processing Paused
- New documents will not be processed for OCR text extraction until processing is resumed. - Users can still upload and view documents, but search functionality may be limited. + {t('settings.general.ocrControls.pausedAlertTitle')}
+ {t('settings.general.ocrControls.pausedAlertMessage')}
)} @@ -1000,7 +1001,7 @@ const SettingsPage: React.FC = () => { - File Processing + {t('settings.general.fileProcessing.title')} @@ -1008,24 +1009,24 @@ const SettingsPage: React.FC = () => { handleSettingsChange('maxFileSizeMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 500 }} - helperText="Maximum allowed file size for uploads" + helperText={t('settings.general.fileProcessing.maxFileSizeHelper')} /> handleSettingsChange('memoryLimitMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 128, max: 4096 }} - helperText="Memory limit per OCR job" + helperText={t('settings.general.fileProcessing.memoryLimitHelper')} /> @@ -1038,10 +1039,10 @@ const SettingsPage: React.FC = () => { disabled={loading} /> } - label="Auto-rotate Images" + label={t('settings.general.fileProcessing.autoRotateImages')} /> - Automatically detect and correct image orientation + {t('settings.general.fileProcessing.autoRotateImagesHelper')} @@ -1055,13 +1056,13 @@ const SettingsPage: React.FC = () => { disabled={loading} /> } - label="Enable Image Preprocessing" + label={t('settings.general.fileProcessing.enableImagePreprocessing')} /> - Enhance images for better OCR accuracy (deskew, denoise, contrast) + {t('settings.general.fileProcessing.enableImagePreprocessingHelper')} - ⚠️ Warning: Enabling preprocessing can significantly alter OCR text results and may reduce accuracy for some documents + {t('settings.general.fileProcessing.preprocessingWarning')} @@ -1075,10 +1076,10 @@ const SettingsPage: React.FC = () => { disabled={loading} /> } - label="Enable Background OCR" + label={t('settings.general.fileProcessing.enableBackgroundOcr')} /> - Process OCR in the background after file upload + {t('settings.general.fileProcessing.enableBackgroundOcrHelper')} @@ -1089,16 +1090,16 @@ const SettingsPage: React.FC = () => { - Search Configuration + {t('settings.general.searchConfiguration.title')} - Results Per Page + {t('settings.general.searchConfiguration.resultsPerPage')} handleSettingsChange('ocrNoiseReductionLevel', e.target.value as number)} > - None - Light - Moderate - Heavy + {t('settings.ocrSettings.enhancementControls.noiseReductionNone')} + {t('settings.ocrSettings.enhancementControls.noiseReductionLight')} + {t('settings.ocrSettings.enhancementControls.noiseReductionModerate')} + {t('settings.ocrSettings.enhancementControls.noiseReductionHeavy')} handleSettingsChange('ocrSharpeningStrength', parseFloat(e.target.value) || 0)} - helperText="Image sharpening amount (0 = auto, >0 = manual)" + helperText={t('settings.ocrSettings.enhancementControls.sharpeningStrengthHelper')} inputProps={{ step: 0.1, min: 0, max: 2 }} /> @@ -1276,52 +1277,52 @@ const SettingsPage: React.FC = () => { - Quality Thresholds (when to apply enhancements) + {t('settings.ocrSettings.qualityThresholds.title')} - + handleSettingsChange('ocrQualityThresholdBrightness', parseFloat(e.target.value) || 40)} - helperText="Enhance if brightness below this value (0-255)" + helperText={t('settings.ocrSettings.qualityThresholds.brightnessThresholdHelper')} inputProps={{ step: 1, min: 0, max: 255 }} /> handleSettingsChange('ocrQualityThresholdContrast', parseFloat(e.target.value) || 0.15)} - helperText="Enhance if contrast below this value (0-1)" + helperText={t('settings.ocrSettings.qualityThresholds.contrastThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdNoise', parseFloat(e.target.value) || 0.3)} - helperText="Enhance if noise above this value (0-1)" + helperText={t('settings.ocrSettings.qualityThresholds.noiseThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdSharpness', parseFloat(e.target.value) || 0.15)} - helperText="Enhance if sharpness below this value (0-1)" + helperText={t('settings.ocrSettings.qualityThresholds.sharpnessThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> @@ -1332,10 +1333,10 @@ const SettingsPage: React.FC = () => { - Advanced Processing Options + {t('settings.ocrSettings.advancedProcessing.title')} - + { onChange={(e) => handleSettingsChange('ocrMorphologicalOperations', e.target.checked)} /> } - label="Morphological Operations (text cleanup)" + label={t('settings.ocrSettings.advancedProcessing.morphologicalOperations')} /> @@ -1356,7 +1357,7 @@ const SettingsPage: React.FC = () => { onChange={(e) => handleSettingsChange('ocrHistogramEqualization', e.target.checked)} /> } - label="Histogram Equalization" + label={t('settings.ocrSettings.advancedProcessing.histogramEqualization')} /> @@ -1367,17 +1368,17 @@ const SettingsPage: React.FC = () => { onChange={(e) => handleSettingsChange('saveProcessedImages', e.target.checked)} /> } - label="Save Processed Images for Review" + label={t('settings.ocrSettings.advancedProcessing.saveProcessedImages')} /> handleSettingsChange('ocrAdaptiveThresholdWindowSize', parseInt(e.target.value) || 15)} - helperText="Window size for contrast enhancement (odd number)" + helperText={t('settings.ocrSettings.advancedProcessing.adaptiveThresholdWindowSizeHelper')} inputProps={{ step: 2, min: 3, max: 101 }} /> @@ -1388,41 +1389,41 @@ const SettingsPage: React.FC = () => { - Image Size and Scaling + {t('settings.ocrSettings.imageSizeScaling.title')} - + handleSettingsChange('ocrMaxImageWidth', parseInt(e.target.value) || 10000)} - helperText="Maximum image width in pixels" + helperText={t('settings.ocrSettings.imageSizeScaling.maxImageWidthHelper')} inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrMaxImageHeight', parseInt(e.target.value) || 10000)} - helperText="Maximum image height in pixels" + helperText={t('settings.ocrSettings.imageSizeScaling.maxImageHeightHelper')} inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrUpscaleFactor', parseFloat(e.target.value) || 1.0)} - helperText="Image scaling factor (1.0 = no scaling)" + helperText={t('settings.ocrSettings.imageSizeScaling.upscaleFactorHelper')} inputProps={{ step: 0.1, min: 0.1, max: 5 }} /> @@ -1436,7 +1437,7 @@ const SettingsPage: React.FC = () => { - User Management + {t('settings.userManagement.title')} @@ -1452,11 +1453,11 @@ const SettingsPage: React.FC = () => { - Username - Email - Created At - Watch Directory - Actions + {t('settings.userManagement.tableHeaders.username')} + {t('settings.userManagement.tableHeaders.email')} + {t('settings.userManagement.tableHeaders.createdAt')} + {t('settings.userManagement.tableHeaders.watchDirectory')} + {t('settings.userManagement.tableHeaders.actions')} @@ -1510,7 +1511,7 @@ const SettingsPage: React.FC = () => { if (!watchDirInfo || !watchDirInfo.exists) { // Show Create Directory button return ( - + handleCreateWatchDirectory(user.id)} disabled={loading || isWatchDirLoading} @@ -1529,7 +1530,7 @@ const SettingsPage: React.FC = () => { // Show View and Remove buttons return ( <> - + handleViewWatchDirectory(watchDirInfo.watch_directory_path)} disabled={loading || isWatchDirLoading} @@ -1539,7 +1540,7 @@ const SettingsPage: React.FC = () => { - + handleRemoveWatchDirectory(user.id, user.username)} disabled={loading || isWatchDirLoading} @@ -1557,11 +1558,11 @@ const SettingsPage: React.FC = () => { ); } })()} - + - + {/* User Management Actions */} - + handleOpenUserDialog('edit', user)} disabled={loading} @@ -1570,7 +1571,7 @@ const SettingsPage: React.FC = () => { - + handleDeleteUser(user.id)} disabled={loading || user.id === currentUser?.id} @@ -1593,7 +1594,7 @@ const SettingsPage: React.FC = () => { {tabValue === 3 && ( - Server Configuration (Admin Only) + {t('settings.serverConfiguration.title')} {configLoading ? ( @@ -1605,22 +1606,22 @@ const SettingsPage: React.FC = () => { - File Upload Configuration + {t('settings.serverConfiguration.fileUpload.title')} - Max File Size + {t('settings.serverConfiguration.fileUpload.maxFileSize')} {serverConfig.max_file_size_mb} MB - Upload Path + {t('settings.serverConfiguration.fileUpload.uploadPath')} {serverConfig.upload_path} - Allowed File Types + {t('settings.serverConfiguration.fileUpload.allowedFileTypes')} {serverConfig.allowed_file_types.map((type) => ( @@ -1629,7 +1630,7 @@ const SettingsPage: React.FC = () => { {serverConfig.watch_folder && ( - Watch Folder + {t('settings.serverConfiguration.fileUpload.watchFolder')} {serverConfig.watch_folder} @@ -1642,36 +1643,36 @@ const SettingsPage: React.FC = () => { - OCR Processing Configuration + {t('settings.serverConfiguration.ocrProcessing.title')} - Concurrent OCR Jobs + {t('settings.serverConfiguration.ocrProcessing.concurrentOcrJobs')} {serverConfig.concurrent_ocr_jobs} - OCR Timeout + {t('settings.serverConfiguration.ocrProcessing.ocrTimeout')} {serverConfig.ocr_timeout_seconds}s - Memory Limit + {t('settings.serverConfiguration.ocrProcessing.memoryLimit')} {serverConfig.memory_limit_mb} MB - OCR Language + {t('settings.serverConfiguration.ocrProcessing.ocrLanguage')} {serverConfig.ocr_language} - CPU Priority + {t('settings.serverConfiguration.ocrProcessing.cpuPriority')} {serverConfig.cpu_priority} - Background OCR - {t('settings.serverConfiguration.ocrProcessing.backgroundOcr')} + @@ -1683,35 +1684,35 @@ const SettingsPage: React.FC = () => { - Server Information + {t('settings.serverConfiguration.serverInformation.title')} - Server Host + {t('settings.serverConfiguration.serverInformation.serverHost')} {serverConfig.server_host} - Server Port + {t('settings.serverConfiguration.serverInformation.serverPort')} {serverConfig.server_port} - JWT Secret - {t('settings.serverConfiguration.serverInformation.jwtSecret')} + - Version + {t('settings.serverConfiguration.serverInformation.version')} {serverConfig.version} {serverConfig.build_info && ( - Build Information + {t('settings.serverConfiguration.serverInformation.buildInformation')} {serverConfig.build_info} @@ -1725,23 +1726,23 @@ const SettingsPage: React.FC = () => { - Watch Folder Configuration + {t('settings.serverConfiguration.watchFolderConfiguration.title')} - Watch Interval + {t('settings.serverConfiguration.watchFolderConfiguration.watchInterval')} {serverConfig.watch_interval_seconds}s {serverConfig.file_stability_check_ms && ( - File Stability Check + {t('settings.serverConfiguration.watchFolderConfiguration.fileStabilityCheck')} {serverConfig.file_stability_check_ms}ms )} {serverConfig.max_file_age_hours && ( - Max File Age + {t('settings.serverConfiguration.watchFolderConfiguration.maxFileAge')} {serverConfig.max_file_age_hours}h )} @@ -1757,13 +1758,13 @@ const SettingsPage: React.FC = () => { startIcon={} disabled={configLoading} > - Refresh Configuration + {t('settings.serverConfiguration.refreshConfiguration')} ) : ( - Failed to load server configuration. Admin access may be required. + {t('settings.serverConfiguration.loadFailed')} )} @@ -1773,14 +1774,14 @@ const SettingsPage: React.FC = () => { - {userDialog.mode === 'create' ? 'Create New User' : 'Edit User'} + {userDialog.mode === 'create' ? t('settings.userManagement.dialogs.createUser') : t('settings.userManagement.dialogs.editUser')} setUserForm({ ...userForm, username: e.target.value })} required @@ -1789,7 +1790,7 @@ const SettingsPage: React.FC = () => { setUserForm({ ...userForm, email: e.target.value })} @@ -1799,7 +1800,7 @@ const SettingsPage: React.FC = () => { setUserForm({ ...userForm, password: e.target.value })} @@ -1810,10 +1811,10 @@ const SettingsPage: React.FC = () => { @@ -1836,7 +1837,7 @@ const SettingsPage: React.FC = () => { diff --git a/frontend/src/pages/SourcesPage.tsx b/frontend/src/pages/SourcesPage.tsx index 3c587c6..e57c277 100644 --- a/frontend/src/pages/SourcesPage.tsx +++ b/frontend/src/pages/SourcesPage.tsx @@ -79,6 +79,7 @@ import { useNavigate } from 'react-router-dom'; import api, { queueService, sourcesService, ErrorHelper, ErrorCodes } from '../services/api'; import { formatDistanceToNow } from 'date-fns'; import { useAuth } from '../contexts/AuthContext'; +import { useTranslation } from 'react-i18next'; import SyncProgressDisplay from '../components/SyncProgress'; interface Source { @@ -115,6 +116,7 @@ const SourcesPage: React.FC = () => { const theme = useTheme(); const navigate = useNavigate(); const { user } = useAuth(); + const { t } = useTranslation(); const [sources, setSources] = useState([]); const [loading, setLoading] = useState(true); const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: string }>({ is_paused: false, status: 'running' }); @@ -226,7 +228,7 @@ const SourcesPage: React.FC = () => { setSources(response.data); } catch (error) { console.error('Failed to load sources:', error); - showSnackbar('Failed to load sources', 'error'); + showSnackbar(t('sources.errors.loadFailed'), 'error'); } finally { setLoading(false); } @@ -253,10 +255,10 @@ const SourcesPage: React.FC = () => { try { await queueService.pauseOcr(); await loadOcrStatus(); - showSnackbar('OCR processing paused successfully', 'success'); + showSnackbar(t('sources.ocr.pausedSuccess'), 'success'); } catch (error) { console.error('Failed to pause OCR:', error); - showSnackbar('Failed to pause OCR processing', 'error'); + showSnackbar(t('sources.ocr.pauseFailed'), 'error'); } finally { setOcrLoading(false); } @@ -268,10 +270,10 @@ const SourcesPage: React.FC = () => { try { await queueService.resumeOcr(); await loadOcrStatus(); - showSnackbar('OCR processing resumed successfully', 'success'); + showSnackbar(t('sources.ocr.resumedSuccess'), 'success'); } catch (error) { console.error('Failed to resume OCR:', error); - showSnackbar('Failed to resume OCR processing', 'error'); + showSnackbar(t('sources.ocr.resumeFailed'), 'error'); } finally { setOcrLoading(false); } @@ -390,7 +392,7 @@ const SourcesPage: React.FC = () => { enabled: formData.enabled, config, }); - showSnackbar('Source updated successfully', 'success'); + showSnackbar(t('sources.messages.updateSuccess'), 'success'); } else { await api.post('/sources', { name: formData.name, @@ -398,7 +400,7 @@ const SourcesPage: React.FC = () => { enabled: formData.enabled, config, }); - showSnackbar('Source created successfully', 'success'); + showSnackbar(t('sources.messages.createSuccess'), 'success'); } setDialogOpen(false); @@ -410,17 +412,17 @@ const SourcesPage: React.FC = () => { // Handle specific source errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_DUPLICATE_NAME)) { - showSnackbar('A source with this name already exists. Please choose a different name.', 'error'); + showSnackbar(t('sources.errors.duplicateName'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_CONFIG_INVALID)) { - showSnackbar('Source configuration is invalid. Please check your settings and try again.', 'error'); + showSnackbar(t('sources.errors.invalidConfig'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_AUTH_FAILED)) { - showSnackbar('Authentication failed. Please verify your credentials.', 'error'); + showSnackbar(t('sources.errors.authFailed'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_CONNECTION_FAILED)) { - showSnackbar('Cannot connect to the source. Please check your network and server settings.', 'error'); + showSnackbar(t('sources.errors.connectionError'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_INVALID_PATH)) { - showSnackbar('Invalid path specified. Please check your folder paths and try again.', 'error'); + showSnackbar(t('sources.errors.invalidPath'), 'error'); } else { - showSnackbar(errorInfo.message || 'Failed to save source', 'error'); + showSnackbar(errorInfo.message || t('sources.errors.saveFailed'), 'error'); } } }; @@ -442,7 +444,7 @@ const SourcesPage: React.FC = () => { setDeleteLoading(true); try { await api.delete(`/sources/${sourceToDelete.id}`); - showSnackbar('Source deleted successfully', 'success'); + showSnackbar(t('sources.messages.deleteSuccess'), 'success'); loadSources(); handleDeleteCancel(); } catch (error) { @@ -452,13 +454,13 @@ const SourcesPage: React.FC = () => { // Handle specific delete errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_NOT_FOUND)) { - showSnackbar('Source not found. It may have already been deleted.', 'warning'); + showSnackbar(t('sources.errors.notFound'), 'warning'); loadSources(); // Refresh the list handleDeleteCancel(); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_SYNC_IN_PROGRESS)) { - showSnackbar('Cannot delete source while sync is in progress. Please stop the sync first.', 'error'); + showSnackbar(t('sources.errors.syncInProgress'), 'error'); } else { - showSnackbar(errorInfo.message || 'Failed to delete source', 'error'); + showSnackbar(errorInfo.message || t('sources.errors.deleteFailed'), 'error'); } setDeleteLoading(false); } @@ -505,9 +507,9 @@ const SourcesPage: React.FC = () => { } if (response && response.data.success) { - showSnackbar(response.data.message || 'Connection successful!', 'success'); + showSnackbar(response.data.message || t('sources.messages.connectionSuccess'), 'success'); } else { - showSnackbar(response?.data.message || 'Connection failed', 'error'); + showSnackbar(response?.data.message || t('sources.errors.connectionFailed'), 'error'); } } catch (error: any) { console.error('Failed to test connection:', error); @@ -516,17 +518,17 @@ const SourcesPage: React.FC = () => { // Handle specific connection test errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_CONNECTION_FAILED)) { - showSnackbar('Connection failed. Please check your server URL and network connectivity.', 'error'); + showSnackbar(t('sources.errors.connectionFailedUrl'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_AUTH_FAILED)) { - showSnackbar('Authentication failed. Please verify your username and password.', 'error'); + showSnackbar(t('sources.errors.authFailedCredentials'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_INVALID_PATH)) { - showSnackbar('Invalid path specified. Please check your folder paths.', 'error'); + showSnackbar(t('sources.errors.invalidFolderPath'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_CONFIG_INVALID)) { - showSnackbar('Configuration is invalid. Please review your settings.', 'error'); + showSnackbar(t('sources.errors.invalidSettings'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_NETWORK_TIMEOUT)) { - showSnackbar('Connection timed out. Please check your network and try again.', 'error'); + showSnackbar(t('sources.errors.timeout'), 'error'); } else { - showSnackbar(errorInfo.message || 'Failed to test connection', 'error'); + showSnackbar(errorInfo.message || t('sources.errors.testConnectionFailed'), 'error'); } } finally { setTestingConnection(false); @@ -552,7 +554,7 @@ const SourcesPage: React.FC = () => { try { await sourcesService.triggerSync(sourceToSync.id); - showSnackbar('Quick sync started successfully', 'success'); + showSnackbar(t('sources.messages.syncStartSuccess'), 'success'); setTimeout(loadSources, 1000); } catch (error: any) { console.error('Failed to trigger sync:', error); @@ -561,16 +563,16 @@ const SourcesPage: React.FC = () => { // Handle specific sync errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_SYNC_IN_PROGRESS)) { - showSnackbar('Source is already syncing. Please wait for the current sync to complete.', 'warning'); + showSnackbar(t('sources.errors.alreadySyncing'), 'warning'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_CONNECTION_FAILED)) { - showSnackbar('Cannot connect to source. Please check your connection and try again.', 'error'); + showSnackbar(t('sources.errors.cannotConnect'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_AUTH_FAILED)) { - showSnackbar('Authentication failed. Please verify your source credentials.', 'error'); + showSnackbar(t('sources.errors.authFailedSource'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SOURCE_NOT_FOUND)) { - showSnackbar('Source not found. It may have been deleted.', 'error'); + showSnackbar(t('sources.errors.sourceDeleted'), 'error'); loadSources(); // Refresh the sources list } else { - showSnackbar(errorInfo.message || 'Failed to start sync', 'error'); + showSnackbar(errorInfo.message || t('sources.errors.syncStartFailed'), 'error'); } } finally { setSyncingSource(null); @@ -585,16 +587,16 @@ const SourcesPage: React.FC = () => { try { await sourcesService.triggerDeepScan(sourceToSync.id); - showSnackbar('Deep scan started successfully', 'success'); + showSnackbar(t('sources.messages.deepScanSuccess'), 'success'); setTimeout(loadSources, 1000); } catch (error: any) { console.error('Failed to trigger deep scan:', error); if (error.response?.status === 409) { showSnackbar('Source is already syncing', 'warning'); } else if (error.response?.status === 400 && error.response?.data?.message?.includes('only supported for WebDAV')) { - showSnackbar('Deep scan is only supported for WebDAV sources', 'warning'); + showSnackbar(t('sources.errors.deepScanWebdavOnly'), 'warning'); } else { - showSnackbar('Failed to start deep scan', 'error'); + showSnackbar(t('sources.errors.deepScanFailed'), 'error'); } } finally { setDeepScanning(false); @@ -605,14 +607,14 @@ const SourcesPage: React.FC = () => { setStoppingSync(sourceId); try { await sourcesService.stopSync(sourceId); - showSnackbar('Sync stopped successfully', 'success'); + showSnackbar(t('sources.messages.syncStopSuccess'), 'success'); setTimeout(loadSources, 1000); } catch (error: any) { console.error('Failed to stop sync:', error); if (error.response?.status === 409) { - showSnackbar('Source is not currently syncing', 'warning'); + showSnackbar(t('sources.errors.notSyncing'), 'warning'); } else { - showSnackbar('Failed to stop sync', 'error'); + showSnackbar(t('sources.errors.syncStopFailed'), 'error'); } } finally { setStoppingSync(null); @@ -646,29 +648,29 @@ const SourcesPage: React.FC = () => { let statusColor = theme.palette.grey[500]; let StatusIcon = HealthIcon; - let statusText = 'Unknown'; - let tooltipText = 'Validation status unknown'; + let statusText = t('sources.validation.unknown'); + let tooltipText = t('sources.validation.statusUnknown'); if (validationStatus === 'healthy') { statusColor = theme.palette.success.main; StatusIcon = CheckCircleIcon; - statusText = 'Healthy'; - tooltipText = `Health score: ${validationScore || 'N/A'}`; + statusText = t('sources.validation.healthy'); + tooltipText = `t('sources.validation.healthScore', { score: validationScore || t('sources.labels.notAvailable') })`; } else if (validationStatus === 'warning') { statusColor = theme.palette.warning.main; StatusIcon = WarningIcon; - statusText = 'Warning'; - tooltipText = `Health score: ${validationScore || 'N/A'} - Issues detected`; + statusText = t('sources.validation.warning'); + tooltipText = `t('sources.validation.healthScore', { score: validationScore || t('sources.labels.notAvailable') }) - Issues detected`; } else if (validationStatus === 'critical') { statusColor = theme.palette.error.main; StatusIcon = CriticalIcon; - statusText = 'Critical'; - tooltipText = `Health score: ${validationScore || 'N/A'} - Critical issues`; + statusText = t('sources.validation.critical'); + tooltipText = `t('sources.validation.healthScore', { score: validationScore || t('sources.labels.notAvailable') }) - Critical issues`; } else if (validationStatus === 'validating') { statusColor = theme.palette.info.main; StatusIcon = HealthIcon; - statusText = 'Validating'; - tooltipText = 'Validation check in progress'; + statusText = t('sources.validation.validating'); + tooltipText = t('sources.validation.inProgress'); } if (lastValidationAt) { @@ -754,10 +756,10 @@ const SourcesPage: React.FC = () => { }); } setCrawlEstimate(response.data); - showSnackbar('Crawl estimation completed', 'success'); + showSnackbar(t('sources.messages.estimationSuccess'), 'success'); } catch (error) { console.error('Failed to estimate crawl:', error); - showSnackbar('Failed to estimate crawl', 'error'); + showSnackbar(t('sources.errors.estimateFailed'), 'error'); } finally { setEstimatingCrawl(false); } @@ -973,7 +975,7 @@ const SourcesPage: React.FC = () => { /> } - label={`${source.total_documents_ocr} OCR'd`} + label={t('sources.stats.ocrCount', { count: source.total_documents_ocr })} size="small" sx={{ borderRadius: 2, @@ -1134,7 +1136,7 @@ const SourcesPage: React.FC = () => { label="Last Sync" value={source.last_sync_at ? formatDistanceToNow(new Date(source.last_sync_at), { addSuffix: true }) - : 'Never'} + : t('sources.stats.never')} color="primary" tooltip="When this source was last synchronized" /> @@ -1256,7 +1258,7 @@ const SourcesPage: React.FC = () => { transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', }} > - {autoRefreshing ? 'Auto-refreshing...' : 'Refresh'} + {autoRefreshing ? t('sources.status.autoRefreshing') : t('common.actions.refresh')} {/* OCR Controls for Admin Users */} @@ -1404,10 +1406,10 @@ const SourcesPage: React.FC = () => { - {editingSource ? 'Edit Source' : 'Create New Source'} + {editingSource ? t('sources.actions.editSource') : t('sources.dialog.createTitle')} - {editingSource ? 'Update your source configuration' : 'Connect a new document source'} + {editingSource ? t('sources.dialog.editSubtitle') : t('sources.dialog.createSubtitle')} @@ -1767,7 +1769,7 @@ const SourcesPage: React.FC = () => { startIcon={estimatingCrawl ? : } sx={{ mb: 2, borderRadius: 2 }} > - {estimatingCrawl ? 'Estimating...' : 'Estimate Crawl'} + {estimatingCrawl ? t('sources.estimation.estimating') : t('sources.estimation.estimate')} {estimatingCrawl && ( @@ -2391,7 +2393,7 @@ const SourcesPage: React.FC = () => { background: `linear-gradient(45deg, ${theme.palette.primary.main}, ${theme.palette.primary.dark})`, }} > - {editingSource ? 'Save Changes' : 'Create Source'} + {editingSource ? 'Save Changes' : t('sources.actions.createSource')} @@ -2426,7 +2428,7 @@ const SourcesPage: React.FC = () => { px: 3, }} > - {deleteLoading ? 'Deleting...' : 'Delete'} + {deleteLoading ? t('sources.delete.deleting') : t('common.actions.delete')} diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index 5b9669d..a22e2bd 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Box, Typography, @@ -35,37 +36,38 @@ interface UploadedDocument { created_at: string; } -const features: Feature[] = [ - { - icon: AutoIcon, - title: 'AI-Powered OCR', - description: 'Advanced text extraction from any document type', - }, - { - icon: SearchIcon, - title: 'Full-Text Search', - description: 'Find documents instantly by content or metadata', - }, - { - icon: SpeedIcon, - title: 'Lightning Fast', - description: 'Process documents in seconds, not minutes', - }, - { - icon: SecurityIcon, - title: 'Secure & Private', - description: 'Your documents are encrypted and protected', - }, - { - icon: LanguageIcon, - title: 'Multi-Language', - description: 'Support for 100+ languages and scripts', - }, -]; - const UploadPage: React.FC = () => { + const { t } = useTranslation(); const navigate = useNavigate(); + const features: Feature[] = [ + { + icon: AutoIcon, + title: t('upload.features.aiOcr.title'), + description: t('upload.features.aiOcr.description'), + }, + { + icon: SearchIcon, + title: t('upload.features.fullTextSearch.title'), + description: t('upload.features.fullTextSearch.description'), + }, + { + icon: SpeedIcon, + title: t('upload.features.lightningFast.title'), + description: t('upload.features.lightningFast.description'), + }, + { + icon: SecurityIcon, + title: t('upload.features.secure.title'), + description: t('upload.features.secure.description'), + }, + { + icon: LanguageIcon, + title: t('upload.features.multiLanguage.title'), + description: t('upload.features.multiLanguage.description'), + }, + ]; + const handleUploadComplete = (document: UploadedDocument): void => { // Optionally navigate to the document or show a success message console.log('Upload completed:', document); @@ -75,10 +77,10 @@ const UploadPage: React.FC = () => { - Upload Documents + {t('upload.title')} - Transform your documents with intelligent OCR processing + {t('upload.subtitle')} @@ -95,27 +97,27 @@ const UploadPage: React.FC = () => { - 📋 Upload Tips + {t('upload.tips.title')} - • For best OCR results, use high-resolution images + {t('upload.tips.highRes')} - • PDF files with text layers are processed faster + {t('upload.tips.pdfText')} - • Ensure documents are well-lit and clearly readable + {t('upload.tips.clarity')} - • Maximum file size is 50MB per document + {t('upload.tips.maxSize')} diff --git a/frontend/src/pages/WatchFolderPage.tsx b/frontend/src/pages/WatchFolderPage.tsx index dbc4495..3eef641 100644 --- a/frontend/src/pages/WatchFolderPage.tsx +++ b/frontend/src/pages/WatchFolderPage.tsx @@ -38,6 +38,7 @@ import { import { useTheme } from '@mui/material/styles'; import { queueService, QueueStats, userWatchService, UserWatchDirectoryResponse } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; +import { useTranslation } from 'react-i18next'; interface WatchConfig { watchFolder: string; @@ -49,6 +50,7 @@ interface WatchConfig { } const WatchFolderPage: React.FC = () => { + const { t } = useTranslation(); const theme = useTheme(); const { user } = useAuth(); @@ -200,7 +202,7 @@ const WatchFolderPage: React.FC = () => { return ( - { WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', }}> - Watch Folder + {t('watchFolder.title')} - + {queueStats && queueStats.failed_count > 0 && ( )} @@ -257,11 +259,11 @@ const WatchFolderPage: React.FC = () => { - Personal Watch Directory + {t('watchFolder.personalWatchDirectory')} {user.role === 'Admin' && ( } - label="Admin" + label={t('watchFolder.admin')} size="small" color="primary" variant="outlined" @@ -287,7 +289,7 @@ const WatchFolderPage: React.FC = () => { - Your Personal Watch Directory + {t('watchFolder.yourPersonalWatchDirectory')} { - Directory Status + {t('watchFolder.directoryStatus')} : } - label={userWatchInfo.exists ? 'Directory Exists' : 'Directory Missing'} + label={userWatchInfo.exists ? t('watchFolder.directoryExists') : t('watchFolder.directoryMissing')} color={userWatchInfo.exists ? 'success' : 'error'} variant="filled" size="small" @@ -320,11 +322,11 @@ const WatchFolderPage: React.FC = () => { - Watch Status + {t('watchFolder.watchStatus')} : } - label={userWatchInfo.enabled ? 'Enabled' : 'Disabled'} + label={userWatchInfo.enabled ? t('watchFolder.enabled') : t('watchFolder.disabled')} color={userWatchInfo.enabled ? 'success' : 'warning'} variant="filled" size="small" @@ -343,7 +345,7 @@ const WatchFolderPage: React.FC = () => { border: `1px solid ${theme.palette.info.main}`, }}> - Your personal watch directory doesn't exist yet. Create it to start uploading files to your own dedicated folder. + {t('watchFolder.directoryNotExist')} @@ -361,7 +363,7 @@ const WatchFolderPage: React.FC = () => { ) : ( - Unable to load personal watch directory information. Please try refreshing the page. + {t('watchFolder.unableToLoad')} )} @@ -373,7 +375,7 @@ const WatchFolderPage: React.FC = () => { - System Configuration + {t('watchFolder.systemConfiguration')} @@ -384,10 +386,10 @@ const WatchFolderPage: React.FC = () => { - Global Watch Folder Configuration + {t('watchFolder.globalWatchFolderConfiguration')} {user?.role === 'Admin' && ( { {user?.role !== 'Admin' && ( - This is the system-wide watch folder configuration. All users can view this information. + {t('watchFolder.systemWideInfo')} )} - Watched Directory + {t('watchFolder.watchedDirectory')} - @@ -420,11 +422,11 @@ const WatchFolderPage: React.FC = () => { - Status + {t('watchFolder.status')} @@ -433,7 +435,7 @@ const WatchFolderPage: React.FC = () => { - Watch Strategy + {t('watchFolder.watchStrategy')} {watchConfig.strategy} @@ -443,27 +445,27 @@ const WatchFolderPage: React.FC = () => { - Scan Interval + {t('watchFolder.scanInterval')} - {watchConfig.watchInterval} seconds + {t('watchFolder.seconds', { count: watchConfig.watchInterval })} - Max File Age + {t('watchFolder.maxFileAge')} - {watchConfig.maxFileAge} hours + {t('watchFolder.hours', { count: watchConfig.maxFileAge })} - Supported File Types + {t('watchFolder.supportedFileTypes')} {watchConfig.allowedTypes.map((type) => ( @@ -488,7 +490,7 @@ const WatchFolderPage: React.FC = () => { - Processing Queue + {t('watchFolder.processingQueue')} @@ -501,14 +503,14 @@ const WatchFolderPage: React.FC = () => { borderRadius: 2, border: theme.palette.mode === 'dark' ? '1px solid rgba(2, 136, 209, 0.3)' : 'none' }}> - {queueStats.pending_count} - Pending + {t('watchFolder.pending')} @@ -522,14 +524,14 @@ const WatchFolderPage: React.FC = () => { borderRadius: 2, border: theme.palette.mode === 'dark' ? '1px solid rgba(237, 108, 2, 0.3)' : 'none' }}> - {queueStats.processing_count} - Processing + {t('watchFolder.processing')} @@ -543,35 +545,35 @@ const WatchFolderPage: React.FC = () => { borderRadius: 2, border: theme.palette.mode === 'dark' ? '1px solid rgba(211, 47, 47, 0.3)' : 'none' }}> - {queueStats.failed_count} - Failed + {t('watchFolder.failed')} - - {queueStats.completed_today} - Completed Today + {t('watchFolder.completedToday')} @@ -579,14 +581,14 @@ const WatchFolderPage: React.FC = () => { - - Average Wait Time + {t('watchFolder.averageWaitTime')} {formatDuration(queueStats.avg_wait_time_minutes)} @@ -594,14 +596,14 @@ const WatchFolderPage: React.FC = () => { - - Oldest Pending Item + {t('watchFolder.oldestPendingItem')} {formatDuration(queueStats.oldest_pending_minutes)} @@ -612,7 +614,7 @@ const WatchFolderPage: React.FC = () => { {lastRefresh && ( - Last updated: {lastRefresh.toLocaleTimeString()} + {t('watchFolder.lastUpdated', { time: lastRefresh.toLocaleTimeString() })} )} @@ -624,39 +626,38 @@ const WatchFolderPage: React.FC = () => { - How Watch Folder Works + {t('watchFolder.howWatchFolderWorks')} - The watch folder system automatically monitors the configured directory for new files and processes them for OCR. + {t('watchFolder.watchFolderDescription')} - + - Processing Pipeline: + {t('watchFolder.processingPipeline')} - 1. File Detection: New files are detected using hybrid watching (inotify + polling) + {t('watchFolder.pipelineSteps.fileDetection')} - 2. Validation: Files are checked for supported format and size limits + {t('watchFolder.pipelineSteps.validation')} - 3. Deduplication: System prevents processing of duplicate files + {t('watchFolder.pipelineSteps.deduplication')} - 4. Storage: Files are moved to the document storage system + {t('watchFolder.pipelineSteps.storage')} - 5. OCR Queue: Documents are queued for OCR processing with priority + {t('watchFolder.pipelineSteps.ocrQueue')} - The system uses a hybrid watching strategy that automatically detects filesystem type and chooses - the optimal monitoring approach (inotify for local filesystems, polling for network mounts). + {t('watchFolder.hybridStrategyInfo')}