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
| Key | Italian (it-IT) | English (en-US) |
|---|---|---|
common.save | Salva | Save |
common.cancel | Annulla | Cancel |
common.delete | Elimina | Delete |
common.edit | Modifica | Edit |
common.search | Cerca | Search |
common.loading | Caricamento... | Loading... |
Validation Messages
| Key | Italian (it-IT) | English (en-US) |
|---|---|---|
validation.required | Campo obbligatorio | This field is required |
validation.email | Inserire un indirizzo email valido | Please enter a valid email address |
validation.minLength(5) | Lunghezza minima: 5 caratteri | Minimum length: 5 characters |
validation.max(100) | Valore massimo: 100 | Maximum value: 100 |
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:
- ✅ Messages update: All translated messages change
- ✅ Formats update: Date, number, currency pipes re-format
- ✅ LOCALE_ID updates: Angular's DI token updates
- ✅ Document lang:
<html lang="">attribute updates (a11y) - ✅ 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
});