Skip to main content

Internationalization (i18n)

Complete internationalization (i18n) support with locale-specific formatting, message translation, and seamless locale switching.

Features

  • Locale Support: Italian (it-IT) and English (en-US) out-of-the-box
  • Message Translation: Centralized translation system with override support
  • Format Utilities: Date, number, and currency formatting
  • Locale Switching: Runtime locale changes with reactive updates
  • Angular Integration: Uses Angular's LOCALE_ID and Intl API
  • Type-Safe: Full TypeScript support with interfaces
  • Extensible: Easy to add new locales and custom messages

Installation

npm install @tess-ui-library/core @angular/common

Quick Start

1. Register Locale Data

In your main.ts:

import { registerLocaleData } from '@angular/common';
import localeIt from '@angular/common/locales/it';
import localeEn from '@angular/common/locales/en';

// Register locale data
registerLocaleData(localeIt);
registerLocaleData(localeEn);

bootstrapApplication(AppComponent, appConfig);

2. Provide i18n Configuration

In your app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideI18n } from '@tess-ui-library/core';

export const appConfig: ApplicationConfig = {
providers: [
provideI18n({ defaultLocale: 'it-IT' })
]
};

3. Use in Components

import { Component, inject } from '@angular/core';
import { I18nService } from '@tess-ui-library/core';
import { TessDatePipe, TessNumberPipe, TessCurrencyPipe } from '@tess-ui-library/shared';

@Component({
selector: 'app-my-component',
standalone: true,
imports: [TessDatePipe, TessNumberPipe, TessCurrencyPipe],
template: `
<h1>{{ messages().common.save }}</h1>

<p>Date: {{ myDate | tessDate }}</p>
<p>Number: {{ 1234.56 | tessNumber }}</p>
<p>Currency: {{ 1234.56 | tessCurrency:'EUR' }}</p>

<button (click)="switchLocale()">
{{ currentLocale() === 'it-IT' ? 'Switch to English' : 'Passa all\'Italiano' }}
</button>
`
})
export class MyComponent {
i18nService = inject(I18nService);

messages = this.i18nService.messages;
currentLocale = this.i18nService.currentLocale;
myDate = new Date();

switchLocale() {
const newLocale = this.currentLocale() === 'it-IT' ? 'en-US' : 'it-IT';
this.i18nService.setLocale(newLocale);
}
}

Message Translation

Available Messages

Common Messages

KeyItalian (it-IT)English (en-US)
common.saveSalvaSave
common.cancelAnnullaCancel
common.deleteEliminaDelete
common.editModificaEdit
common.searchCercaSearch
common.loadingCaricamento...Loading...

Validation Messages

KeyItalian (it-IT)English (en-US)
validation.requiredCampo obbligatorioThis field is required
validation.emailInserire un indirizzo email validoPlease enter a valid email address
validation.minLength(5)Lunghezza minima: 5 caratteriMinimum length: 5 characters
validation.max(100)Valore massimo: 100Maximum value: 100

View full message catalog →

Usage in Templates

<!-- Access messages via service -->
<h1>{{ i18nService.messages().common.save }}</h1>
<p>{{ i18nService.messages().validation.required }}</p>

<!-- With parameters -->
<span>{{ i18nService.messages().validation.minLength(5) }}</span>

Usage in TypeScript

import { inject } from '@angular/core';
import { I18nService } from '@tess-ui-library/core';

export class MyService {
private i18n = inject(I18nService);

showError() {
const errorMsg = this.i18n.messages().errors.generic;
console.error(errorMsg); // "Si è verificato un errore" or "An error occurred"
}

getValidationMessage(fieldName: string, minLength: number) {
const msg = this.i18n.messages().validation.minLength(minLength);
return `${fieldName}: ${msg}`;
}
}

Custom Message Overrides

Override default messages for your application:

import { provideI18n } from '@tess-ui-library/core';
import { I18nMessages } from '@tess-ui-library/core';

// Define custom overrides
const customMessages = new Map<string, Partial<I18nMessages>>([
['it-IT', {
common: {
save: 'Conferma', // Override 'Salva' → 'Conferma'
cancel: 'Indietro' // Override 'Annulla' → 'Indietro'
},
validation: {
required: 'Obbligatorio' // Override 'Campo obbligatorio' → 'Obbligatorio'
}
}],
['en-US', {
common: {
save: 'Confirm' // Override 'Save' → 'Confirm'
}
}]
]);

// Provide with overrides
export const appConfig: ApplicationConfig = {
providers: [
provideI18n({
defaultLocale: 'it-IT',
customMessages
})
]
};

Format Utilities

Date Formatting

import { TessDatePipe } from '@tess-ui-library/shared';

// In template
<p>{{ myDate | tessDate }}</p>
<!-- it-IT: 08/01/2026, 14:30 -->
<!-- en-US: 1/8/2026, 2:30 PM -->

<p>{{ myDate | tessDate:'mediumDate' }}</p>
<!-- it-IT: 8 gen 2026 -->
<!-- en-US: Jan 8, 2026 -->

<p>{{ myDate | tessDate:'dd/MM/yyyy HH:mm' }}</p>
<!-- Both: 08/01/2026 14:30 -->

Format Options:

  • 'short': Short date + time (default)
  • 'medium': Medium date + time
  • 'long': Long date + time
  • 'full': Full date + time
  • 'shortDate', 'mediumDate', 'longDate', 'fullDate'
  • 'shortTime', 'mediumTime', 'longTime', 'fullTime'
  • Custom format: 'dd/MM/yyyy', 'HH:mm:ss', etc.

Number Formatting

import { TessNumberPipe } from '@tess-ui-library/shared';

// In template
<p>{{ 1234.56 | tessNumber }}</p>
<!-- it-IT: 1.234,56 -->
<!-- en-US: 1,234.56 -->

<p>{{ 1234.56 | tessNumber:'1.0-0' }}</p>
<!-- it-IT: 1.235 -->
<!-- en-US: 1,235 -->

<p>{{ 0.123456 | tessNumber:'1.4-4' }}</p>
<!-- it-IT: 0,1235 -->
<!-- en-US: 0.1235 -->

Digits Info Format: 'minIntegerDigits.minFractionDigits-maxFractionDigits'

Currency Formatting

import { TessCurrencyPipe } from '@tess-ui-library/shared';

// In template
<p>{{ 1234.56 | tessCurrency:'EUR' }}</p>
<!-- it-IT:1.234,56 -->
<!-- en-US:1,234.56 -->

<p>{{ 1234.56 | tessCurrency:'USD' }}</p>
<!-- it-IT: $1.234,56 -->
<!-- en-US: $1,234.56 -->

<p>{{ 1234.56 | tessCurrency:'EUR':'code' }}</p>
<!-- it-IT: EUR1.234,56 -->
<!-- en-US: EUR1,234.56 -->

<p>{{ 1234.56 | tessCurrency:'EUR':'symbol':'1.0-0' }}</p>
<!-- it-IT:1.235 -->
<!-- en-US:1,235 -->

FormatUtils Service

For programmatic formatting in components/services:

import { inject } from '@angular/core';
import { FormatUtils } from '@tess-ui-library/shared';

export class MyService {
private formatUtils = inject(FormatUtils);

formatData(data: any) {
// Date formatting
const dateStr = this.formatUtils.formatDate(data.date, 'shortDate');

// Number formatting
const numStr = this.formatUtils.formatNumber(data.value, '1.2-2');

// Currency formatting
const currStr = this.formatUtils.formatCurrency(data.price, 'EUR');

// File size
const sizeStr = this.formatUtils.formatFileSize(data.bytes);
// Example: "1.5 KB", "2.3 MB"

// Percentage
const pctStr = this.formatUtils.formatPercent(data.ratio);
// Example: "12.34%"

// Get separators
const decSep = this.formatUtils.getDecimalSeparator(); // "," or "."
const thouSep = this.formatUtils.getThousandsSeparator(); // "." or ","

return { dateStr, numStr, currStr, sizeStr, pctStr };
}
}

Locale Switching

Programmatic Switching

import { Component, inject } from '@angular/core';
import { I18nService } from '@tess-ui-library/core';

@Component({
selector: 'app-locale-switcher',
template: `
<select [(ngModel)]="selectedLocale" (change)="onLocaleChange()">
<option value="it-IT">🇮🇹 Italiano</option>
<option value="en-US">🇺🇸 English</option>
</select>
`
})
export class LocaleSwitcherComponent {
i18nService = inject(I18nService);
selectedLocale = this.i18nService.currentLocale();

onLocaleChange() {
this.i18nService.setLocale(this.selectedLocale);
}
}

Effects of Locale Switching

When i18nService.setLocale() is called:

  1. Messages update: All translated messages change
  2. Formats update: Date, number, currency pipes re-format
  3. LOCALE_ID updates: Angular's DI token updates
  4. Document lang: <html lang=""> attribute updates (a11y)
  5. Reactive: All components using signals update automatically

Examples

DataTable with i18n

import { Component, inject } from '@angular/core';
import { I18nService } from '@tess-ui-library/core';
import { DataTableComponent } from '@tess-ui-library/shared';
import { TessDatePipe, TessNumberPipe } from '@tess-ui-library/shared';

@Component({
selector: 'app-users-table',
imports: [DataTableComponent, TessDatePipe, TessNumberPipe],
template: `
<tess-data-table
[data]="users"
[columns]="columns"
[emptyMessage]="messages().dataTable.emptyMessage"
[exportFilename]="'users'">
</tess-data-table>
`
})
export class UsersTableComponent {
i18nService = inject(I18nService);
messages = this.i18nService.messages;

users = [
{ id: 1, name: 'Mario Rossi', registeredAt: new Date(), balance: 1234.56 },
// ...
];

columns = [
{ field: 'id', header: 'ID' },
{ field: 'name', header: this.i18nService.messages().common.name },
{
field: 'registeredAt',
header: 'Registration',
// Uses tessDate pipe internally
},
{
field: 'balance',
header: 'Balance',
// Uses tessCurrency pipe internally
}
];
}

Form Validation with i18n

import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { I18nService } from '@tess-ui-library/core';

@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="form">
<input formControlName="email" />
@if (form.get('email')?.errors?.['required']) {
<span class="error">{{ messages().validation.required }}</span>
}
@if (form.get('email')?.errors?.['email']) {
<span class="error">{{ messages().validation.email }}</span>
}

<input formControlName="username" />
@if (form.get('username')?.errors?.['minlength']) {
<span class="error">
{{ messages().validation.minLength(5) }}
</span>
}
</form>
`
})
export class UserFormComponent {
private fb = inject(FormBuilder);
i18nService = inject(I18nService);
messages = this.i18nService.messages;

form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
username: ['', [Validators.required, Validators.minLength(5)]]
});
}

Best Practices

1. Use Signals for Reactivity

// ✅ GOOD: Use signals for automatic updates
messages = this.i18nService.messages;
currentLocale = this.i18nService.currentLocale;

// ❌ BAD: Direct property access (won't update on locale change)
messages = this.i18nService.messages();

2. Centralize Format Logic

// ✅ GOOD: Use pipes or FormatUtils
<p>{{ amount | tessCurrency:'EUR' }}</p>

// ❌ BAD: Manual formatting
<p>{{ amount.toFixed(2).replace('.', ',') }}</p>

3. Register Locale Data Early

// ✅ GOOD: Register in main.ts before bootstrap
registerLocaleData(localeIt);
registerLocaleData(localeEn);
bootstrapApplication(AppComponent, appConfig);

// ❌ BAD: Register in component or lazy-loaded module

4. Override Messages Thoughtfully

// ✅ GOOD: Override only what's needed
customMessages.set('it-IT', {
common: { save: 'Conferma' } // Only override 'save'
});

// ❌ BAD: Override entire structure (loses updates)
customMessages.set('it-IT', {
common: { save: 'Conferma' }, // Where are other common messages?
validation: {} // All validation messages lost!
});

Testing

import { TestBed } from '@angular/core/testing';
import { I18nService } from '@tess-ui-library/core';
import { provideI18n } from '@tess-ui-library/core';

describe('MyComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideI18n({ defaultLocale: 'it-IT' })
]
});
});

it('should display Italian messages', () => {
const fixture = TestBed.createComponent(MyComponent);
const compiled = fixture.nativeElement;

expect(compiled.textContent).toContain('Salva');
});

it('should switch to English', () => {
const service = TestBed.inject(I18nService);
service.setLocale('en-US');

expect(service.messages().common.save).toBe('Save');
});
});

Troubleshooting

Formats Not Updating

Problem: Date/number formats don't change after locale switch.

Solution: Ensure you're using the pipes, not manual formatting:

<!-- ✅ GOOD -->
<p>{{ myDate | tessDate }}</p>

<!-- ❌ BAD -->
<p>{{ myDate.toLocaleDateString() }}</p>

Missing Locale Data

Problem: Error "Missing locale data for 'it-IT'".

Solution: Register locale data in main.ts:

import { registerLocaleData } from '@angular/common';
import localeIt from '@angular/common/locales/it';

registerLocaleData(localeIt);

Messages Not Overriding

Problem: Custom message overrides not applying.

Solution: Ensure partial override structure matches I18nMessages interface:

// ✅ GOOD
customMessages.set('it-IT', {
common: { save: 'Conferma' }
});

// ❌ BAD
customMessages.set('it-IT', {
save: 'Conferma' // Missing 'common' parent
});