// 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; context?: string; pluralForms?: Record>; } @Injectable() export class TranslationService { private readonly logger = new Logger(TranslationService.name); private readonly translations = new Map(); 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>): void { for (const [key, translations] of Object.entries(entries)) { this.translations.set(key, { key, translations }); } } /** * Add plural translations */ private addPluralTranslations(key: string, pluralForms: Record>): void { this.translations.set(key, { key, translations: {}, pluralForms }); } /** * Translate a key */ translate(key: string, locale: string, args?: Record): 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 { 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 { const normalizedLocale = locale.split('-')[0]; const result: Record = {}; for (const [key, entry] of this.translations.entries()) { const translation = entry.translations[normalizedLocale] || entry.translations[this.fallbackLocale]; if (translation) { result[key] = translation; } } return result; } }