Internationalization
Stratal includes a built-in internationalization (i18n) module that provides locale detection, type-safe translation keys, parameter interpolation, and automatic translation of validation errors. The module is loaded automatically as part of the framework core, so system messages like error responses are translated out of the box.
Default behavior
Section titled “Default behavior”The I18nModule is auto-loaded by Stratal. Without any configuration, it:
- Detects the request locale from the
X-Localeheader - Falls back to
enwhen the header is missing or unsupported - Translates all built-in system messages (error responses, validation errors) in English
- Makes
I18nServiceavailable for injection throughout your application
You do not need to import the module for basic i18n support to work.
Configuration
Section titled “Configuration”To add your own translations or support additional locales, call I18nModule.forRoot() in your root module:
import { Module } from 'stratal/module'import { I18nModule } from 'stratal/i18n'import type { I18nModuleOptions } from 'stratal/i18n'import * as en from './messages/en'import * as fr from './messages/fr'
const i18nConfig: I18nModuleOptions = { defaultLocale: 'en', fallbackLocale: 'en', locales: ['en', 'fr'], messages: { en, fr },}
@Module({ imports: [I18nModule.forRoot(i18nConfig)],})export class AppModule {}I18nModuleOptions
Section titled “I18nModuleOptions”| Option | Type | Default | Description |
|---|---|---|---|
defaultLocale | string | 'en' | Locale used when no X-Locale header is present |
fallbackLocale | string | 'en' | Locale used when a translation key is missing in the requested locale |
locales | string[] | ['en'] | List of supported locales |
messages | Record<string, Record<string, unknown>> | {} | Application-specific messages, keyed by locale |
Defining messages
Section titled “Defining messages”Messages are nested objects. Each key path becomes a dot-notation translation key:
export const common = { welcome: 'Welcome', greeting: 'Hello, {name}!',}
export const users = { created: 'User {email} has been created.', deleted: 'User account deleted successfully.',}export const common = { welcome: 'Bienvenue', greeting: 'Bonjour, {name} !',}
export const users = { created: "L'utilisateur {email} a été créé.", deleted: 'Compte utilisateur supprimé avec succès.',}Your application messages are deep-merged with the built-in system messages. If you define a key that already exists in the system messages, your value takes precedence.
Using translations
Section titled “Using translations”Inject I18nService and call t() with a message key:
import { Transient, inject } from 'stratal/di'import { I18N_TOKENS, type II18nService } from 'stratal/i18n'
@Transient()export class GreetingService { constructor( @inject(I18N_TOKENS.I18nService) private readonly i18n: II18nService, ) {}
getWelcome() { return this.i18n.t('common.welcome') }
getGreeting(name: string) { return this.i18n.t('common.greeting', { name }) }}The t method signature:
t(key: MessageKeys, params?: MessageParams): stringkeyis a type-safe union of all system and application message keys (dot-notation paths).paramsis an optionalRecord<string, string | number>for interpolation.- Returns the translated string, or the key itself if no translation is found.
Parameter interpolation
Section titled “Parameter interpolation”Use {paramName} placeholders in your messages. Pass the values as the second argument to t():
// Message: 'Hello, {name}! You have {count} notifications.'i18n.t('common.notifications', { name: 'Alice', count: 5 })// Result: 'Hello, Alice! You have 5 notifications.'In error classes
Section titled “In error classes”Error classes use message keys that GlobalErrorHandler translates at response time:
import { ApplicationError, ERROR_CODES } from 'stratal/errors'
export class UserNotFoundError extends ApplicationError { constructor(userId: string) { super('errors.auth.userNotFound', ERROR_CODES.USERS.NOT_FOUND, { userId }) }}See the Error Handling guide for more details.
Locale detection
Section titled “Locale detection”Stratal detects the locale from the X-Locale request header. The detection flow is:
- Read the
X-Localeheader from the incoming request. - Convert it to lowercase.
- Check if the locale is in the configured
localesarray. - If supported, use it for the request. If not, fall back to
defaultLocale.
The locale is set on the RouterContext and is available for the entire request lifecycle, including validation error messages and queue message metadata.
GET /api/usersX-Locale: frValidation error translation
Section titled “Validation error translation”When the i18n module is active, Zod validation errors are automatically translated based on the request locale. This works through a global Zod error map that intercepts every validation error and translates it using the i18n service.
For example, a request with X-Locale: fr that fails body validation will return French error messages:
{ "code": 1003, "message": "Erreur de validation", "metadata": { "issues": [ { "path": "email", "message": "Adresse e-mail invalide", "code": "invalid_format" } ] }}The translation covers all standard Zod error codes including required, invalid_type, too_small, too_big, and format validations like email, url, uuid, and datetime.
Custom validation messages
Section titled “Custom validation messages”Use the withI18n helper to attach translatable messages to custom Zod validations:
import { z, withI18n } from 'stratal/validation'
const schema = z.object({ email: z.string().email(withI18n('validation.email')), name: z.string().min(2, withI18n('validation.minLength', { min: 2 })),})The withI18n function creates a deferred translation that resolves at validation time using the request’s locale context. You can also pass interpolation parameters:
z.string().min(5, withI18n('validation.minLength', { min: 5 }))Built-in system messages
Section titled “Built-in system messages”Stratal ships with English translations for all framework messages. These cover:
- Error responses:
errors.internalError,errors.notFound,errors.unauthorized,errors.forbidden, and many more - Validation messages:
validation.required,validation.email,validation.minLength,validation.maxLength, and others - Zod error codes: Comprehensive translations for every Zod v4 issue type (
zodI18n.errors.required,zodI18n.errors.invalid_type,zodI18n.errors.too_small.string.inclusive, etc.) - Domain-specific errors: Queue, cron, storage, cache, email, and auth error messages
To override any system message, define the same key in your application messages. Your value will take precedence over the built-in one.
Type-safe message keys
Section titled “Type-safe message keys”The module uses TypeScript module augmentation to provide autocomplete for message keys. The AppMessages interface in stratal is empty by default — augment it with the shape of your app’s messages for a single locale:
import type * as appEn from './messages/en'
declare module 'stratal' { interface AppMessages extends typeof appEn {}}This gives you autocomplete and compile-time checks for both system and app message keys:
this.i18n.t('errors.routeNotFound', { method: 'GET', path: '/api' }) // system keythis.i18n.t('users.welcome', { name: 'Alice' }) // app keyDI tokens
Section titled “DI tokens”All tokens are exported from stratal/i18n via I18N_TOKENS:
| Token | Service | Scope |
|---|---|---|
I18N_TOKENS.MessageLoader | MessageLoaderService | Singleton |
I18N_TOKENS.I18nService | I18nService | Request |
I18N_TOKENS.Options | I18nModuleOptions | Value |
Next steps
Section titled “Next steps”- Validation to learn how validation schemas integrate with i18n error messages.
- Error Handling for details on how translated errors are returned to clients.
- Queues to see how locale context is preserved across queue messages.