diff --git a/packages/backend/package.json b/packages/backend/package.json index 600564b..829ffc5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,6 +40,9 @@ "express-validator": "^7.2.1", "google-auth-library": "^10.1.0", "googleapis": "^152.0.0", + "i18next": "^25.4.2", + "i18next-fs-backend": "^2.6.0", + "i18next-http-middleware": "^3.8.0", "imapflow": "^1.0.191", "jose": "^6.0.11", "mailparser": "^3.7.4", diff --git a/packages/backend/src/api/controllers/archived-email.controller.ts b/packages/backend/src/api/controllers/archived-email.controller.ts index dc64c7f..19a991a 100644 --- a/packages/backend/src/api/controllers/archived-email.controller.ts +++ b/packages/backend/src/api/controllers/archived-email.controller.ts @@ -11,7 +11,7 @@ export class ArchivedEmailController { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const result = await ArchivedEmailService.getArchivedEmails( @@ -23,7 +23,7 @@ export class ArchivedEmailController { return res.status(200).json(result); } catch (error) { console.error('Get archived emails error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -33,23 +33,23 @@ export class ArchivedEmailController { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const email = await ArchivedEmailService.getArchivedEmailById(id, userId); if (!email) { - return res.status(404).json({ message: 'Archived email not found' }); + return res.status(404).json({ message: req.t('archivedEmail.notFound') }); } return res.status(200).json(email); } catch (error) { console.error(`Get archived email by id ${req.params.id} error:`, error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public deleteArchivedEmail = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -59,11 +59,11 @@ export class ArchivedEmailController { console.error(`Delete archived email ${req.params.id} error:`, error); if (error instanceof Error) { if (error.message === 'Archived email not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('archivedEmail.notFound') }); } return res.status(500).json({ message: error.message }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/auth.controller.ts b/packages/backend/src/api/controllers/auth.controller.ts index 9ef6904..a692401 100644 --- a/packages/backend/src/api/controllers/auth.controller.ts +++ b/packages/backend/src/api/controllers/auth.controller.ts @@ -27,7 +27,7 @@ export class AuthController { const { email, password, first_name, last_name } = req.body; if (!email || !password || !first_name || !last_name) { - return res.status(400).json({ message: 'Email, password, and name are required' }); + return res.status(400).json({ message: req.t('auth.setup.allFieldsRequired') }); } try { @@ -37,7 +37,7 @@ export class AuthController { const userCount = Number(userCountResult[0].count); if (userCount > 0) { - return res.status(403).json({ message: 'Setup has already been completed.' }); + return res.status(403).json({ message: req.t('auth.setup.alreadyCompleted') }); } const newUser = await this.#userService.createAdminUser( @@ -48,7 +48,7 @@ export class AuthController { return res.status(201).json(result); } catch (error) { console.error('Setup error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -56,20 +56,20 @@ export class AuthController { const { email, password } = req.body; if (!email || !password) { - return res.status(400).json({ message: 'Email and password are required' }); + return res.status(400).json({ message: req.t('auth.login.emailAndPasswordRequired') }); } try { const result = await this.#authService.login(email, password); if (!result) { - return res.status(401).json({ message: 'Invalid credentials' }); + return res.status(401).json({ message: req.t('auth.login.invalidCredentials') }); } return res.status(200).json(result); } catch (error) { console.error('Login error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -124,7 +124,7 @@ export class AuthController { return res.status(200).json({ needsSetupUser }); } catch (error) { console.error('Status check error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/iam.controller.ts b/packages/backend/src/api/controllers/iam.controller.ts index 60600fb..713e908 100644 --- a/packages/backend/src/api/controllers/iam.controller.ts +++ b/packages/backend/src/api/controllers/iam.controller.ts @@ -22,7 +22,7 @@ export class IamController { } res.status(200).json(roles); } catch (error) { - res.status(500).json({ message: 'Failed to get roles.' }); + res.status(500).json({ message: req.t('iam.failedToGetRoles') }); } }; @@ -34,21 +34,21 @@ export class IamController { if (role) { res.status(200).json(role); } else { - res.status(404).json({ message: 'Role not found.' }); + res.status(404).json({ message: req.t('iam.roleNotFound') }); } } catch (error) { - res.status(500).json({ message: 'Failed to get role.' }); + res.status(500).json({ message: req.t('iam.failedToGetRole') }); } }; public createRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { name, policies } = req.body; if (!name || !policies) { - res.status(400).json({ message: 'Missing required fields: name and policy.' }); + res.status(400).json({ message: req.t('iam.missingRoleFields') }); return; } @@ -56,7 +56,7 @@ export class IamController { for (const statement of policies) { const { valid, reason } = PolicyValidator.isValid(statement as CaslPolicy); if (!valid) { - res.status(400).json({ message: `Invalid policy statement: ${reason}` }); + res.status(400).json({ message: `${req.t('iam.invalidPolicy')} ${reason}` }); return; } } @@ -64,13 +64,13 @@ export class IamController { res.status(201).json(role); } catch (error) { console.log(error); - res.status(500).json({ message: 'Failed to create role.' }); + res.status(500).json({ message: req.t('iam.failedToCreateRole') }); } }; public deleteRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { id } = req.params; @@ -78,19 +78,19 @@ export class IamController { await this.#iamService.deleteRole(id); res.status(204).send(); } catch (error) { - res.status(500).json({ message: 'Failed to delete role.' }); + res.status(500).json({ message: req.t('iam.failedToDeleteRole') }); } }; public updateRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { id } = req.params; const { name, policies } = req.body; if (!name && !policies) { - res.status(400).json({ message: 'Missing fields to update: name or policies.' }); + res.status(400).json({ message: req.t('iam.missingUpdateFields') }); return; } @@ -98,7 +98,7 @@ export class IamController { for (const statement of policies) { const { valid, reason } = PolicyValidator.isValid(statement as CaslPolicy); if (!valid) { - res.status(400).json({ message: `Invalid policy statement: ${reason}` }); + res.status(400).json({ message: `${req.t('iam.invalidPolicy')} ${reason}` }); return; } } @@ -108,7 +108,7 @@ export class IamController { const role = await this.#iamService.updateRole(id, { name, policies }); res.status(200).json(role); } catch (error) { - res.status(500).json({ message: 'Failed to update role.' }); + res.status(500).json({ message: req.t('iam.failedToUpdateRole') }); } }; diff --git a/packages/backend/src/api/controllers/ingestion.controller.ts b/packages/backend/src/api/controllers/ingestion.controller.ts index b7e4d2a..944b2da 100644 --- a/packages/backend/src/api/controllers/ingestion.controller.ts +++ b/packages/backend/src/api/controllers/ingestion.controller.ts @@ -23,13 +23,13 @@ export class IngestionController { public create = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const dto: CreateIngestionSourceDto = req.body; const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const newSource = await IngestionService.create(dto, userId); const safeSource = this.toSafeIngestionSource(newSource); @@ -39,7 +39,7 @@ export class IngestionController { // Return a 400 Bad Request for connection errors return res.status(400).json({ message: - error.message || 'Failed to create ingestion source due to a connection error.', + error.message || req.t('ingestion.failedToCreate'), }); } }; @@ -48,14 +48,14 @@ export class IngestionController { try { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const sources = await IngestionService.findAll(userId); const safeSources = sources.map(this.toSafeIngestionSource); return res.status(200).json(safeSources); } catch (error) { console.error('Find all ingestion sources error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -68,15 +68,15 @@ export class IngestionController { } catch (error) { console.error(`Find ingestion source by id ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public update = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -87,15 +87,15 @@ export class IngestionController { } catch (error) { console.error(`Update ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public delete = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -104,32 +104,32 @@ export class IngestionController { } catch (error) { console.error(`Delete ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public triggerInitialImport = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; await IngestionService.triggerInitialImport(id); - return res.status(202).json({ message: 'Initial import triggered successfully.' }); + return res.status(202).json({ message: req.t('ingestion.initialImportTriggered') }); } catch (error) { console.error(`Trigger initial import for ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public pause = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -139,26 +139,26 @@ export class IngestionController { } catch (error) { console.error(`Pause ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public triggerForceSync = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; await IngestionService.triggerForceSync(id); - return res.status(202).json({ message: 'Force sync triggered successfully.' }); + return res.status(202).json({ message: req.t('ingestion.forceSyncTriggered') }); } catch (error) { console.error(`Trigger force sync for ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/search.controller.ts b/packages/backend/src/api/controllers/search.controller.ts index 7917f93..a7c86ee 100644 --- a/packages/backend/src/api/controllers/search.controller.ts +++ b/packages/backend/src/api/controllers/search.controller.ts @@ -15,12 +15,12 @@ export class SearchController { const userId = req.user?.sub; if (!userId) { - res.status(401).json({ message: 'Unauthorized' }); + res.status(401).json({ message: req.t('errors.unauthorized') }); return; } if (!keywords) { - res.status(400).json({ message: 'Keywords are required' }); + res.status(400).json({ message: req.t('search.keywordsRequired') }); return; } @@ -36,7 +36,7 @@ export class SearchController { res.status(200).json(results); } catch (error) { - const message = error instanceof Error ? error.message : 'An unknown error occurred'; + const message = error instanceof Error ? error.message : req.t('errors.unknown'); res.status(500).json({ message }); } }; diff --git a/packages/backend/src/api/controllers/settings.controller.ts b/packages/backend/src/api/controllers/settings.controller.ts index c6044a2..36d5daa 100644 --- a/packages/backend/src/api/controllers/settings.controller.ts +++ b/packages/backend/src/api/controllers/settings.controller.ts @@ -9,7 +9,7 @@ export const getSettings = async (req: Request, res: Response) => { res.status(200).json(settings); } catch (error) { // A more specific error could be logged here - res.status(500).json({ message: 'Failed to retrieve settings' }); + res.status(500).json({ message: req.t('settings.failedToRetrieve') }); } }; @@ -20,6 +20,6 @@ export const updateSettings = async (req: Request, res: Response) => { res.status(200).json(updatedSettings); } catch (error) { // A more specific error could be logged here - res.status(500).json({ message: 'Failed to update settings' }); + res.status(500).json({ message: req.t('settings.failedToUpdate') }); } }; diff --git a/packages/backend/src/api/controllers/storage.controller.ts b/packages/backend/src/api/controllers/storage.controller.ts index f836692..38697f4 100644 --- a/packages/backend/src/api/controllers/storage.controller.ts +++ b/packages/backend/src/api/controllers/storage.controller.ts @@ -4,13 +4,13 @@ import * as path from 'path'; import { storage as storageConfig } from '../../config/storage'; export class StorageController { - constructor(private storageService: StorageService) {} + constructor(private storageService: StorageService) { } public downloadFile = async (req: Request, res: Response): Promise => { const unsafePath = req.query.path as string; if (!unsafePath) { - res.status(400).send('File path is required'); + res.status(400).send(req.t('storage.filePathRequired')); return; } @@ -24,7 +24,7 @@ export class StorageController { const fullPath = path.join(basePath, normalizedPath); if (!fullPath.startsWith(basePath)) { - res.status(400).send('Invalid file path'); + res.status(400).send(req.t('storage.invalidFilePath')); return; } @@ -34,7 +34,7 @@ export class StorageController { try { const fileExists = await this.storageService.exists(safePath); if (!fileExists) { - res.status(404).send('File not found'); + res.status(404).send(req.t('storage.fileNotFound')); return; } @@ -44,7 +44,7 @@ export class StorageController { fileStream.pipe(res); } catch (error) { console.error('Error downloading file:', error); - res.status(500).send('Error downloading file'); + res.status(500).send(req.t('storage.downloadError')); } }; } diff --git a/packages/backend/src/api/controllers/user.controller.ts b/packages/backend/src/api/controllers/user.controller.ts index 7a10407..940e043 100644 --- a/packages/backend/src/api/controllers/user.controller.ts +++ b/packages/backend/src/api/controllers/user.controller.ts @@ -15,14 +15,14 @@ export const getUsers = async (req: Request, res: Response) => { export const getUser = async (req: Request, res: Response) => { const user = await userService.findById(req.params.id); if (!user) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ message: req.t('user.notFound') }); } res.json(user); }; export const createUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { email, first_name, last_name, password, roleId } = req.body; @@ -35,7 +35,7 @@ export const createUser = async (req: Request, res: Response) => { export const updateUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { email, first_name, last_name, roleId } = req.body; const updatedUser = await userService.updateUser( @@ -44,21 +44,21 @@ export const updateUser = async (req: Request, res: Response) => { roleId ); if (!updatedUser) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ message: req.t('user.notFound') }); } res.json(updatedUser); }; export const deleteUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const userCountResult = await db.select({ count: sql`count(*)` }).from(schema.users); - console.log('iusercount,', userCountResult[0].count); + const isOnlyUser = Number(userCountResult[0].count) === 1; if (isOnlyUser) { return res.status(400).json({ - message: 'You are trying to delete the only user in the database, this is not allowed.', + message: req.t('user.cannotDeleteOnlyUser'), }); } await userService.deleteUser(req.params.id); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1f1b1b3..0d0d9b7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,6 +22,12 @@ import { UserService } from './services/UserService'; import { IamService } from './services/IamService'; import { StorageService } from './services/StorageService'; import { SearchService } from './services/SearchService'; +import { SettingsService } from './services/SettingsService'; +import i18next from 'i18next'; +import FsBackend from 'i18next-fs-backend'; +import i18nextMiddleware from 'i18next-http-middleware'; +import path from 'path'; +import { logger } from './config/logger'; // Load environment variables dotenv.config(); @@ -48,6 +54,25 @@ const searchService = new SearchService(); const searchController = new SearchController(); const iamService = new IamService(); const iamController = new IamController(iamService); +const settingsService = new SettingsService(); + +// --- i18next Initialization --- +const initializeI18next = async () => { + const systemSettings = await settingsService.getSettings(); + const defaultLanguage = systemSettings?.language || 'en'; + logger.info({ language: defaultLanguage }, 'Default language'); + await i18next + .use(FsBackend) + .init({ + lng: defaultLanguage, + fallbackLng: defaultLanguage, + ns: ['translation'], + defaultNS: 'translation', + backend: { + loadPath: path.resolve(__dirname, './locales/{{lng}}/{{ns}}.json'), + }, + }); +}; // --- Express App Initialization --- const app = express(); @@ -70,6 +95,9 @@ app.use('/v1/upload', uploadRouter); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +// i18n middleware +app.use(i18nextMiddleware.handle(i18next)); + app.use('/v1/auth', authRouter); app.use('/v1/iam', iamRouter); app.use('/v1/ingestion-sources', ingestionRouter); @@ -95,15 +123,19 @@ app.get('/', (req, res) => { // --- Server Start --- const startServer = async () => { try { + // Initialize i18next + await initializeI18next(); + logger.info({}, 'i18next initialized'); + // Configure the Meilisearch index on startup - console.log('Configuring email index...'); + logger.info({}, 'Configuring email index...'); await searchService.configureEmailIndex(); app.listen(PORT_BACKEND, () => { - console.log(`Backend listening at http://localhost:${PORT_BACKEND}`); + logger.info({}, `Backend listening at http://localhost:${PORT_BACKEND}`); }); } catch (error) { - console.error('Failed to start the server:', error); + logger.error({ error }, 'Failed to start the server:', error); process.exit(1); } }; diff --git a/packages/backend/src/locales/de/translation.json b/packages/backend/src/locales/de/translation.json new file mode 100644 index 0000000..4a2dbd4 --- /dev/null +++ b/packages/backend/src/locales/de/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-Mail, Passwort und Name sind erforderlich", + "alreadyCompleted": "Die Einrichtung wurde bereits abgeschlossen." + }, + "login": { + "emailAndPasswordRequired": "E-Mail und Passwort sind erforderlich", + "invalidCredentials": "Ungültige Anmeldeinformationen" + } + }, + "errors": { + "internalServerError": "Ein interner Serverfehler ist aufgetreten", + "demoMode": "Dieser Vorgang ist im Demo-Modus nicht zulässig.", + "unauthorized": "Unbefugt", + "unknown": "Ein unbekannter Fehler ist aufgetreten" + }, + "user": { + "notFound": "Benutzer nicht gefunden", + "cannotDeleteOnlyUser": "Sie versuchen, den einzigen Benutzer in der Datenbank zu löschen, dies ist nicht gestattet." + }, + "iam": { + "failedToGetRoles": "Rollen konnten nicht abgerufen werden.", + "roleNotFound": "Rolle nicht gefunden.", + "failedToGetRole": "Rolle konnte nicht abgerufen werden.", + "missingRoleFields": "Fehlende erforderliche Felder: Name und Richtlinie.", + "invalidPolicy": "Ungültige Richtlinienanweisung:", + "failedToCreateRole": "Rolle konnte nicht erstellt werden.", + "failedToDeleteRole": "Rolle konnte nicht gelöscht werden.", + "missingUpdateFields": "Fehlende Felder zum Aktualisieren: Name oder Richtlinien.", + "failedToUpdateRole": "Rolle konnte nicht aktualisiert werden." + }, + "settings": { + "failedToRetrieve": "Einstellungen konnten nicht abgerufen werden", + "failedToUpdate": "Einstellungen konnten nicht aktualisiert werden" + }, + "ingestion": { + "failedToCreate": "Die Erfassungsquelle konnte aufgrund eines Verbindungsfehlers nicht erstellt werden.", + "notFound": "Erfassungsquelle nicht gefunden", + "initialImportTriggered": "Der erstmalige Import wurde erfolgreich ausgelöst.", + "forceSyncTriggered": "Die Zwangssynchronisierung wurde erfolgreich ausgelöst." + }, + "archivedEmail": { + "notFound": "Archivierte E-Mail nicht gefunden" + }, + "search": { + "keywordsRequired": "Schlüsselwörter sind erforderlich" + }, + "storage": { + "filePathRequired": "Dateipfad ist erforderlich", + "invalidFilePath": "Ungültiger Dateipfad", + "fileNotFound": "Datei nicht gefunden", + "downloadError": "Fehler beim Herunterladen der Datei" + } +} diff --git a/packages/backend/src/locales/el/translation.json b/packages/backend/src/locales/el/translation.json new file mode 100644 index 0000000..97c3b87 --- /dev/null +++ b/packages/backend/src/locales/el/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Το email, ο κωδικός πρόσβασης και το όνομα είναι υποχρεωτικά", + "alreadyCompleted": "Η εγκατάσταση έχει ήδη ολοκληρωθεί." + }, + "login": { + "emailAndPasswordRequired": "Το email και ο κωδικός πρόσβασης είναι υποχρεωτικά", + "invalidCredentials": "Μη έγκυρα διαπιστευτήρια" + } + }, + "errors": { + "internalServerError": "Παρουσιάστηκε ένα εσωτερικό σφάλμα διακομιστή", + "demoMode": "Αυτή η λειτουργία δεν επιτρέπεται σε λειτουργία επίδειξης.", + "unauthorized": "Μη εξουσιοδοτημένο", + "unknown": "Παρουσιάστηκε ένα άγνωστο σφάλμα" + }, + "user": { + "notFound": "Ο χρήστης δεν βρέθηκε", + "cannotDeleteOnlyUser": "Προσπαθείτε να διαγράψετε τον μοναδικό χρήστη στη βάση δεδομένων, αυτό δεν επιτρέπεται." + }, + "iam": { + "failedToGetRoles": "Η λήψη των ρόλων απέτυχε.", + "roleNotFound": "Ο ρόλος δεν βρέθηκε.", + "failedToGetRole": "Η λήψη του ρόλου απέτυχε.", + "missingRoleFields": "Λείπουν τα απαιτούμενα πεδία: όνομα και πολιτική.", + "invalidPolicy": "Μη έγκυρη δήλωση πολιτικής:", + "failedToCreateRole": "Η δημιουργία του ρόλου απέτυχε.", + "failedToDeleteRole": "Η διαγραφή του ρόλου απέτυχε.", + "missingUpdateFields": "Λείπουν πεδία για ενημέρωση: όνομα ή πολιτικές.", + "failedToUpdateRole": "Η ενημέρωση του ρόλου απέτυχε." + }, + "settings": { + "failedToRetrieve": "Η ανάκτηση των ρυθμίσεων απέτυχε", + "failedToUpdate": "Η ενημέρωση των ρυθμίσεων απέτυχε" + }, + "ingestion": { + "failedToCreate": "Η δημιουργία της πηγής πρόσληψης απέτυχε λόγω σφάλματος σύνδεσης.", + "notFound": "Η πηγή πρόσληψης δεν βρέθηκε", + "initialImportTriggered": "Η αρχική εισαγωγή ενεργοποιήθηκε με επιτυχία.", + "forceSyncTriggered": "Ο εξαναγκασμένος συγχρονισμός ενεργοποιήθηκε με επιτυχία." + }, + "archivedEmail": { + "notFound": "Το αρχειοθετημένο email δεν βρέθηκε" + }, + "search": { + "keywordsRequired": "Απαιτούνται λέξεις-κλειδιά" + }, + "storage": { + "filePathRequired": "Απαιτείται η διαδρομή του αρχείου", + "invalidFilePath": "Μη έγκυρη διαδρομή αρχείου", + "fileNotFound": "Το αρχείο δεν βρέθηκε", + "downloadError": "Σφάλμα κατά τη λήψη του αρχείου" + } +} diff --git a/packages/backend/src/locales/en/translation.json b/packages/backend/src/locales/en/translation.json new file mode 100644 index 0000000..d2c8c4d --- /dev/null +++ b/packages/backend/src/locales/en/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Email, password, and name are required", + "alreadyCompleted": "Setup has already been completed." + }, + "login": { + "emailAndPasswordRequired": "Email and password are required", + "invalidCredentials": "Invalid credentials this is a translation" + } + }, + "errors": { + "internalServerError": "An internal server error occurred", + "demoMode": "This operation is not allowed in demo mode.", + "unauthorized": "Unauthorized", + "unknown": "An unknown error occurred" + }, + "user": { + "notFound": "User not found", + "cannotDeleteOnlyUser": "You are trying to delete the only user in the database, this is not allowed." + }, + "iam": { + "failedToGetRoles": "Failed to get roles.", + "roleNotFound": "Role not found.", + "failedToGetRole": "Failed to get role.", + "missingRoleFields": "Missing required fields: name and policy.", + "invalidPolicy": "Invalid policy statement:", + "failedToCreateRole": "Failed to create role.", + "failedToDeleteRole": "Failed to delete role.", + "missingUpdateFields": "Missing fields to update: name or policies.", + "failedToUpdateRole": "Failed to update role." + }, + "settings": { + "failedToRetrieve": "Failed to retrieve settings", + "failedToUpdate": "Failed to update settings" + }, + "ingestion": { + "failedToCreate": "Failed to create ingestion source due to a connection error.", + "notFound": "Ingestion source not found", + "initialImportTriggered": "Initial import triggered successfully.", + "forceSyncTriggered": "Force sync triggered successfully." + }, + "archivedEmail": { + "notFound": "Archived email not found" + }, + "search": { + "keywordsRequired": "Keywords are required" + }, + "storage": { + "filePathRequired": "File path is required", + "invalidFilePath": "Invalid file path", + "fileNotFound": "File not found", + "downloadError": "Error downloading file" + } +} diff --git a/packages/backend/src/locales/es/translation.json b/packages/backend/src/locales/es/translation.json new file mode 100644 index 0000000..2222d95 --- /dev/null +++ b/packages/backend/src/locales/es/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Se requieren correo electrónico, contraseña y nombre", + "alreadyCompleted": "La configuración ya se ha completado." + }, + "login": { + "emailAndPasswordRequired": "Se requieren correo electrónico y contraseña", + "invalidCredentials": "Credenciales no válidas" + } + }, + "errors": { + "internalServerError": "Ocurrió un error interno del servidor", + "demoMode": "Esta operación no está permitida en modo de demostración.", + "unauthorized": "No autorizado", + "unknown": "Ocurrió un error desconocido" + }, + "user": { + "notFound": "Usuario no encontrado", + "cannotDeleteOnlyUser": "Está intentando eliminar el único usuario de la base de datos, esto no está permitido." + }, + "iam": { + "failedToGetRoles": "Error al obtener los roles.", + "roleNotFound": "Rol no encontrado.", + "failedToGetRole": "Error al obtener el rol.", + "missingRoleFields": "Faltan campos obligatorios: nombre y política.", + "invalidPolicy": "Declaración de política no válida:", + "failedToCreateRole": "Error al crear el rol.", + "failedToDeleteRole": "Error al eliminar el rol.", + "missingUpdateFields": "Faltan campos para actualizar: nombre o políticas.", + "failedToUpdateRole": "Error al actualizar el rol." + }, + "settings": { + "failedToRetrieve": "Error al recuperar la configuración", + "failedToUpdate": "Error al actualizar la configuración" + }, + "ingestion": { + "failedToCreate": "Error al crear la fuente de ingesta debido a un error de conexión.", + "notFound": "Fuente de ingesta no encontrada", + "initialImportTriggered": "Importación inicial activada correctamente.", + "forceSyncTriggered": "Sincronización forzada activada correctamente." + }, + "archivedEmail": { + "notFound": "Correo electrónico archivado no encontrado" + }, + "search": { + "keywordsRequired": "Se requieren palabras clave" + }, + "storage": { + "filePathRequired": "Se requiere la ruta del archivo", + "invalidFilePath": "Ruta de archivo no válida", + "fileNotFound": "Archivo no encontrado", + "downloadError": "Error al descargar el archivo" + } +} diff --git a/packages/backend/src/locales/et/translation.json b/packages/backend/src/locales/et/translation.json new file mode 100644 index 0000000..f07b28a --- /dev/null +++ b/packages/backend/src/locales/et/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-post, parool ja nimi on kohustuslikud", + "alreadyCompleted": "Seadistamine on juba lõpule viidud." + }, + "login": { + "emailAndPasswordRequired": "E-post ja parool on kohustuslikud", + "invalidCredentials": "Valed sisselogimisandmed" + } + }, + "errors": { + "internalServerError": "Ilmnes sisemine serveri viga", + "demoMode": "See toiming pole demo-režiimis lubatud.", + "unauthorized": "Volitamata", + "unknown": "Ilmnes tundmatu viga" + }, + "user": { + "notFound": "Kasutajat ei leitud", + "cannotDeleteOnlyUser": "Püüate kustutada andmebaasi ainsat kasutajat, see pole lubatud." + }, + "iam": { + "failedToGetRoles": "Rollide hankimine ebaõnnestus.", + "roleNotFound": "Rolli ei leitud.", + "failedToGetRole": "Rolli hankimine ebaõnnestus.", + "missingRoleFields": "Puuduvad kohustuslikud väljad: nimi ja poliitika.", + "invalidPolicy": "Kehtetu poliitika avaldus:", + "failedToCreateRole": "Rolli loomine ebaõnnestus.", + "failedToDeleteRole": "Rolli kustutamine ebaõnnestus.", + "missingUpdateFields": "Uuendatavad väljad puuduvad: nimi või poliitikad.", + "failedToUpdateRole": "Rolli värskendamine ebaõnnestus." + }, + "settings": { + "failedToRetrieve": "Seadete toomine ebaõnnestus", + "failedToUpdate": "Seadete värskendamine ebaõnnestus" + }, + "ingestion": { + "failedToCreate": "Söötmeallika loomine ebaõnnestus ühenduse vea tõttu.", + "notFound": "Söötmeallikat ei leitud", + "initialImportTriggered": "Esialgne import käivitati edukalt.", + "forceSyncTriggered": "Sunnitud sünkroonimine käivitati edukalt." + }, + "archivedEmail": { + "notFound": "Arhiveeritud e-kirja ei leitud" + }, + "search": { + "keywordsRequired": "Märksõnad on kohustuslikud" + }, + "storage": { + "filePathRequired": "Faili tee on kohustuslik", + "invalidFilePath": "Kehtetu faili tee", + "fileNotFound": "Faili ei leitud", + "downloadError": "Faili allalaadimisel ilmnes viga" + } +} diff --git a/packages/backend/src/locales/fr/translation.json b/packages/backend/src/locales/fr/translation.json new file mode 100644 index 0000000..bee47bc --- /dev/null +++ b/packages/backend/src/locales/fr/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-mail, mot de passe et nom sont requis", + "alreadyCompleted": "La configuration est déjà terminée." + }, + "login": { + "emailAndPasswordRequired": "E-mail et mot de passe sont requis", + "invalidCredentials": "Identifiants non valides" + } + }, + "errors": { + "internalServerError": "Une erreur interne du serveur est survenue", + "demoMode": "Cette opération n'est pas autorisée en mode démo.", + "unauthorized": "Non autorisé", + "unknown": "Une erreur inconnue est survenue" + }, + "user": { + "notFound": "Utilisateur non trouvé", + "cannotDeleteOnlyUser": "Vous essayez de supprimer le seul utilisateur de la base de données, ce n'est pas autorisé." + }, + "iam": { + "failedToGetRoles": "Échec de la récupération des rôles.", + "roleNotFound": "Rôle non trouvé.", + "failedToGetRole": "Échec de la récupération du rôle.", + "missingRoleFields": "Champs obligatoires manquants : nom et politique.", + "invalidPolicy": "Déclaration de politique non valide :", + "failedToCreateRole": "Échec de la création du rôle.", + "failedToDeleteRole": "Échec de la suppression du rôle.", + "missingUpdateFields": "Champs à mettre à jour manquants : nom ou politiques.", + "failedToUpdateRole": "Échec de la mise à jour du rôle." + }, + "settings": { + "failedToRetrieve": "Échec de la récupération des paramètres", + "failedToUpdate": "Échec de la mise à jour des paramètres" + }, + "ingestion": { + "failedToCreate": "Échec de la création de la source d'ingestion en raison d'une erreur de connexion.", + "notFound": "Source d'ingestion non trouvée", + "initialImportTriggered": "Importation initiale déclenchée avec succès.", + "forceSyncTriggered": "Synchronisation forcée déclenchée avec succès." + }, + "archivedEmail": { + "notFound": "E-mail archivé non trouvé" + }, + "search": { + "keywordsRequired": "Mots-clés requis" + }, + "storage": { + "filePathRequired": "Chemin du fichier requis", + "invalidFilePath": "Chemin de fichier non valide", + "fileNotFound": "Fichier non trouvé", + "downloadError": "Erreur lors du téléchargement du fichier" + } +} diff --git a/packages/backend/src/locales/it/translation.json b/packages/backend/src/locales/it/translation.json new file mode 100644 index 0000000..d70a798 --- /dev/null +++ b/packages/backend/src/locales/it/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Email, password e nome sono obbligatori", + "alreadyCompleted": "La configurazione è già stata completata." + }, + "login": { + "emailAndPasswordRequired": "Email e password sono obbligatori", + "invalidCredentials": "Credenziali non valide" + } + }, + "errors": { + "internalServerError": "Si è verificato un errore interno del server", + "demoMode": "Questa operazione non è consentita in modalità demo.", + "unauthorized": "Non autorizzato", + "unknown": "Si è verificato un errore sconosciuto" + }, + "user": { + "notFound": "Utente non trovato", + "cannotDeleteOnlyUser": "Stai tentando di eliminare l'unico utente nel database, ciò non è consentito." + }, + "iam": { + "failedToGetRoles": "Impossibile ottenere i ruoli.", + "roleNotFound": "Ruolo non trovato.", + "failedToGetRole": "Impossibile ottenere il ruolo.", + "missingRoleFields": "Campi obbligatori mancanti: nome e politica.", + "invalidPolicy": "Dichiarazione di politica non valida:", + "failedToCreateRole": "Impossibile creare il ruolo.", + "failedToDeleteRole": "Impossibile eliminare il ruolo.", + "missingUpdateFields": "Campi da aggiornare mancanti: nome o politiche.", + "failedToUpdateRole": "Impossibile aggiornare il ruolo." + }, + "settings": { + "failedToRetrieve": "Impossibile recuperare le impostazioni", + "failedToUpdate": "Impossibile aggiornare le impostazioni" + }, + "ingestion": { + "failedToCreate": "Impossibile creare l'origine di ingestione a causa di un errore di connessione.", + "notFound": "Origine di ingestione non trovata", + "initialImportTriggered": "Importazione iniziale attivata con successo.", + "forceSyncTriggered": "Sincronizzazione forzata attivata con successo." + }, + "archivedEmail": { + "notFound": "Email archiviata non trovata" + }, + "search": { + "keywordsRequired": "Le parole chiave sono obbligatorie" + }, + "storage": { + "filePathRequired": "Il percorso del file è obbligatorio", + "invalidFilePath": "Percorso del file non valido", + "fileNotFound": "File non trovato", + "downloadError": "Errore durante il download del file" + } +} diff --git a/packages/backend/src/locales/ja/translation.json b/packages/backend/src/locales/ja/translation.json new file mode 100644 index 0000000..ceebeba --- /dev/null +++ b/packages/backend/src/locales/ja/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "メールアドレス、パスワード、氏名が必要です", + "alreadyCompleted": "セットアップは既に完了しています。" + }, + "login": { + "emailAndPasswordRequired": "メールアドレスとパスワードが必要です", + "invalidCredentials": "認証情報が無効です" + } + }, + "errors": { + "internalServerError": "内部サーバーエラーが発生しました", + "demoMode": "この操作はデモモードでは許可されていません。", + "unauthorized": "不正なアクセスです", + "unknown": "不明なエラーが発生しました" + }, + "user": { + "notFound": "ユーザーが見つかりません", + "cannotDeleteOnlyUser": "データベース内の唯一のユーザーを削除しようとしていますが、これは許可されていません。" + }, + "iam": { + "failedToGetRoles": "ロールの取得に失敗しました。", + "roleNotFound": "ロールが見つかりません。", + "failedToGetRole": "ロールの取得に失敗しました。", + "missingRoleFields": "必須フィールドがありません:名前とポリシー。", + "invalidPolicy": "無効なポリシーステートメント:", + "failedToCreateRole": "ロールの作成に失敗しました。", + "failedToDeleteRole": "ロールの削除に失敗しました。", + "missingUpdateFields": "更新するフィールドがありません:名前またはポリシー。", + "failedToUpdateRole": "ロールの更新に失敗しました。" + }, + "settings": { + "failedToRetrieve": "設定の取得に失敗しました", + "failedToUpdate": "設定の更新に失敗しました" + }, + "ingestion": { + "failedToCreate": "接続エラーのため、取り込みソースの作成に失敗しました。", + "notFound": "取り込みソースが見つかりません", + "initialImportTriggered": "初期インポートが正常にトリガーされました。", + "forceSyncTriggered": "強制同期が正常にトリガーされました。" + }, + "archivedEmail": { + "notFound": "アーカイブされたメールが見つかりません" + }, + "search": { + "keywordsRequired": "キーワードが必要です" + }, + "storage": { + "filePathRequired": "ファイルパスが必要です", + "invalidFilePath": "無効なファイルパスです", + "fileNotFound": "ファイルが見つかりません", + "downloadError": "ファイルのダウンロード中にエラーが発生しました" + } +} diff --git a/packages/backend/src/locales/nl/translation.json b/packages/backend/src/locales/nl/translation.json new file mode 100644 index 0000000..d5a41f4 --- /dev/null +++ b/packages/backend/src/locales/nl/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-mail, wachtwoord en naam zijn verplicht", + "alreadyCompleted": "De installatie is al voltooid." + }, + "login": { + "emailAndPasswordRequired": "E-mail en wachtwoord zijn verplicht", + "invalidCredentials": "Ongeldige inloggegevens" + } + }, + "errors": { + "internalServerError": "Er is een interne serverfout opgetreden", + "demoMode": "Deze bewerking is niet toegestaan in de demomodus.", + "unauthorized": "Ongeautoriseerd", + "unknown": "Er is een onbekende fout opgetreden" + }, + "user": { + "notFound": "Gebruiker niet gevonden", + "cannotDeleteOnlyUser": "U probeert de enige gebruiker in de database te verwijderen, dit is niet toegestaan." + }, + "iam": { + "failedToGetRoles": "Rollen ophalen mislukt.", + "roleNotFound": "Rol niet gevonden.", + "failedToGetRole": "Rol ophalen mislukt.", + "missingRoleFields": "Verplichte velden ontbreken: naam en beleid.", + "invalidPolicy": "Ongeldige beleidsverklaring:", + "failedToCreateRole": "Rol aanmaken mislukt.", + "failedToDeleteRole": "Rol verwijderen mislukt.", + "missingUpdateFields": "Te updaten velden ontbreken: naam of beleid.", + "failedToUpdateRole": "Rol bijwerken mislukt." + }, + "settings": { + "failedToRetrieve": "Instellingen ophalen mislukt", + "failedToUpdate": "Instellingen bijwerken mislukt" + }, + "ingestion": { + "failedToCreate": "Aanmaken van de opnamebron mislukt vanwege een verbindingsfout.", + "notFound": "Opnamebron niet gevonden", + "initialImportTriggered": "Initiële import succesvol gestart.", + "forceSyncTriggered": "Geforceerde synchronisatie succesvol gestart." + }, + "archivedEmail": { + "notFound": "Gearchiveerde e-mail niet gevonden" + }, + "search": { + "keywordsRequired": "Trefwoorden zijn verplicht" + }, + "storage": { + "filePathRequired": "Bestandspad is verplicht", + "invalidFilePath": "Ongeldig bestandspad", + "fileNotFound": "Bestand niet gevonden", + "downloadError": "Fout bij het downloaden van het bestand" + } +} diff --git a/packages/backend/src/locales/pt/translation.json b/packages/backend/src/locales/pt/translation.json new file mode 100644 index 0000000..b85dea7 --- /dev/null +++ b/packages/backend/src/locales/pt/translation.json @@ -0,0 +1,55 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-mail, senha e nome são obrigatórios", + "alreadyCompleted": "A configuração já foi concluída." + }, + "login": { + "emailAndPasswordRequired": "E-mail and senha são obrigatórios", + "invalidCredentials": "Credenciais inválidas" + } + }, + "errors": { + "internalServerError": "Ocorreu um erro interno do servidor", + "demoMode": "Esta operação não é permitida no modo de demonstração.", + "unauthorized": "Não autorizado", + "unknown": "Ocorreu um erro desconhecido" + }, + "user": { + "notFound": "Usuário não encontrado", + "cannotDeleteOnlyUser": "Você está tentando excluir o único usuário no banco de dados, isso não é permitido." + }, + "iam": { + "failedToGetRoles": "Falha ao obter as funções.", + "roleNotFound": "Função não encontrada.", + "failedToGetRole": "Falha ao obter a função.", + "missingRoleFields": "Campos obrigatórios ausentes: nome e política.", + "invalidPolicy": "Declaração de política inválida:", + "failedToCreateRole": "Falha ao criar a função.", + "failedToDeleteRole": "Falha ao excluir a função.", + "missingUpdateFields": "Campos ausentes para atualizar: nome ou políticas.", + "failedToUpdateRole": "Falha ao atualizar a função." + }, + "settings": { + "failedToRetrieve": "Falha ao recuperar as configurações", + "failedToUpdate": "Falha ao atualizar as configurações" + }, + "ingestion": { + "failedToCreate": "Falha ao criar a fonte de ingestão devido a um erro de conexão.", + "notFound": "Fonte de ingestão não encontrada", + "initialImportTriggered": "Importação inicial acionada com sucesso.", + "forceSyncTriggered": "Sincronização forçada acionada com sucesso." + }, + "archivedEmail": { + "notFound": "E-mail arquivado não encontrado" + }, + "search": { + "keywordsRequired": "Palavras-chave são obrigatórias" + }, + "storage": { + "filePathRequired": "O caminho do arquivo é obrigatório", + "invalidFilePath": "Caminho de arquivo inválido", + "fileNotFound": "Arquivo não encontrado", + "downloadError": "Erro ao baixar o arquivo" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40d3da9..3a2376c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,15 @@ importers: googleapis: specifier: ^152.0.0 version: 152.0.0 + i18next: + specifier: ^25.4.2 + version: 25.4.2(typescript@5.8.3) + i18next-fs-backend: + specifier: ^2.6.0 + version: 2.6.0 + i18next-http-middleware: + specifier: ^3.8.0 + version: 3.8.0 imapflow: specifier: ^1.0.191 version: 1.0.191 @@ -3052,6 +3061,20 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + i18next-fs-backend@2.6.0: + resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==} + + i18next-http-middleware@3.8.0: + resolution: {integrity: sha512-G3DpT/ibwOx6BCI5A2xUmZ2ybUDUrI6emCdEE7AX9TKvz+WzA6peuyv0BxC8kIrJHtrdnmUWwk4rDN9nxWvsFg==} + + i18next@25.4.2: + resolution: {integrity: sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -7931,6 +7954,16 @@ snapshots: ms: 2.1.3 optional: true + i18next-fs-backend@2.6.0: {} + + i18next-http-middleware@3.8.0: {} + + i18next@25.4.2(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + optionalDependencies: + typescript: 5.8.3 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2