recipes.migrating-from-ngx-translate
Migrating from ngx-translate
A concept-mapping table and one worked component conversion for teams moving off @ngx-translate/core.
- @ngx-runtime-i18n/angular
This is a migration guide, not a claim that switching is trivial in every codebase. The two libraries solve overlapping problems with different primitives — @ngx-translate/core is Observable-first (with signals layered on in recent versions), while @ngx-runtime-i18n/angular is signals-first from the ground up. A large app with many TranslateService.stream() subscriptions or a custom TranslateLoader will need real per-component work, not a find-and-replace.
Facts about @ngx-translate/core below reflect its published 18.0.0 release, current as of this guide's last update. Check the ngx-translate documentation directly if you're on an older major version — v18 removed several APIs that earlier versions still had.
Concept mapping
ngx-translate (@ngx-translate/core v18) |
@ngx-runtime-i18n/angular |
Notes |
|---|---|---|
TranslateService.instant(key, params?) |
I18nService.t(key, params?) |
Both are synchronous. instant() returns the key itself if the catalog hasn't loaded yet; t() behaves the same way before ready() is true. |
TranslateService.get(key, params?) |
I18nService.t$(key, params?) |
get() is an Observable that emits once and completes. t$() is a Signal<string> that recomputes on language or param changes — read it directly in a template ({{ greeting() }}), no async pipe needed. |
TranslatePipe ({{ 'key' | translate:params }}) |
I18nPipe ({{ 'key' | i18n:params }}) |
Same shape, different pipe name and import. |
TranslateDirective (<span [translate]="key">) |
No direct equivalent | Use I18nPipe in the template or I18nService.t() in the component instead. |
TranslateService.use(lang) |
I18nService.setLang(lang) |
use() returns an Observable that completes on load; setLang() returns a Promise and cancels any in-flight switch for a previous request automatically. |
TranslateService.currentLang() (signal, v18+) / onLangChange (Observable) |
I18nService.lang() (signal) or I18nCompatService.lang$ (Observable) |
Both libraries are signal-based for the current language as of their latest majors. Use I18nCompatService.lang$ in an RxJS-heavy codebase that isn't on signals yet — see Getting started for when to reach for it over I18nService. |
ICU plurals via ngx-translate-messageformat-compiler (separate package, runtime eval-based) |
Built-in ICU-lite {count, plural, one {...} other {...}} |
ngx-translate's core has no plural syntax of its own — pluralization needs a third-party compiler that requires relaxing your CSP for unsafe-eval. Plurals here are part of @ngx-runtime-i18n/core with no extra dependency or CSP change. See ICU-lite formatting for what's covered. |
Worked conversion
A component using TranslateService.get() for an initial reactive translation, TranslatePipe in the template, and a manual onLangChange subscription to refresh a computed value:
// Before — ngx-translate
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Subscription } from 'rxjs';
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
@Component({
standalone: true,
imports: [TranslatePipe],
template: `
<h1>{{ 'hello.user' | translate:{ name: username } }}</h1>
<p>{{ itemCount }}</p>
<button (click)="switch('de')">Deutsch</button>
`,
})
export class CartSummaryComponent implements OnInit, OnDestroy {
private translate = inject(TranslateService);
username = 'Ashwin';
itemCount = '';
private sub?: Subscription;
ngOnInit() {
this.updateCount();
this.sub = this.translate.onLangChange.subscribe(() => this.updateCount());
}
ngOnDestroy() {
this.sub?.unsubscribe();
}
private updateCount() {
this.translate.get('cart.items', { count: 3 }).subscribe((text) => (this.itemCount = text));
}
switch(lang: string) {
this.translate.use(lang);
}
}// After — @ngx-runtime-i18n/angular
import { Component, inject } from '@angular/core';
import { I18nService, I18nPipe } from '@ngx-runtime-i18n/angular';
@Component({
standalone: true,
imports: [I18nPipe],
template: `
<h1>{{ 'hello.user' | i18n:{ name: username } }}</h1>
<p>{{ itemCount() }}</p>
<button (click)="switch('de')">Deutsch</button>
`,
})
export class CartSummaryComponent {
private i18n = inject(I18nService);
username = 'Ashwin';
// t$() recomputes on its own when the language changes — no subscription,
// no ngOnDestroy teardown, no manual re-fetch on langChange.
itemCount = this.i18n.t$('cart.items', { count: 3 });
switch(lang: string) {
this.i18n.setLang(lang);
}
}The OnInit/OnDestroy lifecycle and the onLangChange subscription disappear entirely — t$() is a signal, so Angular's change detection already knows when it needs to recompute. This is the general shape most conversions take: an Observable-plus-manual-subscription pair becomes a signal read, and the surrounding lifecycle code is often deleted.
What doesn't map one-to-one
| Concept | Difference |
|---|---|
| Loaders | ngx-translate's TranslateLoader (typically provideTranslateHttpLoader()) is replaced by fetchCatalog in provideRuntimeI18n() — a plain async function instead of an injectable class, so a custom loader becomes a function instead of a DI token. |
| Fallback language | ngx-translate's fallbackLang (the replacement for the removed defaultLang in v18) is roughly RuntimeI18nConfig.fallbacks here, but the resolution chain differs: this library supports an ordered list of fallbacks, not a single one. See Fallback chains. |
| Missing-key handling | ngx-translate's MissingTranslationHandler is an injectable class; onMissingKey here is a plain function passed into provideRuntimeI18n(). |
| SSR | If your ngx-translate setup runs under Angular Universal, don't assume the same request lifecycle carries over: provideRuntimeI18nSsr() needs its own request-scoped snapshot. See SSR and hydration for the setup. |
Running both libraries side by side during a migration is an option for a large codebase: nothing in @ngx-runtime-i18n/angular requires ngx-translate to be removed first. Converting one route or feature module at a time and shipping incrementally is a reasonable path.