@samveys/i18n

Versi
1.0.35 Nov 2026
Pasang
pnpm add @samveys/i18n

Dari packages/i18n/README.md di repository framework.

Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.

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

ts
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).

ts
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.

json
{
  "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.

ts
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_E111 in strict mode.
  • Messages in code: messages takes 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.