Translations

The Domain Locker app uses ngx-translate for translations, enabling multi-language support. This section explains how to implement translations in your components, use translations in TypeScript files, and add new languages.

Using Translations in Components

To use translations in a component, import the TranslateModule and add it to the component's imports:

import { TranslateModule } from '@ngx-translate/core';

Example: Inline Template

Here is a minimal example of using translations in a template:

@Component({
  selector: 'app-language-switcher',
  standalone: true,
  imports: [TranslateModule],
  template: `
    <div>
      <label *ngFor="let lang of languages">
        <input
          type="radio"
          [value]="lang.code"
          [(ngModel)]="selectedLanguage"
          (change)="onLanguageChange(lang.code)"
        />
        {{ lang.flag }} {{ lang.name }}
      </label>
    </div>
  `,
})
export class LanguageSwitcherComponent {
  languages = [
    { code: 'en', name: 'English', flag: '🇬🇧' },
    { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
  ];
  selectedLanguage = 'en';

  constructor(private translationService: TranslationService) {}

  onLanguageChange(langCode: string) {
    this.translationService.switchLanguage(langCode);
  }
}

Using Translations in HTML

To translate strings in HTML, use the translate pipe:

<h2>{{ 'HOME.SUBHEADINGS.DOMAINS' | translate }}</h2>

Using Translations in TypeScript

To translate strings in TypeScript, use the TranslationService:

constructor(private translationService: TranslationService) {}

someFunction() {
  const translatedText = this.translationService.translateService.instant('HOME.SUBHEADINGS.DOMAINS');
  console.log(translatedText); // Outputs the translated string
}

Example with Parameters

You can pass dynamic parameters to translations:

const message = this.translationService.translateService.instant('TAGS.SUMMARY', { count: 5 });
console.log(message); // Outputs: "Showing 5 tags -"

Adding a New Language

  1. Create a Language File: Add a new file for the language in source/src/assets/i18n/. For example, for German, create de.json:
{
  "NAV": {
    "DOMAINS": "Domains",
    "INVENTORY": "Inventar"
  },
  "HOME": {
    "SUBHEADINGS": {
      "DASHBOARD": "Dashboard",
      "DOMAINS": "Domains"
    }
  }
}
  1. Register the language in two places. Both lists must contain only languages that have a file, or picking the language loads nothing and every key renders as [KEY]:
  // source/src/app/utils/translation-loader.factory.ts — what `?lang=` accepts
- export const AVAILABLE_LANGS = ['vi', 'en', 'de'];
+ export const AVAILABLE_LANGS = ['vi', 'en', 'de', 'fr'];

  // source/src/app/services/translation.service.ts — what the picker shows
  availableLanguages = [
    { code: 'vi', name: 'Tiếng Việt', flag: '🇻🇳' },
+   { code: 'fr', name: 'Français', flag: '🇫🇷' },
  ];
  1. Test the Language: Switch to the new language using the language switcher, or append ?lang=fr to the URL — which the server honours too, so the served HTML is already in that language.

  2. Run the audit: npm run i18n:audit checks that the new file has exactly the same keys as en.json with no empty values, and reports template strings that are still hard-coded.

Notes

  • Default language is Vietnamese. DEFAULT_LANG in source/src/app/utils/translation-loader.factory.ts is the single source of truth; the <html lang> attribute and the SSR translation endpoint follow it. With no ?lang= and no stored preference, the server renders Vietnamese.
  • Language resolution order: ?lang= → stored preference (browser only) → DEFAULT_LANG. Accept-Language is deliberately not consulted: the browser has no equivalent input at that point, so honouring it on the server alone would make the server render one language and hydration replace it with another.
  • Dynamic Language Switching: Changes take effect immediately after calling switchLanguage.
  • Fallback Mechanism: Missing translations return the key wrapped in square brackets (e.g., [MISSING.KEY]).
  • Auditing coverage: npm run i18n:audit runs two checks — untranslated template strings and key parity — and exits non-zero on either. It is intentionally not part of npm run hold-my-beer: most templates still carry hard-coded English, so it would block every unrelated change until that work is finished. scripts/i18n-audit-allowlist.json holds the genuine non-strings; add to it only for brand names, units and code samples.
flowchart TD
  subgraph Init_Translation
    Start["App Start (Client/Server)"] --> LanguageInit["languageInitializerFactory"]
    LanguageInit -->|Platform: Browser| GetLocalStorageLang["?lang=, then localStorage.getItem('language')"]
    LanguageInit -->|Platform: Server| ServerLang["?lang= from request.originalUrl"]
    GetLocalStorageLang --> SetLang["translate.use(lang), else DEFAULT_LANG ('vi')"]
    ServerLang --> SetLang
    SetLang --> SetHtmlLang["document.documentElement lang = lang"]
  end

  subgraph TranslateLoader
    TranslateService --> ServerSafeTranslateLoader
    ServerSafeTranslateLoader -->|isPlatformBrowser| HTTPFetch["HttpClient GET /i18n/{lang}.json"]
    ServerSafeTranslateLoader -->|isPlatformServer| FSRead["fs.readFileSync('/assets/i18n/{lang}.json')"]
    HTTPFetch --> TranslationsLoaded
    FSRead --> TranslationsLoaded
  end

  subgraph TranslationService
    ComponentUsesTranslate --> TranslationServiceSwitch["switchLanguage(code)"]
    TranslationServiceSwitch -->|Valid Code| UseTranslate["translate.use(code)"]
    UseTranslate --> StoreLang["localStorage.setItem('language')"]
    ComponentTS --> InstantTranslate["translateService.instant('KEY')"]
    InstantWithParams["instant('TAGS.SUMMARY', { count })"] --> InstantTranslate
  end

  subgraph Fallbacks
    MissingKey["Missing Translation Key"] --> CustomHandler["CustomMissingTranslationHandler"]
    CustomHandler --> ShowFallback["Return '[KEY]' and warn"]
  end

  subgraph Template_Usage
    TranslatePipe["{{ 'HOME.SUBHEADINGS.DOMAINS' | translate }}"] --> TranslateService
  end

  Start --> TranslateService
  TranslateService --> TranslatePipe
  TranslateService --> ComponentTS
  TranslateService --> ComponentUsesTranslate
  TranslationsLoaded --> TranslateService
  TranslateService --> MissingKey