Skip to content

Locale and Translation ​

Moost does not ship an i18n runtime. It gives you three small pieces and lets your app bring its own translations (vue-i18n, i18next, FormatJS, or a plain map):

  • the locale of each event — useLocale() / @Locale(), chosen by a policy your app supplies;
  • codes on errors — framework errors carry a stable code and params next to the English message (for example the workflow trigger's WF_EXPIRED), so a translator can key on them;
  • one translate hook — setTranslator(), which your code and the packages that opt in call through useTranslate(). Moost's own packages do not translate anything yet: today the hook is for your code (error interceptors, transformErrorBody, your serialisers) and for libraries built on moost that adopt it.

The locale of an event ​

useLocale() (from moost) works for every event kind — HTTP, CLI, workflows, WebSocket:

ts
import { useLocale } from 'moost'

useLocale().get()                 // 'de' | undefined — this event's, or its parent event's
useLocale().set('de')             // for this event and the events it starts
useLocale().set(() => pickLocale()) // lazy: runs on the first get(), once

For HTTP, set your policy once with the adapter's resolveLocale option. It runs lazily, on the first read in a request:

ts
import { MoostHttp, useAcceptLanguage } from '@moostjs/event-http'
import { useCookies, useUrlParams } from '@wooksjs/event-http'

const SUPPORTED = ['en', 'de', 'fr']

const http = new MoostHttp({
  resolveLocale: () => {
    const fromQuery = useUrlParams().params().get('lang')
    if (fromQuery && SUPPORTED.includes(fromQuery)) return fromQuery
    const fromCookie = useCookies().getCookie('lang')
    if (fromCookie && SUPPORTED.includes(fromCookie)) return fromCookie
    return useAcceptLanguage().negotiate(SUPPORTED, 'en') // adds Vary: Accept-Language
  },
})

The precedence (query, cookie, user profile, Accept-Language) is your policy, not the framework's. A workflow started from the request, and an in-process fetch() made while serving it, read the request's locale.

In a handler, inject it with @Locale():

ts
import { Locale } from 'moost'

@Get('greeting')
greeting(@Locale() locale: string | undefined) {
  return locale === 'de' ? 'Hallo' : 'Hello'
}

Registering a translator ​

ts
import { setTranslator } from 'moost'

const messages: Record<string, Record<string, string>> = {
  de: {
    'Name taken': 'Name vergeben',
    WF_EXPIRED: 'Dieser Link ist abgelaufen.',
  },
}

setTranslator((text, params, { locale }) => messages[locale ?? '']?.[text])

The translator receives the text (a source text used as the message id, or a code), its params, and { locale, event }. Return the translation, or undefined to keep the text as it is — untranslated text degrades to the source text, never to a raw key. The registration is process-wide, and it takes effect only where code calls useTranslate().

Translate in your own code with useTranslate():

ts
import { useTranslate } from 'moost'
import { HttpError } from '@moostjs/event-http'

const t = useTranslate()
throw new HttpError(409, { message: t('Name taken'), code: 'NAME_TAKEN', params: { name } })

useTranslate() returns t(text, params?) bound to the current event's locale. Without a registered translator t returns text unchanged (moost does not interpolate params; your translator does). getTranslator() tells whether one is registered — skip per-locale work when it is not.

Translating error responses ​

Errors keep their English message and carry a code. Translate them in one place: an error interceptor, or the HTTP adapter's transformErrorBody option, which also sees the router's 404:

ts
import { useTranslate } from 'moost'
import { MoostHttp } from '@moostjs/event-http'

const http = new MoostHttp({
  transformErrorBody: (body, _error, ctx) => {
    const t = useTranslate(ctx)
    const message = t(body.code ?? body.message, body.params)
    // a code with no translation keeps the English message
    return { ...body, message: message === body.code ? body.message : message }
  },
})

See also ​

Released under the MIT License.