- Create your Readur account
+ {t('register.title')}
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... }>
+