
Table of content
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 |
| Rendering text in a template | Yes |
| Element content with inline markup | Yes |
| Signal-based component code | Yes |
| Long-lived subscription in TypeScript | Yes, re-emits |
| One-off async lookup | No, emits once and completes |
| 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 | Dates stay in | Pass |
Skipping | Pipes format with | Import and register each shipped locale at bootstrap |
Calling | The raw key renders in the UI | Use |
Defining only | Polish and Russian read wrong at 2 and 5 | Add |
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 | Translators reorder text and lose the meaning | Name every parameter after what it holds |
Extra spaces inside braces, as in | The placeholder renders as literal text | Keep the braces tight: |
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.
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.
Related articles

i18n and l10n for AngularJS apps from development to deployment
Adding multiple languages to your app requires careful planning. Read our blog about i18n and l10n best practices specifically for AngularJS apps.

AngularJS chart directives for app development
In the article, we compared the popular AngularJS directives for image cropping. Find a list and our champion inside.
$touched is the new $dirty - AngularJS Migration Guide 1.2 to 1.3
Need help to migrate from AngularJS 1.2 to 1.3? Check out our guide. I'll cover key changes and introduce a new feature. Click and learn more ib LingoHub's blog
Comparison of AngularJS directives for charts in front end app development
Explore the best AngularJS chart directives for front end development. Compare popular chart libraries and choose the right solution for your application.