Intl
@voltage/intl wraps @formatjs/intl for NestJS. It provides a global Intl service for formatting messages, dates, numbers, and more. Locale is extracted from the URL path on every request and stored in async context, so the correct locale is always available without threading it through your call stack.
Installation
Section titled “Installation”yarn add @voltage/intl @voltage/event-manager @voltage/logger @voltage/zod zod@formatjs/intl is bundled and re-exported — you do not need to install it separately.
Basic usage
Section titled “Basic usage”Call forRoot() once in your root module:
import { IntlModule } from '@voltage/intl';
@Module({ imports: [ IntlModule.forRoot({ locales: ['en', 'de'], defaultLocale: 'en', negotiate: true, paths: ['/app/messages/compiled/[locale].json'], }), ],})export class AppModule {}The module is global — you do not need to import it in feature modules.
Inject Intl into any service or controller:
import { Intl } from '@voltage/intl';
@Injectable()export class PasscodeMailFactory { constructor(private readonly intl: Intl) {}
create(user: User, code: string): Mail { return { subject: this.intl.formatMessage(messages.subject), body: this.intl.formatMessage(messages.body, { name: user.firstName, code }), }; }}Defining messages
Section titled “Defining messages”Use defineMessage() for a single descriptor or defineMessages() for a group. Keep descriptors colocated with the code that uses them:
import { defineMessages } from '@voltage/intl';
export const authMessages = defineMessages({ title: { id: 'auth.title', defaultMessage: 'Sign in', description: 'Page title', }, body: { id: 'auth.body', defaultMessage: 'Hi {name}, your code is {code}.', },});defineMessages() accepts a type parameter, useful for keying messages off an enum:
export const salutationMessages = defineMessages<Salutation>({ [Salutation.Male]: { id: 'salutation.male', defaultMessage: 'Mr' }, [Salutation.Female]: { id: 'salutation.female', defaultMessage: 'Mrs' },});
this.intl.formatMessage(salutationMessages[user.salutation]);Translation files
Section titled “Translation files”Translations are JSON files with message IDs as keys. The paths option accepts glob patterns; use [locale] as a placeholder for the locale code:
/app/messages/compiled/[locale].jsonA compiled translation file:
{ "auth.title": "Anmelden", "auth.body": "Hallo {name}, dein Code ist {code}."}The defaultMessage in each descriptor is the fallback when no translation exists for the current locale. Translation files are loaded at application bootstrap.
Extracting and compiling messages
Section titled “Extracting and compiling messages”@formatjs/cli is included with the package. Use it to extract descriptors from source files and compile them into the JSON format above:
# Extract message descriptors from sourceyarn formatjs extract 'src/**/*.{ts,tsx}' --out-file messages/en.json
# Compile to the format expected by the loaderyarn formatjs compile messages/de.json --out-file messages/compiled/de.jsonFormatting
Section titled “Formatting”Intl exposes the full @formatjs formatting API:
// Messages with optional interpolation valuesthis.intl.formatMessage(messages.greeting, { name: 'Ada' });
// Dates and timesthis.intl.formatDate(order.createdAt, { dateStyle: 'medium' });this.intl.formatTime(order.createdAt, { timeStyle: 'short' });this.intl.formatDateTimeRange(start, end, { dateStyle: 'short' });
// Numbersthis.intl.formatNumber(price, { style: 'currency', currency: 'EUR' });
// Relative timethis.intl.formatRelativeTime(-2, 'day'); // "2 days ago"
// Plural categorythis.intl.formatPlural(count); // 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'JSX components
Section titled “JSX components”For JSX templates, the package ships formatting components under a separate import path:
import { FormattedMessage, FormattedDate, FormattedTime, FormattedNumber, FormattedRelativeTime, FormattedPlural,} from '@voltage/intl/components';These are injectable components — inject Intl via the service API in render methods instead when you need access to other injected dependencies. The components are convenient for simple inline formatting that doesn’t require service access:
<p> <FormattedDate value={order.createdAt} dateStyle="medium" /></p>Locale
Section titled “Locale”intl.locale returns the locale for the current request. The middleware sets it by extracting the leading path segment — /de/account → de. If no locale is in the path and negotiate is enabled, the Accept-Language header is used as a fallback. GET and HEAD requests without a recognised locale prefix are redirected to the appropriate locale URL automatically.
To format in a specific locale regardless of the current request, use intl.for(). The typical case is sending an email or notification in the recipient’s own locale, which may differ from the locale of the current request:
@Injectable()export class WelcomeMailFactory { constructor(private readonly intl: Intl) {}
create(user: User): Mail { const intl = this.intl.for(user.locale); return { subject: intl.formatMessage(messages.subject), body: intl.formatMessage(messages.body, { name: user.firstName }), }; }}intl.for() returns the same Intl API scoped to that locale. intl.locales returns the full list of configured locales, which is useful when you need to generate content for all locales at once — for example in a sitemap or static page generator.
Custom translations via IntlCreateEvent
Section titled “Custom translations via IntlCreateEvent”On bootstrap, Intl emits IntlCreateEvent once per locale with an empty messages object. The built-in handler populates it from the JSON files on disk. Any @OnEvent(IntlCreateEvent) handler in your application can also write into event.messages — useful for loading translations from a database, CMS, or other source:
import { OnEvent } from '@voltage/event-manager';import { IntlCreateEvent } from '@voltage/intl';
@Injectable()export class CmsTranslationLoader { constructor(private readonly cms: CmsService) {}
@OnEvent(IntlCreateEvent) protected async onIntlCreate(event: IntlCreateEvent) { const translations = await this.cms.getTranslations(event.locale); Object.assign(event.messages, translations); }}All handlers run sequentially and their contributions are merged. Handlers registered after the built-in file loader can override individual messages.
Reloading translations
Section titled “Reloading translations”Call intl.reload() to re-read all translation files without restarting the application:
await this.intl.reload();Configuration
Section titled “Configuration”interface IntlConfiguration { /** Supported locale codes (ISO 639-1, e.g. 'en', 'de') */ locales: string[]; /** Locale to use when none can be determined from the request */ defaultLocale: string; /** If true, fall back to the Accept-Language header when no locale is in the URL */ negotiate?: boolean; /** Glob patterns for translation files. Use [locale] as a placeholder */ paths: string[];}