> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 19. Internationalization (i18n)

> Build multi-language Angular applications with i18n and localization best practices

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/angular-hero.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=32645ae19fa9bc25d3ec281022aba371" alt="Angular Internationalization" width="1200" height="400" data-path="images/courses/angular-crash-course/angular-hero.svg" />
</Frame>

## Internationalization Overview

<Info>
  **Estimated Time**: 2 hours | **Difficulty**: Intermediate | **Prerequisites**: Components, Routing
</Info>

Internationalization (i18n) is the process of designing your application to support multiple languages and locales. Think of it like building a house with interchangeable wall panels -- the structure stays the same, but the surfaces can swap out depending on who is living there. Localization (l10n) is the actual process of creating those panels for each locale.

Angular provides built-in i18n tools and also supports third-party libraries like ngx-translate and Transloco. The choice between them is one of the most consequential architectural decisions you will make early in a project, because switching later is extremely painful -- every template and every string literal gets touched.

<Warning>
  **Choose early, or pay later.** Retrofitting i18n onto an existing app means touching every template in your codebase. If there is even a 20% chance your app will need multiple languages, set up the i18n infrastructure on day one, even if you only ship one language initially.
</Warning>

```
┌─────────────────────────────────────────────────────────────────────────┐
│              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

```bash theme={null}
# Add localize package
ng add @angular/localize

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

### Marking Text for Translation

```html theme={null}
<!-- 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 theme={null}
<?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

```json theme={null}
// 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"
            }
          }
        }
      }
    }
  }
}
```

```bash theme={null}
# Build all locales
ng build --localize

# Build specific locale
ng build --configuration=es

# Serve specific locale
ng serve --configuration=es
```

***

## Transloco (Recommended Runtime Solution)

### Setup

```bash theme={null}
npm install @jsverse/transloco
ng add @jsverse/transloco
```

### Configuration

```typescript theme={null}
// 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

```json theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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)

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Use Translation Keys" icon="key">
    Use meaningful, hierarchical keys like `products.details.price`
  </Card>

  <Card title="Avoid Concatenation" icon="ban">
    Never concatenate translated strings - use placeholders instead
  </Card>

  <Card title="Handle Plurals Properly" icon="list-ol">
    Use ICU message format for pluralization
  </Card>

  <Card title="Lazy Load Translations" icon="download">
    Load feature-specific translations on demand
  </Card>
</CardGroup>

```typescript theme={null}
// ❌ Bad - String concatenation breaks in languages with different word order.
// German says "Willkommen zurück, Max!" but Japanese puts the name first.
const message = translate('hello') + ' ' + userName + '!';

// ✅ Good - Placeholders let each language put the variable where it belongs.
// EN: "Hello, {{name}}!"  JA: "{{name}}さん、こんにちは！"
const message = translate('greeting', { name: userName });

// ❌ Bad - Many languages have more than two plural forms.
// Polish has different forms for 1, 2-4, 5-21, and 22+.
const items = count === 1 ? 'item' : 'items';

// ✅ Good - ICU plural format handles every language's plural rules.
// Translation: "{count, plural, =1 {# item} other {# items}}"
const items = translate('itemCount', { count });
```

<Tip>
  **Practical tip**: Never embed punctuation (periods, exclamation marks, colons) outside of translation strings. Languages use different punctuation -- French puts a space before colons, Spanish uses inverted question marks, and Japanese uses different full stops. Let the translator handle all punctuation inside the translated string.
</Tip>

***

<Card title="Next: Accessibility (a11y)" icon="arrow-right" href="/courses/angular-crash-course/20-accessibility">
  Build accessible Angular applications for all users
</Card>
