Skip to main content
Angular Internationalization

Internationalization Overview

Estimated Time: 2 hours | Difficulty: Intermediate | Prerequisites: Components, Routing
Internationalization (i18n) is the process of designing your application to support multiple languages and locales. Angular provides built-in i18n tools and also supports third-party libraries like ngx-translate and Transloco.
┌─────────────────────────────────────────────────────────────────────────┐
│              Angular i18n Approaches                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Built-in i18n (AOT)             Runtime i18n (ngx-translate/Transloco)│
│   ─────────────────────           ──────────────────────────────────────│
│                                                                          │
│   ✓ Best performance              ✓ Switch languages at runtime        │
│   ✓ AOT compilation               ✓ Single bundle for all languages    │
│   ✓ ICU message format            ✓ Lazy load translations             │
│   ✗ Separate build per locale     ✓ Easier to manage                   │
│   ✗ No runtime switching          ✓ API/CMS integration                │
│                                   ✗ Larger bundle size                  │
│                                                                          │
│   Best for: Marketing sites,      Best for: Apps, dashboards,          │
│   SEO-critical pages              admin panels, SPAs                    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Built-in Angular i18n

Setup

# Add localize package
ng add @angular/localize

# Generate translation file
ng extract-i18n --output-path src/locale

Marking Text for Translation

<!-- Using i18n attribute -->
<h1 i18n>Welcome to our application</h1>

<!-- With description and meaning -->
<h1 i18n="site header|Main welcome message">Welcome to our application</h1>

<!-- With custom ID for stable translations -->
<h1 i18n="@@welcomeHeader">Welcome to our application</h1>

<!-- Translate attributes -->
<img [src]="logo" i18n-alt alt="Company logo" i18n-title title="Click to go home" />

<!-- Plural expressions -->
<span i18n>{count, plural, 
  =0 {No items}
  =1 {One item}
  other {{{count}} items}
}</span>

<!-- Select expressions -->
<span i18n>{gender, select,
  male {He is online}
  female {She is online}
  other {They are online}
}</span>

<!-- Nested ICU expressions -->
<span i18n>{gender, select,
  male {He has {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
  female {She has {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
  other {They have {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
}</span>

Translation File (messages.xlf)

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="welcomeHeader" datatype="html">
        <source>Welcome to our application</source>
        <target>Bienvenido a nuestra aplicación</target>
        <note priority="1" from="description">Main welcome message</note>
        <note priority="1" from="meaning">site header</note>
      </trans-unit>
      
      <trans-unit id="itemCount" datatype="html">
        <source>{VAR_PLURAL, plural, =0 {No items} =1 {One item} other {<x id="INTERPOLATION"/> items}}</source>
        <target>{VAR_PLURAL, plural, =0 {Sin elementos} =1 {Un elemento} other {<x id="INTERPOLATION"/> elementos}}</target>
      </trans-unit>
    </body>
  </file>
</xliff>

Build Configuration

// angular.json
{
  "projects": {
    "my-app": {
      "i18n": {
        "sourceLocale": "en-US",
        "locales": {
          "es": {
            "translation": "src/locale/messages.es.xlf",
            "baseHref": "/es/"
          },
          "fr": {
            "translation": "src/locale/messages.fr.xlf",
            "baseHref": "/fr/"
          },
          "de": "src/locale/messages.de.xlf"
        }
      },
      "architect": {
        "build": {
          "options": {
            "localize": true
          },
          "configurations": {
            "es": {
              "localize": ["es"]
            },
            "fr": {
              "localize": ["fr"]
            }
          }
        },
        "serve": {
          "configurations": {
            "es": {
              "buildTarget": "my-app:build:development,es"
            }
          }
        }
      }
    }
  }
}
# Build all locales
ng build --localize

# Build specific locale
ng build --configuration=es

# Serve specific locale
ng serve --configuration=es

Setup

npm install @jsverse/transloco
ng add @jsverse/transloco

Configuration

// transloco.config.ts
import { TranslocoGlobalConfig } from '@jsverse/transloco-utils';

const config: TranslocoGlobalConfig = {
  rootTranslationsPath: 'src/assets/i18n/',
  langs: ['en', 'es', 'fr', 'de'],
  defaultLang: 'en'
};

export default config;

// app.config.ts
import { provideTransloco, TranslocoModule } from '@jsverse/transloco';
import { TranslocoHttpLoader } from './transloco-loader';

export const appConfig: ApplicationConfig = {
  providers: [
    provideTransloco({
      config: {
        availableLangs: ['en', 'es', 'fr', 'de'],
        defaultLang: 'en',
        fallbackLang: 'en',
        reRenderOnLangChange: true,
        prodMode: !isDevMode(),
        missingHandler: {
          useFallbackTranslation: true,
          logMissingKey: true
        }
      },
      loader: TranslocoHttpLoader
    })
  ]
};

// transloco-loader.ts
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
  private http = inject(HttpClient);
  
  getTranslation(lang: string) {
    return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
  }
}

Translation Files

// assets/i18n/en.json
{
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "edit": "Edit",
    "loading": "Loading...",
    "error": "An error occurred"
  },
  "auth": {
    "login": "Log In",
    "logout": "Log Out",
    "register": "Register",
    "email": "Email",
    "password": "Password",
    "forgotPassword": "Forgot Password?",
    "welcomeBack": "Welcome back, {{name}}!"
  },
  "products": {
    "title": "Products",
    "addToCart": "Add to Cart",
    "outOfStock": "Out of Stock",
    "price": "Price: {{price, currency}}",
    "itemCount": {
      "zero": "No items",
      "one": "1 item",
      "other": "{{count}} items"
    }
  },
  "validation": {
    "required": "This field is required",
    "email": "Please enter a valid email",
    "minLength": "Minimum {{min}} characters required",
    "maxLength": "Maximum {{max}} characters allowed"
  }
}

// assets/i18n/es.json
{
  "common": {
    "save": "Guardar",
    "cancel": "Cancelar",
    "delete": "Eliminar",
    "edit": "Editar",
    "loading": "Cargando...",
    "error": "Ocurrió un error"
  },
  "auth": {
    "login": "Iniciar Sesión",
    "logout": "Cerrar Sesión",
    "register": "Registrarse",
    "email": "Correo Electrónico",
    "password": "Contraseña",
    "forgotPassword": "¿Olvidaste tu contraseña?",
    "welcomeBack": "¡Bienvenido de nuevo, {{name}}!"
  },
  "products": {
    "title": "Productos",
    "addToCart": "Añadir al Carrito",
    "outOfStock": "Agotado",
    "price": "Precio: {{price, currency}}",
    "itemCount": {
      "zero": "Sin artículos",
      "one": "1 artículo",
      "other": "{{count}} artículos"
    }
  }
}

Using Transloco in Components

// product-list.component.ts
import { TranslocoModule, TranslocoService } from '@jsverse/transloco';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [TranslocoModule],
  template: `
    <!-- Structural directive approach (recommended) -->
    <ng-container *transloco="let t; read: 'products'">
      <h1>{{ t('title') }}</h1>
      
      @for (product of products(); track product.id) {
        <div class="product-card">
          <h3>{{ product.name }}</h3>
          <p>{{ t('price', { price: product.price }) }}</p>
          
          @if (product.inStock) {
            <button>{{ t('addToCart') }}</button>
          } @else {
            <span class="out-of-stock">{{ t('outOfStock') }}</span>
          }
        </div>
      }
      
      <p>{{ t('itemCount', { count: products().length }) }}</p>
    </ng-container>
    
    <!-- Pipe approach -->
    <button>{{ 'common.save' | transloco }}</button>
    
    <!-- With params -->
    <p>{{ 'auth.welcomeBack' | transloco:{ name: userName() } }}</p>
  `
})
export class ProductListComponent {
  private transloco = inject(TranslocoService);
  
  products = input.required<Product[]>();
  userName = signal('John');
  
  // Programmatic translation
  showSuccessMessage() {
    const message = this.transloco.translate('common.saved');
    // Or with params
    const greeting = this.transloco.translate('auth.welcomeBack', { 
      name: this.userName() 
    });
  }
  
  // Observe translations (reactive)
  message$ = this.transloco.selectTranslate('products.title');
}

Language Switcher

// language-switcher.component.ts
@Component({
  selector: 'app-language-switcher',
  standalone: true,
  imports: [TranslocoModule],
  template: `
    <div class="language-switcher">
      @for (lang of availableLangs; track lang.id) {
        <button
          [class.active]="lang.id === activeLang()"
          (click)="switchLang(lang.id)"
        >
          {{ lang.label }}
        </button>
      }
    </div>
  `
})
export class LanguageSwitcherComponent {
  private transloco = inject(TranslocoService);
  
  availableLangs = [
    { id: 'en', label: 'English' },
    { id: 'es', label: 'Español' },
    { id: 'fr', label: 'Français' },
    { id: 'de', label: 'Deutsch' }
  ];
  
  activeLang = signal(this.transloco.getActiveLang());
  
  constructor() {
    // Subscribe to language changes
    this.transloco.langChanges$.subscribe(lang => {
      this.activeLang.set(lang);
      // Persist preference
      localStorage.setItem('preferredLang', lang);
      // Update document lang attribute
      document.documentElement.lang = lang;
    });
  }
  
  switchLang(lang: string) {
    this.transloco.setActiveLang(lang);
  }
}

Scoped Translations (Lazy Loading)

// features/admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./admin.component'),
    providers: [
      provideTranslocoScope({
        scope: 'admin',
        loader: {
          en: () => import('./i18n/en.json'),
          es: () => import('./i18n/es.json')
        }
      })
    ]
  }
];

// features/admin/admin.component.ts
@Component({
  template: `
    <ng-container *transloco="let t; read: 'admin'">
      <h1>{{ t('dashboard.title') }}</h1>
      <p>{{ t('dashboard.welcome') }}</p>
    </ng-container>
  `
})
export class AdminComponent {}

Locale-Aware Formatting

Number, Date, and Currency Formatting

// locale.service.ts
@Injectable({ providedIn: 'root' })
export class LocaleService {
  private transloco = inject(TranslocoService);
  
  private localeMap: Record<string, string> = {
    'en': 'en-US',
    'es': 'es-ES',
    'fr': 'fr-FR',
    'de': 'de-DE'
  };
  
  get currentLocale(): string {
    return this.localeMap[this.transloco.getActiveLang()] ?? 'en-US';
  }
  
  formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
    return new Intl.NumberFormat(this.currentLocale, options).format(value);
  }
  
  formatCurrency(value: number, currency = 'USD'): string {
    return new Intl.NumberFormat(this.currentLocale, {
      style: 'currency',
      currency
    }).format(value);
  }
  
  formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
    return new Intl.DateTimeFormat(this.currentLocale, options).format(date);
  }
  
  formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit): string {
    return new Intl.RelativeTimeFormat(this.currentLocale, {
      numeric: 'auto'
    }).format(value, unit);
  }
}

// Custom pipe for locale-aware formatting
@Pipe({
  name: 'localeCurrency',
  standalone: true,
  pure: false
})
export class LocaleCurrencyPipe implements PipeTransform {
  private locale = inject(LocaleService);
  
  transform(value: number, currency = 'USD'): string {
    return this.locale.formatCurrency(value, currency);
  }
}

// Usage
@Component({
  imports: [LocaleCurrencyPipe],
  template: `
    <span>{{ product.price | localeCurrency:'EUR' }}</span>
  `
})
export class PriceComponent {}

RTL (Right-to-Left) Support

// rtl.service.ts
@Injectable({ providedIn: 'root' })
export class RtlService {
  private transloco = inject(TranslocoService);
  
  private rtlLanguages = ['ar', 'he', 'fa', 'ur'];
  
  isRtl = signal(false);
  
  constructor() {
    this.transloco.langChanges$.subscribe(lang => {
      const isRtl = this.rtlLanguages.includes(lang);
      this.isRtl.set(isRtl);
      
      document.documentElement.dir = isRtl ? 'rtl' : 'ltr';
      document.documentElement.lang = lang;
    });
  }
}

// app.component.ts
@Component({
  template: `
    <div [class.rtl]="rtl.isRtl()" [dir]="rtl.isRtl() ? 'rtl' : 'ltr'">
      <router-outlet />
    </div>
  `,
  styles: [`
    :host {
      --spacing-start: 1rem;
      --spacing-end: 0;
    }
    
    :host-context([dir="rtl"]) {
      --spacing-start: 0;
      --spacing-end: 1rem;
    }
    
    .card {
      margin-inline-start: var(--spacing-start);
      margin-inline-end: var(--spacing-end);
    }
  `]
})
export class AppComponent {
  rtl = inject(RtlService);
}

Best Practices

Use Translation Keys

Use meaningful, hierarchical keys like products.details.price

Avoid Concatenation

Never concatenate translated strings - use placeholders instead

Handle Plurals Properly

Use ICU message format for pluralization

Lazy Load Translations

Load feature-specific translations on demand
// ❌ Bad - String concatenation
const message = translate('hello') + ' ' + userName + '!';

// ✅ Good - Use placeholders
// Translation: "Hello, {{name}}!"
const message = translate('greeting', { name: userName });

// ❌ Bad - Manual plural handling
const items = count === 1 ? 'item' : 'items';

// ✅ Good - ICU plural format
// Translation: "{count, plural, =1 {# item} other {# items}}"
const items = translate('itemCount', { count });

Next: Accessibility (a11y)

Build accessible Angular applications for all users