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
codeandparamsnext to the Englishmessage(for example the workflow trigger'sWF_EXPIRED), so a translator can key on them; - one translate hook —
setTranslator(), which your code and the packages that opt in call throughuseTranslate(). 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:
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(), onceFor HTTP, set your policy once with the adapter's resolveLocale option. It runs lazily, on the first read in a request:
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():
import { Locale } from 'moost'
@Get('greeting')
greeting(@Locale() locale: string | undefined) {
return locale === 'de' ? 'Hallo' : 'Hello'
}Registering a translator
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():
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:
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
- Reading request data —
@Locale()withresolveLocale. - Interceptors — error interceptors and the after/error chain.