generated from fahricansecer/boilerplate-be
229 lines
13 KiB
TypeScript
229 lines
13 KiB
TypeScript
// 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;
|
||
}
|
||
}
|