Skip to content
Home / Blog / Angular i18n with ngx-translate: setup, plurals, and runtime language switching

Angular i18n with ngx-translate: setup, plurals, and runtime language switching

Developer resources
Markus Merzinger
Senior Developer

Last updated

7/30/2026

Read time

9 min

Best for

Developers

LingoHub and Angular logos representing an Angular i18n workflow with ngx-translate and JSON translation files.

Angular i18n covers the work of getting user-facing text out of your components and into translation files your team can maintain, then rendering the right string, plural form, and date format for whichever locale the user arrived with. Decisions that look arbitrary at two languages become expensive to reverse at twelve, which makes the early ones worth getting right.

This guide walks through Angular localization with ngx-translate, the runtime translation library for Angular, using JSON resource files. Code samples target Angular 22 and @ngx-translate/core v18; the library declares a peer range of Angular 18 or newer, so the same setup applies across recent major versions.

What is Angular i18n?

Internationalization, abbreviated “i18n” for the eighteen letters between its first and last, means building an application that can adapt to any language and region without code changes. Localization, abbreviated “l10n”, is the follow-on work of producing the actual translations and locale conventions for a specific market. Developers own i18n; translators and reviewers own l10n, and the goal is a workflow where neither blocks the other.

In Angular, the topic is often written angular-i18n in package names and search queries, and it splits into three concerns. Text needs to leave your templates and live in translation files. Values interpolated into that text need to survive translation intact. Numbers, dates, and currencies need locale-aware formatting, which Angular’s own pipes handle once you register the locale data.

You implement the first two concerns with a translation library. ngx-translate resolves strings at runtime from JSON files, meaning one application bundle serves every language and users switch language without a page reload.

How to set up ngx-translate in Angular

Install the core library and the HTTP loader that fetches your JSON files:

npm i @ngx-translate/core @ngx-translate/http-loader

Version 18 uses a provider-based API. Register the service in app.config.ts, point the loader at your translation directory, and declare which language to load first:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideTranslateService } from '@ngx-translate/core';
import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideHttpClient(),
    provideTranslateService({
      loader: provideTranslateHttpLoader({
        prefix: '/assets/i18n/',
        suffix: '.json',
      }),
      fallbackLang: 'en',
      lang: 'en',
    }),
  ],
};

lang is the language loaded at startup; fallbackLang is what resolves when a key is missing from the active language. Older tutorials show TranslateModule.forRoot({ defaultLanguage }), which the installation guide has replaced. If you are migrating an existing project, defaultLanguage is the option that became fallbackLang.

How to structure JSON translation files

The loader configuration above resolves en to /assets/i18n/en.json. Each language gets one file, and the structure is plain nested JSON:

{
  "cart": {
    "title": "Your cart",
    "checkout": "Proceed to checkout",
    "empty": "Your cart is empty"
  }
}

Nesting groups related strings and keeps keys short at the point of use. Keep one language as the source of truth, conventionally en.json, and treat every other file as derived from it. That rule prevents the most common maintenance problem in a multilingual Angular codebase: keys that exist in three languages and are quietly absent from the other nine.

ngx-translate has no extraction command of its own. You author keys in the source file as you build components. If you prefer to generate the source file from your templates, @vendure/ngx-translate-extract is the maintained community extractor, though it sits outside the ngx-translate project and requires Angular 20 or newer. Keeping target files aligned with the source is a workflow concern, which the closing section returns to.

An angular i18n example in templates and TypeScript

The translate pipe covers most template text:

<h1>{{ 'cart.title' | translate }}</h1>
<button>{{ 'cart.checkout' | translate }}</button>

TranslateDirective does the same for element content, which helps when a string wraps inline elements:

<p [translate]="'cart.empty'"></p>

From TypeScript, TranslateService gives you several lookup shapes, and picking the wrong one causes most of the confusion around this library:

import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

export class CartComponent {
  private translate = inject(TranslateService);

  notifyEmpty() {
    this.translate.get('cart.empty').subscribe((text) => this.toast.show(text));
  }
}

Surface

Use it when

Follows a language change

translate pipe

Rendering text in a template

Yes

[translate] directive

Element content with inline markup

Yes

translate() standalone function

Signal-based component code

Yes

TranslateService.stream()

Long-lived subscription in TypeScript

Yes, re-emits

TranslateService.get()

One-off async lookup

No, emits once and completes

TranslateService.instant()

Synchronous read, files already loaded

No

instant() is the sharp edge. It returns the key itself when translations have not finished loading, which produces cart.title in your UI instead of “Your cart”. Reach for stream() when the value has to survive a language switch, and for get() when a single resolved value is enough. The TranslateService API reference documents the full surface.

Angular i18n placeholder and interpolation syntax

An angular i18n placeholder is written with double braces inside the translated string itself, and the default parser substitutes the parameters you pass in:

{
  "cart": {
    "greeting": "Welcome back, {{name}}",
    "total": "Order total: {{amount}}"
  }
}

Angular i18n interpolation then supplies those parameters as an object, in the pipe or through the service:

<p>{{ 'cart.greeting' | translate: { name: user.firstName } }}</p>
this.translate.get('cart.total', { amount: formattedTotal });

Write the braces tight against the name. The default parser allows at most one space on either side: {{name}} and {{ name }} both resolve, while {{ name }} renders as literal text. The tight form removes the question in files translators edit directly.

Name parameters for what they mean rather than their position, because a translator reordering a sentence has no way to know what {{0}} referred to. Pass already-formatted values for anything locale-sensitive, since the parser performs substitution only and applies no number or date formatting of its own.

Handling plurals with CLDR categories

English needs two forms for a countable noun. Polish needs four, Arabic needs six, and Japanese needs one. Pluralization means selecting the correct word form for a given number, and getting it right for a language you do not speak requires that language’s actual plural rules rather than a singular-versus-plural guess.

The browser already knows those rules. Intl.PluralRules returns the CLDR plural category for a number in a given locale, and has been available across browsers since 2019. For Polish, select(1) returns one, select(2) returns few, and select(5) returns many.

Store one key per category, nested under the base key:

{
  "cart": {
    "items": {
      "one": "{{count}} item in your cart",
      "other": "{{count}} items in your cart"
    }
  }
}

The Polish file uses the same key with the categories that language requires:

{
  "cart": {
    "items": {
      "one": "{{count}} produkt w koszyku",
      "few": "{{count}} produkty w koszyku",
      "many": "{{count}} produktów w koszyku",
      "other": "{{count}} produktu w koszyku"
    }
  }
}

A small pipe picks the category and falls back to other when a language does not define it:

import { Pipe, PipeTransform, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

@Pipe({ name: 'plural', pure: false })
export class PluralPipe implements PipeTransform {
  private translate = inject(TranslateService);

  transform(key: string, count: number, params: Record<string, unknown> = {}): string {
    const lang = this.translate.currentLang() ?? 'en';
    const category = new Intl.PluralRules(lang).select(count);
    const args = { count, ...params };
    const exact = `${key}.${category}`;
    const resolved = this.translate.instant(exact, args);
    return resolved === exact ? this.translate.instant(`${key}.other`, args) : resolved;
  }
}

Add PluralPipe to a component’s imports and the call site stays readable:

<span>{{ 'cart.items' | plural: itemCount }}</span>

The pipe is impure because it has to re-evaluate when the active language changes; keep its body cheap, since Angular runs it on every change detection cycle. In exchange, a translator working on Polish sees one, few, many, and other grouped under one key, and no per-language logic ends up in your components.

Formatting dates, numbers, and currencies

ngx-translate resolves strings. Locale-aware formatting belongs to Angular’s own pipes, and the two systems do not talk to each other by default. This catches almost every team once.

DatePipe, DecimalPipe, and CurrencyPipe read the injected LOCALE_ID, which is fixed at bootstrap and defaults to en-US. Calling translate.use('de') changes the active translation language and leaves LOCALE_ID exactly where it was, which is why dates keep rendering in American format after a language switch that otherwise worked.

Register the locale data you intend to support, then pass the active language to the pipe explicitly. DatePipe accepts locale as its fourth argument, after format and timezone:

import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localePl from '@angular/common/locales/pl';

registerLocaleData(localeDe);
registerLocaleData(localePl);

Expose the active language on the component with protected currentLang = this.translate.currentLang;, then hand it to each pipe:

<time>{{ order.placedAt | date: 'medium' : undefined : currentLang() }}</time>
<span>{{ order.total | currency: 'EUR' : 'symbol' : undefined : currentLang() }}</span>

The registerLocaleData reference covers the signature. Registering only the locales you ship keeps the bundle from carrying CLDR data for languages nobody uses.

Switching languages at runtime

Because translations resolve at runtime, angular i18n dynamic behavior comes almost free. use() loads a language on demand and updates every pipe, directive, and stream() subscription in place:

switchLanguage(lang: string) {
  this.translate.use(lang).subscribe(() => localStorage.setItem('lang', lang));
}

In v18 the active language is exposed as a signal, which composes cleanly with the rest of a modern Angular component:

protected isGerman = computed(() => this.currentLang() === 'de');

For content that arrives from an API rather than your translation files, translate the labels and leave the payload alone. A product description stored in your database belongs in your backend’s localization model rather than in en.json. Mixing the two is how translation files grow to thousands of keys nobody can audit.

Persist the choice and restore it before the first render: reading a stored language during bootstrap and passing it as lang avoids the flash of English while the preferred file loads.

Common ngx-translate mistakes

Mistake

What you see

Fix

Formatting dates after use() without passing locale

Dates stay in en-US format

Pass currentLang() as DatePipe’s fourth argument

Skipping registerLocaleData

Pipes format with en-US instead of the active locale

Import and register each shipped locale at bootstrap

Calling instant() before files load

The raw key renders in the UI

Use get() or stream(), or resolve during bootstrap

Defining only one and other plural keys

Polish and Russian read wrong at 2 and 5

Add few and many for languages that use them

Editing target language files by hand

Keys drift between languages over releases

Keep one source file and sync the rest automatically

Positional placeholders such as {{0}}

Translators reorder text and lose the meaning

Name every parameter after what it holds

Extra spaces inside braces, as in {{ name }}

The placeholder renders as literal text

Keep the braces tight: {{name}}

Scaling angular localization i18n workflows

The setup above is complete for one developer and two languages. At twelve languages and a weekly release the code stays much the same while the coordination grows: a source file that gains keys every sprint, translators who need context, and target files that have to stay structurally identical to the source. Since ngx-translate ships no extraction or sync tooling, that coordination is where the actual work accumulates.

LingoHub’s Angular integration handles that layer. It connects to your repository, pulls new and changed keys automatically as they land in the source file, and opens a pull request once translations are ready, which keeps localization inside the git workflow your team already reviews. Every key in the source language is tracked across all target files, so exports come back with the same structure you shipped, and a key added to en.json never goes missing in pl.json.

The Angular JSON format documentation covers how the files are processed. Flat and nested structures are both supported, and {{placeholder}} interpolation is recognized as the documented placeholder mechanism, which matches ngx-translate’s default parser exactly. Descriptions and quality rules can travel with a segment as metadata, giving translators the context that a key name alone never carries.

On the review side, quality checks flag mismatched placeholders and missing translations before they reach a build. A glossary keep terminology consistent across languages, translation memory reuses what has already been approved, and AI translation with human review shortens the first pass without handing final wording to a machine. Developers, translators, reviewers, and product managers work in the same place, which removes the spreadsheet round-trips that make localization feel expensive.

FAQ

What is i18n in Angular?

i18n is the practice of building an Angular application that can adapt to any language and region without code changes: user-facing text lives in translation files instead of templates, and formatting follows the active locale. Localization, or l10n, is the separate step of producing translations and locale conventions for each market you ship to.

How to use i18n in Angular with ngx-translate?

Install @ngx-translate/core and @ngx-translate/http-loader, register provideTranslateService with an HTTP loader pointing at /assets/i18n/, and keep one JSON file per language. Reference keys through the translate pipe in templates and TranslateService in TypeScript, and pass interpolation parameters as a named object.

Can users switch language without reloading the page?
Yes. ngx-translate resolves strings at runtime from a single application bundle, so calling translate.use('de') swaps every rendered string in place. Values read through the translate pipe, the directive, or stream() update automatically; values read once through get() or instant() do not.

Why do dates still show in English after switching language?

Angular’s DatePipe reads LOCALE_ID, which is set at bootstrap and unaffected by translate.use(). Call registerLocaleData for each locale you support and pass the active language as the pipe’s fourth argument to format dates, numbers, and currencies alongside your translated text.

Conclusion

Angular i18n with ngx-translate comes down to a handful of decisions that compound well: JSON files with one source of truth, named placeholders, plural keys driven by real CLDR categories, and locale passed explicitly to Angular’s formatting pipes.

As your application adds languages, keeping JSON files aligned becomes a workflow challenge. LingoHub connects translation files, repository updates, quality checks, and reviews in one place.

Want to test one project independently?

Start a 14-day free trial and try LingoHub with your own Angular translation files. No credit card is required.

Start a free trial

Evaluating LingoHub for a larger organization?

Book a demo if your setup involves multiple teams, repositories, complex workflows, migration requirements, security reviews, or custom integrations.

Book a demo

Related articles