@samveys/i18n
From packages/i18n/README.md in the framework repository.
Formatting with Intl for the request's locale, and translations written in MessageFormat 2. The only runtime dependency is messageformat 4.1.0 (Apache-2.0, no dependencies of its own).
Set up
import { i18n } from '@samveys/i18n'
export const plugin = i18n({
defaultLocale: 'en',
fallbackLocale: 'en', // used when the requested locale lacks a key
langDir: 'lang', // lang/en.json, lang/id.json, …
strict: true, // a missing key throws SVS_E111 instead of returning the key
})The plugin binds 'i18n' and adds two fields to every request: ctx.locale (negotiated from Accept-Language) and ctx.i18n (a translator for it).
import type { HttpContext } from '@samveys/core'
import '@samveys/i18n'
export function invoice(ctx: HttpContext) {
return {
locale: ctx.locale,
total: ctx.i18n.money(1250001, 'IDR'), // "Rp 1.250.001" for id-ID, "IDR 1,250,001.00" for en-US
when: ctx.i18n.date(new Date(), 'long'),
tags: ctx.i18n.list(['a', 'b', 'c']), // "a, b, dan c" for id, "a, b, and c" for en
ago: ctx.i18n.relative(Date.now() - 3 * 86_400_000), // "3 hari yang lalu"
}
}Formatting comes from Intl, so it needs no data files: money(amount, currency), number(value, options?), date(value, style) and dateTime(value, style) (short, medium, long, full), list(items, type?), relative(value).
Translations
lang/<locale>.json holds one catalog per locale. Nested keys are dotted (cart.items). Messages are MessageFormat 2.
{
"greeting": "Halo {$name}!",
"cart": {
"items": ".input {$count :number}\n.match $count\none {{{$count} item}}\n* {{{$count} items}}"
}
}Plural categories come from the language's CLDR rules, so the same message works everywhere: English picks one or other, Indonesian has only other, Russian also has few and many.
import { createI18nManager } from '@samveys/i18n'
const plural = '.input {$count :number}\n.match $count\none {{{$count} item}}\n* {{{$count} items}}'
const manager = createI18nManager({ defaultLocale: 'en', messages: { en: { items: plural }, id: { items: plural } } })
export const english = manager.for('en').t('items', { count: 1 }) // "1 item"
export const indonesian = manager.for('id').t('items', { count: 1 }) // "1 items": id has no "one"- Fallback: a key missing from the locale is looked up in
fallbackLocale. - Missing everywhere: the key itself is returned, or
SVS_E111instrictmode. - Messages in code:
messagestakes catalogs directly and wins over files.
Locale negotiation
manager.negotiate(acceptLanguage) returns the best supported locale: tags are tried in quality order; for each an exact match, then the bare language (id-ID → id), then any locale of the same language (pt-PT → pt-BR); otherwise the default.