main
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s

This commit is contained in:
Harun CAN
2026-02-10 12:27:14 +03:00
parent 80f53511d8
commit fc88faddb9
141 changed files with 35961 additions and 101 deletions

View File

@@ -0,0 +1,512 @@
// Translation Management Service - Admin translation management
// Path: src/modules/i18n/services/translation-management.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
export interface TranslationKey {
id: string;
key: string;
namespace: string;
description?: string;
context?: string;
translations: TranslationRecord[];
createdAt: Date;
updatedAt: Date;
}
export interface TranslationRecord {
id: string;
keyId: string;
locale: string;
value: string;
status: 'draft' | 'review' | 'approved' | 'published';
translatedBy?: string;
reviewedBy?: string;
createdAt: Date;
updatedAt: Date;
}
export interface TranslationExport {
locale: string;
namespace?: string;
format: 'json' | 'csv' | 'xliff' | 'po';
data: string;
}
export interface TranslationStats {
totalKeys: number;
byNamespace: Record<string, number>;
byLocale: Record<string, {
translated: number;
pending: number;
percentage: number;
}>;
lastUpdated: Date;
}
@Injectable()
export class TranslationManagementService {
private readonly logger = new Logger(TranslationManagementService.name);
// In-memory storage (in production, use database)
private readonly translationKeys = new Map<string, TranslationKey>();
private readonly translationRecords = new Map<string, TranslationRecord>();
constructor(private readonly prisma: PrismaService) {
this.initializeDefaultKeys();
}
/**
* Initialize default translation keys
*/
private initializeDefaultKeys(): void {
const defaultNamespaces = ['common', 'auth', 'content', 'analytics', 'errors'];
const defaultLocales = ['en', 'tr', 'es', 'fr', 'de', 'zh', 'pt', 'ar', 'ru', 'ja'];
let keyIndex = 0;
for (const namespace of defaultNamespaces) {
for (let i = 0; i < 10; i++) {
const keyId = `key-${keyIndex++}`;
const key: TranslationKey = {
id: keyId,
key: `${namespace}.item${i}`,
namespace,
description: `Translation for ${namespace} item ${i}`,
translations: [],
createdAt: new Date(),
updatedAt: new Date(),
};
// Create translation records for each locale
for (const locale of defaultLocales) {
const recordId = `record-${keyId}-${locale}`;
const record: TranslationRecord = {
id: recordId,
keyId,
locale,
value: `[${locale.toUpperCase()}] ${namespace}.item${i}`,
status: 'published',
createdAt: new Date(),
updatedAt: new Date(),
};
this.translationRecords.set(recordId, record);
key.translations.push(record);
}
this.translationKeys.set(keyId, key);
}
}
}
// ========== KEY MANAGEMENT ==========
/**
* Create a new translation key
*/
createKey(input: {
key: string;
namespace: string;
description?: string;
context?: string;
}): TranslationKey {
const id = `key-${Date.now()}-${Math.random().toString(36).substring(7)}`;
const translationKey: TranslationKey = {
id,
key: input.key,
namespace: input.namespace,
description: input.description,
context: input.context,
translations: [],
createdAt: new Date(),
updatedAt: new Date(),
};
this.translationKeys.set(id, translationKey);
return translationKey;
}
/**
* Get a translation key by ID
*/
getKey(keyId: string): TranslationKey | null {
return this.translationKeys.get(keyId) || null;
}
/**
* Get key by key string
*/
getKeyByString(keyString: string): TranslationKey | null {
for (const key of this.translationKeys.values()) {
if (key.key === keyString) {
return key;
}
}
return null;
}
/**
* List all keys with filtering
*/
listKeys(options?: {
namespace?: string;
search?: string;
page?: number;
limit?: number;
}): { keys: TranslationKey[]; total: number } {
let keys = Array.from(this.translationKeys.values());
// Filter by namespace
if (options?.namespace) {
keys = keys.filter(k => k.namespace === options.namespace);
}
// Filter by search
if (options?.search) {
const search = options.search.toLowerCase();
keys = keys.filter(k =>
k.key.toLowerCase().includes(search) ||
k.description?.toLowerCase().includes(search)
);
}
// Paginate
const page = options?.page || 1;
const limit = options?.limit || 20;
const start = (page - 1) * limit;
const paginatedKeys = keys.slice(start, start + limit);
return { keys: paginatedKeys, total: keys.length };
}
/**
* Update a translation key
*/
updateKey(keyId: string, updates: Partial<Pick<TranslationKey, 'description' | 'context'>>): TranslationKey | null {
const key = this.translationKeys.get(keyId);
if (!key) return null;
if (updates.description !== undefined) key.description = updates.description;
if (updates.context !== undefined) key.context = updates.context;
key.updatedAt = new Date();
return key;
}
/**
* Delete a translation key
*/
deleteKey(keyId: string): boolean {
const key = this.translationKeys.get(keyId);
if (!key) return false;
// Delete all translation records
for (const record of key.translations) {
this.translationRecords.delete(record.id);
}
return this.translationKeys.delete(keyId);
}
// ========== TRANSLATION MANAGEMENT ==========
/**
* Add or update translation for a key
*/
setTranslation(keyId: string, locale: string, value: string, status: TranslationRecord['status'] = 'draft'): TranslationRecord | null {
const key = this.translationKeys.get(keyId);
if (!key) return null;
// Find existing record
const existingIndex = key.translations.findIndex(t => t.locale === locale);
if (existingIndex >= 0) {
// Update existing
const record = key.translations[existingIndex];
record.value = value;
record.status = status;
record.updatedAt = new Date();
return record;
} else {
// Create new
const recordId = `record-${keyId}-${locale}`;
const record: TranslationRecord = {
id: recordId,
keyId,
locale,
value,
status,
createdAt: new Date(),
updatedAt: new Date(),
};
key.translations.push(record);
this.translationRecords.set(recordId, record);
return record;
}
}
/**
* Bulk update translations
*/
bulkSetTranslations(translations: Array<{
keyId: string;
locale: string;
value: string;
status?: TranslationRecord['status'];
}>): number {
let updated = 0;
for (const t of translations) {
const result = this.setTranslation(t.keyId, t.locale, t.value, t.status);
if (result) updated++;
}
return updated;
}
/**
* Update translation status
*/
updateTranslationStatus(recordId: string, status: TranslationRecord['status'], reviewedBy?: string): TranslationRecord | null {
const record = this.translationRecords.get(recordId);
if (!record) return null;
record.status = status;
record.updatedAt = new Date();
if (reviewedBy) record.reviewedBy = reviewedBy;
return record;
}
/**
* Get missing translations for a locale
*/
getMissingTranslations(locale: string): TranslationKey[] {
const missing: TranslationKey[] = [];
for (const key of this.translationKeys.values()) {
const hasTranslation = key.translations.some(t => t.locale === locale && t.value);
if (!hasTranslation) {
missing.push(key);
}
}
return missing;
}
// ========== IMPORT/EXPORT ==========
/**
* Export translations
*/
exportTranslations(locale: string, options?: {
namespace?: string;
format?: 'json' | 'csv' | 'xliff' | 'po';
includeMetadata?: boolean;
}): TranslationExport {
const format = options?.format || 'json';
let keys = Array.from(this.translationKeys.values());
if (options?.namespace) {
keys = keys.filter(k => k.namespace === options.namespace);
}
const translations: Record<string, string> = {};
for (const key of keys) {
const record = key.translations.find(t => t.locale === locale);
if (record) {
translations[key.key] = record.value;
}
}
let data: string;
switch (format) {
case 'json':
data = JSON.stringify(translations, null, 2);
break;
case 'csv':
data = this.toCSV(translations);
break;
case 'xliff':
data = this.toXLIFF(translations, locale);
break;
case 'po':
data = this.toPO(translations);
break;
default:
data = JSON.stringify(translations, null, 2);
}
return { locale, namespace: options?.namespace, format, data };
}
/**
* Import translations
*/
importTranslations(locale: string, data: string, format: 'json' | 'csv'): number {
let translations: Record<string, string>;
if (format === 'json') {
translations = JSON.parse(data);
} else {
translations = this.fromCSV(data);
}
let imported = 0;
for (const [keyString, value] of Object.entries(translations)) {
const key = this.getKeyByString(keyString);
if (key) {
this.setTranslation(key.id, locale, value, 'draft');
imported++;
} else {
// Create new key
const namespace = keyString.split('.')[0];
const newKey = this.createKey({ key: keyString, namespace });
this.setTranslation(newKey.id, locale, value, 'draft');
imported++;
}
}
return imported;
}
// ========== STATISTICS ==========
/**
* Get translation statistics
*/
getStats(): TranslationStats {
const locales = ['en', 'tr', 'es', 'fr', 'de', 'zh', 'pt', 'ar', 'ru', 'ja'];
const keys = Array.from(this.translationKeys.values());
const byNamespace: Record<string, number> = {};
for (const key of keys) {
byNamespace[key.namespace] = (byNamespace[key.namespace] || 0) + 1;
}
const byLocale: Record<string, { translated: number; pending: number; percentage: number }> = {};
for (const locale of locales) {
let translated = 0;
let pending = 0;
for (const key of keys) {
const record = key.translations.find(t => t.locale === locale);
if (record && record.value) {
if (record.status === 'published') {
translated++;
} else {
pending++;
}
}
}
byLocale[locale] = {
translated,
pending,
percentage: keys.length > 0 ? Math.round((translated / keys.length) * 100) : 0,
};
}
return {
totalKeys: keys.length,
byNamespace,
byLocale,
lastUpdated: new Date(),
};
}
// ========== NAMESPACES ==========
/**
* Get all namespaces
*/
getNamespaces(): string[] {
const namespaces = new Set<string>();
for (const key of this.translationKeys.values()) {
namespaces.add(key.namespace);
}
return Array.from(namespaces);
}
/**
* Rename namespace
*/
renameNamespace(oldName: string, newName: string): number {
let renamed = 0;
for (const key of this.translationKeys.values()) {
if (key.namespace === oldName) {
key.namespace = newName;
key.key = key.key.replace(`${oldName}.`, `${newName}.`);
key.updatedAt = new Date();
renamed++;
}
}
return renamed;
}
// ========== HELPER METHODS ==========
private toCSV(translations: Record<string, string>): string {
const lines = ['key,value'];
for (const [key, value] of Object.entries(translations)) {
const escapedValue = value.replace(/"/g, '""');
lines.push(`"${key}","${escapedValue}"`);
}
return lines.join('\n');
}
private fromCSV(data: string): Record<string, string> {
const result: Record<string, string> = {};
const lines = data.split('\n').slice(1); // Skip header
for (const line of lines) {
const match = line.match(/^"([^"]+)","(.+)"$/);
if (match) {
result[match[1]] = match[2].replace(/""/g, '"');
}
}
return result;
}
private toXLIFF(translations: Record<string, string>, locale: string): string {
let xliff = `<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="${locale}" datatype="plaintext">
<body>`;
for (const [key, value] of Object.entries(translations)) {
xliff += `
<trans-unit id="${key}">
<source>${key}</source>
<target>${this.escapeXml(value)}</target>
</trans-unit>`;
}
xliff += `
</body>
</file>
</xliff>`;
return xliff;
}
private toPO(translations: Record<string, string>): string {
let po = '';
for (const [key, value] of Object.entries(translations)) {
po += `
msgid "${key}"
msgstr "${value.replace(/"/g, '\\"')}"
`;
}
return po.trim();
}
private escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
}

View File

@@ -0,0 +1,228 @@
// Translation Service - Key-based translations
// Path: src/modules/i18n/services/translation.service.ts
import { Injectable, Logger } from '@nestjs/common';
interface TranslationEntry {
key: string;
translations: Record<string, string>;
context?: string;
pluralForms?: Record<string, Record<string, string>>;
}
@Injectable()
export class TranslationService {
private readonly logger = new Logger(TranslationService.name);
private readonly translations = new Map<string, TranslationEntry>();
private fallbackLocale = 'en';
constructor() {
this.loadDefaultTranslations();
}
/**
* Load default system translations
*/
private loadDefaultTranslations(): void {
// Common UI translations
this.addTranslations('common', {
'common.loading': { en: 'Loading...', tr: 'Yükleniyor...', es: 'Cargando...', fr: 'Chargement...', de: 'Laden...', zh: '加载中...', pt: 'Carregando...', ar: 'جار التحميل...', ru: 'Загрузка...', ja: '読み込み中...' },
'common.save': { en: 'Save', tr: 'Kaydet', es: 'Guardar', fr: 'Enregistrer', de: 'Speichern', zh: '保存', pt: 'Salvar', ar: 'حفظ', ru: 'Сохранить', ja: '保存' },
'common.cancel': { en: 'Cancel', tr: 'İptal', es: 'Cancelar', fr: 'Annuler', de: 'Abbrechen', zh: '取消', pt: 'Cancelar', ar: 'إلغاء', ru: 'Отмена', ja: 'キャンセル' },
'common.delete': { en: 'Delete', tr: 'Sil', es: 'Eliminar', fr: 'Supprimer', de: 'Löschen', zh: '删除', pt: 'Excluir', ar: 'حذف', ru: 'Удалить', ja: '削除' },
'common.edit': { en: 'Edit', tr: 'Düzenle', es: 'Editar', fr: 'Modifier', de: 'Bearbeiten', zh: '编辑', pt: 'Editar', ar: 'تحرير', ru: 'Редактировать', ja: '編集' },
'common.create': { en: 'Create', tr: 'Oluştur', es: 'Crear', fr: 'Créer', de: 'Erstellen', zh: '创建', pt: 'Criar', ar: 'إنشاء', ru: 'Создать', ja: '作成' },
'common.search': { en: 'Search', tr: 'Ara', es: 'Buscar', fr: 'Rechercher', de: 'Suchen', zh: '搜索', pt: 'Pesquisar', ar: 'بحث', ru: 'Поиск', ja: '検索' },
'common.back': { en: 'Back', tr: 'Geri', es: 'Volver', fr: 'Retour', de: 'Zurück', zh: '返回', pt: 'Voltar', ar: 'رجوع', ru: 'Назад', ja: '戻る' },
'common.next': { en: 'Next', tr: 'İleri', es: 'Siguiente', fr: 'Suivant', de: 'Weiter', zh: '下一步', pt: 'Próximo', ar: 'التالي', ru: 'Далее', ja: '次へ' },
'common.confirm': { en: 'Confirm', tr: 'Onayla', es: 'Confirmar', fr: 'Confirmer', de: 'Bestätigen', zh: '确认', pt: 'Confirmar', ar: 'تأكيد', ru: 'Подтвердить', ja: '確認' },
});
// Error messages
this.addTranslations('errors', {
'error.generic': { en: 'Something went wrong', tr: 'Bir şeyler yanlış gitti', es: 'Algo salió mal', fr: 'Une erreur est survenue', de: 'Etwas ist schiefgelaufen', zh: '出了点问题', pt: 'Algo deu errado', ar: 'حدث خطأ ما', ru: 'Что-то пошло не так', ja: '問題が発生しました' },
'error.notFound': { en: 'Not found', tr: 'Bulunamadı', es: 'No encontrado', fr: 'Non trouvé', de: 'Nicht gefunden', zh: '未找到', pt: 'Não encontrado', ar: 'غير موجود', ru: 'Не найдено', ja: '見つかりません' },
'error.unauthorized': { en: 'Unauthorized', tr: 'Yetkisiz', es: 'No autorizado', fr: 'Non autorisé', de: 'Nicht autorisiert', zh: '未授权', pt: 'Não autorizado', ar: 'غير مصرح', ru: 'Не авторизован', ja: '権限がありません' },
'error.forbidden': { en: 'Access denied', tr: 'Erişim engellendi', es: 'Acceso denegado', fr: 'Accès refusé', de: 'Zugriff verweigert', zh: '拒绝访问', pt: 'Acesso negado', ar: 'تم رفض الوصول', ru: 'Доступ запрещен', ja: 'アクセスが拒否されました' },
'error.validation': { en: 'Validation error', tr: 'Doğrulama hatası', es: 'Error de validación', fr: 'Erreur de validation', de: 'Validierungsfehler', zh: '验证错误', pt: 'Erro de validação', ar: 'خطأ في التحقق', ru: 'Ошибка валидации', ja: '検証エラー' },
});
// Auth translations
this.addTranslations('auth', {
'auth.login': { en: 'Login', tr: 'Giriş Yap', es: 'Iniciar sesión', fr: 'Connexion', de: 'Anmelden', zh: '登录', pt: 'Entrar', ar: 'تسجيل الدخول', ru: 'Войти', ja: 'ログイン' },
'auth.register': { en: 'Register', tr: 'Kayıt Ol', es: 'Registrarse', fr: "S'inscrire", de: 'Registrieren', zh: '注册', pt: 'Cadastrar', ar: 'التسجيل', ru: 'Регистрация', ja: '登録' },
'auth.logout': { en: 'Logout', tr: ıkış Yap', es: 'Cerrar sesión', fr: 'Déconnexion', de: 'Abmelden', zh: '退出', pt: 'Sair', ar: 'تسجيل الخروج', ru: 'Выйти', ja: 'ログアウト' },
'auth.forgotPassword': { en: 'Forgot password?', tr: 'Şifreni mi unuttun?', es: '¿Olvidaste tu contraseña?', fr: 'Mot de passe oublié?', de: 'Passwort vergessen?', zh: '忘记密码?', pt: 'Esqueceu a senha?', ar: 'نسيت كلمة المرور؟', ru: 'Забыли пароль?', ja: 'パスワードを忘れましたか?' },
});
// Content translations
this.addTranslations('content', {
'content.create': { en: 'Create Content', tr: 'İçerik Oluştur', es: 'Crear contenido', fr: 'Créer du contenu', de: 'Inhalt erstellen', zh: '创建内容', pt: 'Criar conteúdo', ar: 'إنشاء محتوى', ru: 'Создать контент', ja: 'コンテンツを作成' },
'content.generate': { en: 'Generate', tr: 'Oluştur', es: 'Generar', fr: 'Générer', de: 'Generieren', zh: '生成', pt: 'Gerar', ar: 'توليد', ru: 'Сгенерировать', ja: '生成する' },
'content.publish': { en: 'Publish', tr: 'Yayınla', es: 'Publicar', fr: 'Publier', de: 'Veröffentlichen', zh: '发布', pt: 'Publicar', ar: 'نشر', ru: 'Опубликовать', ja: '公開' },
'content.schedule': { en: 'Schedule', tr: 'Zamanla', es: 'Programar', fr: 'Planifier', de: 'Planen', zh: '定时', pt: 'Agendar', ar: 'جدولة', ru: 'Запланировать', ja: 'スケジュール' },
'content.draft': { en: 'Draft', tr: 'Taslak', es: 'Borrador', fr: 'Brouillon', de: 'Entwurf', zh: '草稿', pt: 'Rascunho', ar: 'مسودة', ru: 'Черновик', ja: '下書き' },
});
// Analytics translations
this.addTranslations('analytics', {
'analytics.engagement': { en: 'Engagement', tr: 'Etkileşim', es: 'Interacción', fr: 'Engagement', de: 'Engagement', zh: '互动', pt: 'Engajamento', ar: 'التفاعل', ru: 'Вовлечённость', ja: 'エンゲージメント' },
'analytics.reach': { en: 'Reach', tr: 'Erişim', es: 'Alcance', fr: 'Portée', de: 'Reichweite', zh: '覆盖', pt: 'Alcance', ar: 'الوصول', ru: 'Охват', ja: 'リーチ' },
'analytics.impressions': { en: 'Impressions', tr: 'Gösterim', es: 'Impresiones', fr: 'Impressions', de: 'Impressionen', zh: '展示次数', pt: 'Impressões', ar: 'المشاهدات', ru: 'Показы', ja: 'インプレッション' },
'analytics.clicks': { en: 'Clicks', tr: 'Tıklama', es: 'Clics', fr: 'Clics', de: 'Klicks', zh: '点击', pt: 'Cliques', ar: 'النقرات', ru: 'Клики', ja: 'クリック' },
});
// Plural forms
this.addPluralTranslations('content.items', {
en: { one: '{{count}} item', other: '{{count}} items' },
tr: { one: '{{count}} öğe', other: '{{count}} öğe' },
es: { one: '{{count}} elemento', other: '{{count}} elementos' },
fr: { one: '{{count}} élément', other: '{{count}} éléments' },
de: { one: '{{count}} Element', other: '{{count}} Elemente' },
zh: { other: '{{count}} 项' },
pt: { one: '{{count}} item', other: '{{count}} itens' },
ar: { zero: 'لا عناصر', one: 'عنصر واحد', two: 'عنصران', few: '{{count}} عناصر', many: '{{count}} عنصر', other: '{{count}} عنصر' },
ru: { one: '{{count}} элемент', few: '{{count}} элемента', many: '{{count}} элементов', other: '{{count}} элементов' },
ja: { other: '{{count}} アイテム' },
});
}
/**
* Add translations for a namespace
*/
private addTranslations(namespace: string, entries: Record<string, Record<string, string>>): void {
for (const [key, translations] of Object.entries(entries)) {
this.translations.set(key, { key, translations });
}
}
/**
* Add plural translations
*/
private addPluralTranslations(key: string, pluralForms: Record<string, Record<string, string>>): void {
this.translations.set(key, { key, translations: {}, pluralForms });
}
/**
* Translate a key
*/
translate(key: string, locale: string, args?: Record<string, any>): string {
const entry = this.translations.get(key);
if (!entry) {
this.logger.warn(`Missing translation for key: ${key}`);
return key;
}
const normalizedLocale = locale.split('-')[0];
let translation = entry.translations[normalizedLocale] || entry.translations[this.fallbackLocale] || key;
// Replace interpolation variables
if (args) {
for (const [argKey, argValue] of Object.entries(args)) {
translation = translation.replace(new RegExp(`{{${argKey}}}`, 'g'), String(argValue));
}
}
return translation;
}
/**
* Translate with plural support
*/
translatePlural(key: string, count: number, locale: string, args?: Record<string, any>): string {
const entry = this.translations.get(key);
if (!entry || !entry.pluralForms) {
return this.translate(key, locale, { ...args, count });
}
const normalizedLocale = locale.split('-')[0];
const pluralForms = entry.pluralForms[normalizedLocale] || entry.pluralForms[this.fallbackLocale];
if (!pluralForms) {
return this.translate(key, locale, { ...args, count });
}
const form = this.getPluralForm(count, normalizedLocale);
let translation = pluralForms[form] || pluralForms['other'] || key;
// Replace interpolation variables
const allArgs = { ...args, count };
for (const [argKey, argValue] of Object.entries(allArgs)) {
translation = translation.replace(new RegExp(`{{${argKey}}}`, 'g'), String(argValue));
}
return translation;
}
/**
* Get plural form based on locale rules
*/
private getPluralForm(count: number, locale: string): string {
// Arabic has 6 plural forms
if (locale === 'ar') {
if (count === 0) return 'zero';
if (count === 1) return 'one';
if (count === 2) return 'two';
if (count >= 3 && count <= 10) return 'few';
if (count >= 11 && count <= 99) return 'many';
return 'other';
}
// Russian has 3 plural forms
if (locale === 'ru') {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return 'one';
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'few';
return 'many';
}
// Most languages use simple one/other
return count === 1 ? 'one' : 'other';
}
/**
* Check if translation exists
*/
hasTranslation(key: string, locale?: string): boolean {
const entry = this.translations.get(key);
if (!entry) return false;
if (!locale) return true;
const normalizedLocale = locale.split('-')[0];
return normalizedLocale in entry.translations;
}
/**
* Get all translation keys
*/
getAllKeys(): string[] {
return Array.from(this.translations.keys());
}
/**
* Add or update a translation
*/
setTranslation(key: string, locale: string, value: string): void {
const entry = this.translations.get(key) || { key, translations: {} };
const normalizedLocale = locale.split('-')[0];
entry.translations[normalizedLocale] = value;
this.translations.set(key, entry);
}
/**
* Get all translations for a locale
*/
getTranslationsForLocale(locale: string): Record<string, string> {
const normalizedLocale = locale.split('-')[0];
const result: Record<string, string> = {};
for (const [key, entry] of this.translations.entries()) {
const translation = entry.translations[normalizedLocale] || entry.translations[this.fallbackLocale];
if (translation) {
result[key] = translation;
}
}
return result;
}
}