Skip to content

Internacionalização ​

@basaltkit/i18n resolve o locale a partir do contexto do request (por utilizador, depois por tenant), traduz catálogos de mensagens tipados com interpolação e plurais CLDR, e formata números, moeda, datas, tempo relativo e listas através das APIs nativas Intl. Zero dependências.

Setup ​

Define um catálogo por locale com defineMessages (fixa os tipos de chave/valor para que t() complete automaticamente), depois regista-os com i18nPlugin:

ts
// src/i18n.ts
import { defineMessages } from '@basaltkit/i18n'

export const en = defineMessages({
  greeting: 'Hi {name}',
  notes: { one: '{count} note', other: '{count} notes' },
})
export const pt = defineMessages({
  greeting: 'Olá {name}',
  notes: { one: '{count} nota', other: '{count} notas' },
})
ts
// src/app.ts
import { createApp } from '@basaltkit/core'
import { I18n, I18N, i18nPlugin } from '@basaltkit/i18n'
import { en, pt } from './i18n.js'

const app = await createApp({
  plugins: [i18nPlugin({ locales: { en, pt }, defaultLocale: 'en' })],
}).boot()

// The I18N token erases the catalog type; cast to keep key autocompletion.
export const i18n = app.container.get(I18N) as I18n<typeof en>

Dica

Por defeito o locale do request é ctx().user.locale, depois ctx().tenant.locale. Para o resolver de outra forma, passa resolveLocale. Preenche tu mesmo uma chave locale no contexto do request (ex.: a partir de um header Accept-Language num enricher) — RequestContext tem uma index signature aberta — e depois lê-a aqui:

ts
import { ctx } from '@basaltkit/core'

i18nPlugin({
  locales: { en, pt },
  defaultLocale: 'en',
  resolveLocale: () => ctx().locale as string | undefined,
})

O locale resolvido é dado controlado pelo cliente e é validado antes de chegar ao Intl: uma tag BCP-47 inválida num registo de utilizador (en_US, !!) cai para o defaultLocale em vez de transformar cada página formatada num 500.

Traduzir ​

ts
i18n.in('pt').t('greeting', { name: 'Ada' }) // 'Olá Ada'
i18n.in('en').t('notes', { count: 3 })        // '3 notes' (plural via Intl.PluralRules)

Dentro de um request, i18n.t(...) usa o locale do contexto automaticamente — ctx().user.locale, depois ctx().tenant.locale (guarda um campo locale nesses registos, ou passa um resolveLocale personalizado). Um locale regional negoceia para baixo até um catálogo disponível (pt-BR → pt), depois recorre a defaultLocale, e por fim à própria chave.

Formatar ​

Cada tradutor transporta formatadores baseados em Intl no mesmo locale:

ts
const l = i18n.in('en')
l.n(1234.5)                // '1,234.5'
l.currency(9.9, 'USD')     // '$9.90'
l.date(new Date(), { dateStyle: 'long' })
l.relativeTime(-1, 'day')  // '1 day ago'
l.list(['a', 'b', 'c'])    // 'a, b, and c'

Num request ​

Dentro de um handler, chama i18n.t / os formatadores diretamente — usam o locale do contexto sem plumbing por chamada:

ts
import { z } from 'zod'
import { route } from '@basaltkit/fastify'
import { i18n } from './app.js' // the module-scoped instance resolved above

export const summary = route({
  method: 'GET',
  url: '/summary',
  query: z.object({ count: z.coerce.number() }),
  handler({ query }) {
    return {
      title: i18n.t('greeting', { name: 'Ada' }),   // context locale
      line: i18n.t('notes', { count: query.count }), // '1 note' / '3 notes'
      when: i18n.relativeTime(-1, 'day'),            // '1 day ago' / 'há 1 dia'
    }
  },
})

Combina-o com @basaltkit/mailer / @basaltkit/notifications para renderizar conteúdo de saída no locale do destinatário.

Publicado sob a licença MIT.