ngx-translate
Loading translation JSON at runtime buys instant language switching, at the cost of parsing and lookup work in the browser
Modern Angular only No equivalent exists in the original 100 Days series. Written fresh for Angular v22, ngx-translate v18.
Lead with this: ngx-translate loads translations at runtime from JSON files, so users can switch languages instantly without a page reload — the tradeoff compared to Angular's built-in i18n, which inlines translations at build time for zero runtime overhead.
What it is
@ngx-translate/core is the most widely adopted third-party i18n library
for Angular. Instead of requiring a separate bundle per language (Angular's
built-in approach), ngx-translate ships one bundle and fetches the appropriate
translation JSON file at runtime.
When to pick ngx-translate over built-in i18n:
| Scenario | Built-in i18n | ngx-translate |
|---|---|---|
| Language switching without page reload | ❌ | ✅ |
| One bundle, multiple languages | ❌ | ✅ |
| Zero runtime translation overhead | ✅ | ❌ (JSON parse + lookup) |
| Translations loaded from an API | ❌ | ✅ |
| ICU plural/gender expressions | ✅ built-in | ❌ manual |
| Teams with translation TMS integration | Both work | Both work |
For new projects where compile-time performance and simplicity matter, start with built-in i18n. Reach for ngx-translate when you need runtime switching or dynamic translation loading.
How it works under the hood
The key difference — runtime vs compile-time
Angular's built-in i18n inlines translations into the bundle at build time. Each locale is a complete, separate JS bundle. The user never downloads translations they don't use, but switching languages means loading a new bundle (a full navigation or page reload).
ngx-translate works differently:
App bootstraps ↓TranslateService.use('en') called ↓TranslateHttpLoader fetches /i18n/en.json from the server ↓Translations stored in-memory as a JavaScript object ↓{{ 'HELLO' | translate }} → looks up 'HELLO' in the object → renders 'Hello'
User switches to French: ↓TranslateService.use('fr') called ↓Loader fetches /i18n/fr.json (or returns from cache) ↓In-memory translations replaced with French object ↓All TranslatePipe / translate() instances re-render automaticallyNo page reload. Same bundle. New language.How pipes and signals react: Since ngx-translate v18, TranslatePipe
is powered by Angular signals internally. When use('fr') fires, a
translations signal updates, which invalidates all computed values and
template views that read it — the re-render is pure signal propagation,
not manual ChangeDetectorRef.markForCheck().
The tradeoff: every {{ key | translate }} performs a key lookup at render
time. In large templates this is fast in practice but isn't free like
compile-time inlining.
Setup
npm install @ngx-translate/core @ngx-translate/http-loader// app.config.tsimport { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';import { provideHttpClient } from '@angular/common/http';import { provideTranslateService, TranslateService} from '@ngx-translate/core';import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), // TranslateHttpLoader uses HttpClient provideTranslateService({ lang: 'en', // default language on startup fallbackLang: 'en', // fallback when a key is missing loader: provideTranslateHttpLoader({ prefix: '/i18n/', // serves /i18n/en.json, /i18n/fr.json suffix: '.json', }), }), // Optional: load translations before the app renders provideAppInitializer(() => inject(TranslateService).use('en') ), ],};// NgModule approach (Angular 2–13, ngx-translate v12–v14)@NgModule({ imports: [ HttpClientModule, TranslateModule.forRoot({ defaultLanguage: 'en', loader: { provide: TranslateLoader, useFactory: (http: HttpClient) => new TranslateHttpLoader(http, '/i18n/', '.json'), deps: [HttpClient], }, }), ],})export class AppModule {}Translation files
Translation files are plain JSON. Use nested keys for organization:
// src/i18n/en.json{ "COMMON": { "SAVE": "Save", "CANCEL": "Cancel", "LOADING": "Loading…" }, "AUTH": { "LOGIN": "Log in", "LOGOUT": "Log out", "WELCOME": "Welcome, {{ name }}!", "ERRORS": { "INVALID_CREDENTIALS": "Invalid email or password" } }, "NOTIFICATIONS": { "COUNT_ONE": "You have 1 notification", "COUNT_OTHER": "You have {{ count }} notifications" }}// src/i18n/fr.json{ "COMMON": { "SAVE": "Enregistrer", "CANCEL": "Annuler", "LOADING": "Chargement…" }, "AUTH": { "LOGIN": "Se connecter", "LOGOUT": "Se déconnecter", "WELCOME": "Bienvenue, {{ name }} !", "ERRORS": { "INVALID_CREDENTIALS": "Email ou mot de passe invalide" } }, "NOTIFICATIONS": { "COUNT_ONE": "Vous avez 1 notification", "COUNT_OTHER": "Vous avez {{ count }} notifications" }}Using translations in components
TranslatePipe — in templates
Import TranslatePipe in each standalone component that needs it:
import { Component } from '@angular/core';import { TranslatePipe } from '@ngx-translate/core';
@Component({ selector: 'app-header', standalone: true, imports: [TranslatePipe], template: ` <nav> <a>{{ 'AUTH.LOGIN' | translate }}</a> <a>{{ 'AUTH.LOGOUT' | translate }}</a>
<!-- With interpolation parameters --> <p>{{ 'AUTH.WELCOME' | translate:{ name: currentUser().name } }}</p> </nav> `,})export class HeaderComponent { /* ... */ }The translate() signal function — v18 (recommended for component code)
The new standalone translate() function returns a Signal<string> that
automatically updates when the language changes:
import { Component, inject } from '@angular/core';import { translate, TranslateService, TranslatePipe } from '@ngx-translate/core';
@Component({ selector: 'app-page-title', standalone: true, imports: [TranslatePipe], template: ` <title>{{ pageTitle() }}</title> <h1>{{ 'HOME.HEADING' | translate }}</h1> `,})export class PageTitleComponent { // Signal that auto-updates on language change — use in computed(), effect(), or template pageTitle = translate('HOME.TITLE');
// With parameters — reactive parameters via a signal userGreeting = translate('AUTH.WELCOME', { name: 'Alice' });}TranslateService — for programmatic translations
For toasts, alerts, document titles, or any translation needed in TypeScript code:
import { Component, inject, computed } from '@angular/core';import { TranslateService } from '@ngx-translate/core';
@Component({ /* ... */ })export class NotificationComponent { private translate = inject(TranslateService);
// currentLang is a Signal<Language | null> in v18+ — call it as a function currentLang = this.translate.currentLang; // Signal<'en' | 'fr' | null>
// Synchronous translation (only safe after translations are loaded) getInstant(key: string): string { return this.translate.instant(key); }
// Observable — emits once when loaded getAsync(key: string): void { this.translate.get('AUTH.ERRORS.INVALID_CREDENTIALS').subscribe(msg => { this.showToast(msg); }); }
// Stream — emits immediately AND re-emits on language change watchedMessage = this.translate.stream('NOTIFICATIONS.COUNT_OTHER', { count: 5 });
// Switch language switchToFrench(): void { this.translate.use('fr'); // fetches fr.json if not cached, then updates all pipes/signals }}*translateBlock — translating several keys in one block
The TranslateBlockDirective is a structural directive that exposes a typed
t() function to translate multiple keys cleanly in a template block:
import { TranslatePipe, TranslateBlockDirective } from '@ngx-translate/core';
@Component({ standalone: true, imports: [TranslatePipe, TranslateBlockDirective], template: ` <ng-container *translateBlock="let t"> <button>{{ t('COMMON.SAVE') }}</button> <button>{{ t('COMMON.CANCEL') }}</button> <p>{{ t('AUTH.WELCOME', { name: user().name }) }}</p> </ng-container> `,})export class FormFooterComponent { /* ... */ }*translateBlock is the recommended replacement for the deprecated
content-as-key pattern (<div translate>KEY</div>, removed in v19).
Real-world patterns
Pattern 1 — Language switcher
@Component({ selector: 'app-lang-switcher', standalone: true, imports: [TranslatePipe], template: ` <div class="lang-switcher"> @for (lang of availableLangs; track lang.code) { <button [class.active]="currentLang() === lang.code" (click)="switchLang(lang.code)" > {{ lang.label }} </button> } </div> `,})export class LangSwitcherComponent { private translate = inject(TranslateService);
currentLang = this.translate.currentLang; // Signal — reads reactively in template
availableLangs = [ { code: 'en', label: 'English' }, { code: 'fr', label: 'Français' }, { code: 'de', label: 'Deutsch' }, ];
switchLang(lang: string): void { this.translate.use(lang); localStorage.setItem('preferred-lang', lang); }}// app.config.ts — restore saved language on startupprovideAppInitializer(() => { const translate = inject(TranslateService); const saved = localStorage.getItem('preferred-lang') ?? 'en'; return translate.use(saved);})Pattern 2 — Lazy-loaded feature translations
For large apps, load only the global translations at startup and merge feature-specific translations when a route loads:
// feature/feature.routes.tsexport const featureRoutes: Routes = [ { path: 'dashboard', providers: [ { provide: APP_INITIALIZER, useFactory: (http: HttpClient, translate: TranslateService) => () => http.get(`/i18n/dashboard/en.json`).pipe( tap(translations => translate.setTranslation('en', translations, true)), ).toPromise(), deps: [HttpClient, TranslateService], multi: true, }, ], component: DashboardComponent, },];The true third argument to setTranslation() merges into existing
translations rather than replacing them.
Pattern 3 — Custom MissingTranslationHandler
Log missing keys in development; return the key as a fallback in production:
import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core';
export class LoggingMissingTranslationHandler implements MissingTranslationHandler { handle(params: MissingTranslationHandlerParams): string { if (!environment.production) { console.warn(`[i18n] Missing translation for key: "${params.key}"`); } return params.key; // Show key as fallback rather than empty string }}
// Register in provideTranslateServiceprovideTranslateService({ lang: 'en', missingTranslationHandler: { provide: MissingTranslationHandler, useClass: LoggingMissingTranslationHandler, },})Testing
Use a static translation loader in tests to avoid HTTP dependencies:
import { TestBed } from '@angular/core/testing';import { provideTranslateService, provideTranslateLoader, TranslateLoader, Translation,} from '@ngx-translate/core';import { of } from 'rxjs';
const testTranslations: { [key: string]: Translation } = { en: { 'AUTH.LOGIN': 'Log in', 'AUTH.WELCOME': 'Welcome, {{ name }}!', },};
class StaticTranslateLoader implements TranslateLoader { getTranslation(lang: string) { return of(testTranslations[lang] ?? {}); }}
describe('HeaderComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HeaderComponent], providers: [ provideTranslateService({ lang: 'en', loader: provideTranslateLoader(StaticTranslateLoader), }), ], }); });});Common mistakes
Mistake 1 — Reading currentLang as a string (v18 breaking change)
In ngx-translate v17 and earlier, currentLang was a plain string property.
In v18 it became a Signal<Language | null>. Code that reads it directly
without calling it now gets a signal object, not the language code:
// ❌ Returns a Signal object in v18 — not the language stringif (this.translate.currentLang === 'en') { ... }
// ✅ Call the signalif (this.translate.currentLang() === 'en') { ... }
// ✅ Or use the non-reactive snapshot helperif (this.translate.getCurrentLang() === 'en') { ... }Mistake 2 — Using instant() before translations are loaded
instant() reads translations synchronously from the current in-memory state.
If called before the language file has loaded, it returns the key (or the
missing-translation handler's result):
// ❌ Called too early — translations not loaded yet, returns the keyexport class AppComponent { title = this.translate.instant('APP.TITLE'); // probably returns 'APP.TITLE' constructor(private translate: TranslateService) {}}
// ✅ Use translate() signal — resolves when translations are availableexport class AppComponent { title = translate('APP.TITLE'); // Signal — empty until loaded, then fills in}
// ✅ Or wait for translations to load via get()export class AppComponent { title = ''; constructor(private translate: TranslateService) { translate.get('APP.TITLE').subscribe(t => this.title = t); }}Mistake 3 — Using deprecated content-as-key syntax
The <div translate>KEY</div> pattern was deprecated in v18 and will be
removed in v19. It's also hard to read — the key isn't in the attribute where
you'd expect it:
<!-- ❌ Deprecated — content as translation key --><span translate>AUTH.LOGIN</span>
<!-- ✅ Pipe — clear and explicit --><span>{{ 'AUTH.LOGIN' | translate }}</span>
<!-- ✅ translate() signal — for use in component logic --><span>{{ loginLabel() }}</span>Mistake 4 — Forgetting to import TranslatePipe per component
TranslatePipe is a standalone pipe that must be imported in each component's
imports array. Forgetting it causes the pipe to be treated as unknown and
either errors or renders nothing:
// ❌ TranslatePipe not imported — {{ 'KEY' | translate }} renders nothing@Component({ standalone: true, imports: [], template: `<p>{{ 'AUTH.LOGIN' | translate }}</p>`,})
// ✅ TranslatePipe imported@Component({ standalone: true, imports: [TranslatePipe], template: `<p>{{ 'AUTH.LOGIN' | translate }}</p>`,})How this evolved
-
ngx-translate v1–8 (2015–2019): The go-to Angular i18n library before Angular's own
@angular/localizeexisted. NgModule-only setup,TranslateModule.forRoot(). Imperative API withgetDefaultLang()andsetDefaultLang(). -
ngx-translate v9–14 (2019–2022): Maintained alongside Angular's growing built-in i18n. API largely stable. The use case narrowed: Angular's built-in i18n gained ICU support, XLIFF2, and multi-locale builds. ngx-translate remained the choice for runtime switching.
-
ngx-translate v15–17 (2023–2024): Standalone component support. The library's API modernized significantly.
setDefaultLang()anddefaultLangdeprecated in favor offallbackLang. -
ngx-translate v18 (2025): Signal-first rewrite.
currentLangandfallbackLangbecameSignal<Language | null>.isLoadingsignal added.TranslatePipeinternals replaced with signal-based reactivity. New standalonetranslate()signal function.*translateBlockdirective. Content-as-key pattern deprecated. Angular 18–22 compatible. This is the stable v18 API documented in this article.
See also
- Built-in i18n — Angular's compile-time alternative with zero runtime overhead; covers when to use each approach
- Signals — the Angular signal primitives that power ngx-translate v18's reactivity
- Official ngx-translate docs
- ngx-translate GitHub
- @ngx-translate/http-loader