Files
Content-Hunter_BE/src/modules/i18n/services/translation.service.ts
Harun CAN fc88faddb9
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s
main
2026-02-10 12:27:14 +03:00

229 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
}