Internationalization (i18n) is one of those features that’s easier to add early than retrofit later. If there’s any chance your Angular app will need multiple languages, setting up the foundation now will save you significant headaches down the road.
This guide covers everything you need to know about internationalizing Angular applications in 2025, from choosing the right approach to managing translations at scale.
Two Approaches: Built-in vs. Runtime
Angular offers two main paths for internationalization:
1. Built-in Angular i18n (compile-time)
- Translations baked into the build
- Separate bundles per language
- Best performance
- Harder to switch languages without page reload
2. ngx-translate or Transloco (runtime)
- Translations loaded at runtime
- Single build, multiple languages
- Switch languages without reload
- Slightly more runtime overhead
For most applications, we recommend ngx-translate or Transloco for their flexibility. Angular’s built-in i18n is great for performance-critical apps with a small number of languages.
Setting Up ngx-translate
Let’s walk through a complete setup with ngx-translate, the most popular runtime solution.
Installation
npm install @ngx-translate/core @ngx-translate/http-loader
Basic Configuration
Create your translation loader in app.module.ts (or app.config.ts for standalone):
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
@NgModule({
imports: [
HttpClientModule,
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
]
})
export class AppModule { }
For standalone components (Angular 17+):
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
)
]
};
Create Translation Files
Create JSON files in src/assets/i18n/:
en.json:
{
"common": {
"welcome": "Welcome",
"login": "Log In",
"logout": "Log Out",
"save": "Save",
"cancel": "Cancel"
},
"home": {
"title": "Welcome to Our App",
"description": "The best way to manage your tasks"
},
"errors": {
"required": "This field is required",
"email": "Please enter a valid email"
}
}
es.json:
{
"common": {
"welcome": "Bienvenido",
"login": "Iniciar Sesión",
"logout": "Cerrar Sesión",
"save": "Guardar",
"cancel": "Cancelar"
},
"home": {
"title": "Bienvenido a Nuestra App",
"description": "La mejor manera de gestionar tus tareas"
},
"errors": {
"required": "Este campo es obligatorio",
"email": "Por favor ingresa un email válido"
}
}
Using Translations in Templates
<!-- Using the pipe -->
<h1>{{ 'home.title' | translate }}</h1>
<!-- With parameters -->
<p>{{ 'greeting' | translate:{ name: userName } }}</p>
<!-- Using directive -->
<span [translate]="'common.welcome'"></span>
Using Translations in Components
import { TranslateService } from '@ngx-translate/core';
@Component({...})
export class MyComponent {
constructor(private translate: TranslateService) {
// Get translation as observable
this.translate.get('home.title').subscribe(text => {
console.log(text);
});
// Get instant translation (must be loaded)
const text = this.translate.instant('home.title');
}
changeLanguage(lang: string) {
this.translate.use(lang);
}
}
Structuring Translation Keys
A good key structure makes translations maintainable. Here’s what we recommend:
feature.component.element
Examples:
{
"auth": {
"login": {
"title": "Sign In",
"email_label": "Email Address",
"password_label": "Password",
"submit": "Sign In",
"forgot_password": "Forgot your password?"
},
"register": {
"title": "Create Account",
"submit": "Sign Up"
}
},
"dashboard": {
"header": {
"title": "Dashboard",
"subtitle": "Welcome back, {{name}}"
}
}
}
Avoid:
- Generic keys like
text1,label,button - Deeply nested structures (3 levels is usually enough)
- Duplicating strings—use shared keys for common elements
Handling Pluralization
ngx-translate supports ICU message format for complex pluralization:
{
"items": "{count, plural, =0{No items} =1{One item} other{{{count}} items}}"
}
<span>{{ 'items' | translate:{ count: itemCount } }}</span>
Language Switching with Persistence
Create a language service to handle switching and persistence:
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Injectable({ providedIn: 'root' })
export class LanguageService {
private readonly STORAGE_KEY = 'app_language';
readonly supportedLanguages = ['en', 'es', 'de', 'fr'];
readonly defaultLanguage = 'en';
constructor(private translate: TranslateService) {
this.initLanguage();
}
private initLanguage(): void {
const stored = localStorage.getItem(this.STORAGE_KEY);
const browserLang = this.translate.getBrowserLang();
let lang = this.defaultLanguage;
if (stored && this.supportedLanguages.includes(stored)) {
lang = stored;
} else if (browserLang && this.supportedLanguages.includes(browserLang)) {
lang = browserLang;
}
this.translate.setDefaultLang(this.defaultLanguage);
this.translate.use(lang);
}
setLanguage(lang: string): void {
if (this.supportedLanguages.includes(lang)) {
localStorage.setItem(this.STORAGE_KEY, lang);
this.translate.use(lang);
}
}
getCurrentLanguage(): string {
return this.translate.currentLang || this.defaultLanguage;
}
}
Lazy Loading Translations
For large apps, load translations per module:
// feature.module.ts
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function FeatureLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/feature/', '.json');
}
@NgModule({
imports: [
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: FeatureLoaderFactory,
deps: [HttpClient]
},
isolate: true
})
]
})
export class FeatureModule { }
Managing Translations at Scale
As your app grows, managing JSON files manually becomes painful. This is where translation management tools come in.
The Manual Approach (Small Projects)
For small projects with a few languages, you can manage JSON files directly:
- Create a spreadsheet with keys and translations
- Export to JSON manually
- Keep files in sync manually
This breaks down quickly as you add languages or team members.
Using LangCtl for Angular Projects
LangCtl integrates directly with Angular projects. Here’s a typical workflow:
Initial Setup:
# Install CLI
npm install -g langctl
# Initialize in your project
langctl init
# Scan for translation keys
langctl scan ./src --format ngx-translate
Daily Workflow:
# Push new keys to dashboard
langctl push
# Pull latest translations
langctl pull
The CLI automatically detects your Angular setup and syncs with your assets/i18n directory.
Extracting Keys Automatically
LangCtl scans your templates and TypeScript files to find translation keys:
langctl scan ./src --format ngx-translate
This finds:
{{ 'key' | translate }}in templatestranslate.get('key')in componentstranslate.instant('key')calls
New keys are flagged for translation, and unused keys are identified for cleanup.
Testing Translations
Don’t forget to test your i18n implementation:
describe('TranslatedComponent', () => {
let translate: TranslateService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: FakeTranslateLoader
}
})
]
}).compileComponents();
translate = TestBed.inject(TranslateService);
translate.setDefaultLang('en');
translate.use('en');
});
it('should display translated title', () => {
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const title = fixture.nativeElement.querySelector('h1');
expect(title.textContent).toContain('Expected Translation');
});
});
Common Pitfalls to Avoid
1. Hardcoding strings “temporarily”
They’re never temporary. Use translation keys from the start, even for prototype text.
2. Using long translation keys
Keys like pages.dashboard.sections.analytics.charts.revenue.title are hard to manage. Keep them concise.
3. Forgetting about pluralization
English pluralization rules don’t apply everywhere. Use ICU format for numbers.
4. Ignoring RTL languages
If you might support Arabic or Hebrew, set up your CSS for RTL early. It’s much harder to add later.
5. Not providing context for translators
“Save” could mean many things. Add descriptions or context for ambiguous strings.
Conclusion
Internationalization in Angular is straightforward once you have the right setup. The key decisions are:
- Runtime vs compile-time: Use ngx-translate for flexibility, built-in i18n for performance
- Key structure: Be consistent and keep it shallow
- Management: Use a tool like LangCtl once you have more than a couple of languages
If you’re building a new Angular app, add i18n support now—even if you only need one language today. The incremental effort is minimal, and future you will be grateful.
Have questions about Angular i18n? We’ve built dozens of multilingual Angular apps and are happy to help. Get in touch or check out LangCtl if you need a better translation workflow.