Internationalization

Your project ships with full internationalization (i18n) support covering UI text, validation errors, date/time formatting, and email templates. Five locales are included out of the box:

LocaleLanguage
enEnglish (default)
esSpanish
frFrench
deGerman
pt-BRPortuguese (Brazil)

How it works

Backend

  1. The browser sends an Accept-Language header with each request.
  2. A middleware extracts the preferred locale and validates it against the supported list (falling back to en if no match).
  3. The matching dictionary and locale are set on the request context, making them available in every route handler via context.dictionary and context.locale.
  4. dayjs is configured for the active locale so date formatting respects locale conventions.

Frontend

  1. The locale is stored in a Zustand auth store and persisted to localStorage.
  2. On app init, the store loads the matching dictionary and configures Zod error messages for the active locale.
  3. All components read translations from the store:
const { dictionary } = useAuthStore();
 
<Button>{dictionary.shared.save}</Button>
<span>{dictionary.auth.signIn.title}</span>
  1. Users can switch languages at any time via the language switcher in the UI, which updates the store, reloads the dictionary, and reconfigures validation messages.

Dictionary structure

Each locale has a dictionary file (e.g., en.ts, es.ts) exporting a deeply nested object. Keys are organized by feature area:

Key prefixContent
shared.*Common buttons, labels, dialogs
auth.*Sign-in, sign-up, password reset, etc.
member.*User/member management
organization.*Organization management
subscription.*Billing and subscription
{entityName}.*Per-entity labels, list titles, form fields
emails.*Email templates (verification, invitations)
notification.*Push notification titles and bodies
auditLog.*Activity log labels
chatbot.*AI assistant system prompts

String formatting

Dictionaries use numbered placeholders for dynamic values:

// Dictionary definition
'Processing {0} of {1}';
 
// Usage
dictionaryFormat(dictionary.shared.importer.importedMessage, 100, 200);
// → "Processing 100 of 200"

Enumerator labels

Enum fields are mapped to localized display strings via dictionaryEnumerator():

dictionaryEnumerator(dictionary.customer.enumerators.status, 'active');
// → "Activo" (Spanish), "Aktiv" (German), etc.

Date and time formatting

Each locale defines its own date/time format strings:

LocaleDateDatetime
enMMM DD, YYYYMMM DD, YYYY hh:mma
esDD MMM YYYYDD MMM YYYY HH:mm
frDD MMM YYYYDD MMM YYYY HH:mm
deDD.MM.YYYYDD.MM.YYYY HH:mm
pt-BRDD/MM/YYYYDD/MM/YYYY HH:mm

The backend applies the matching dayjs locale automatically, so relative dates (e.g., "2 hours ago") are also localized.

Localized validation errors

Each locale has a Zod error map file (e.g., zodEn.ts, zodEs.ts) that translates validation messages. When the user switches locale, the Zod error map is reconfigured so form validation errors appear in the selected language.

Locale detection and fallback

The dictionaryValidateLocale() function handles matching:

  • Exact match → use it (pt-BRpt-BR)
  • Language-only match → strip region (en-USen)
  • Partial match → find closest (pt-XXpt-BR)
  • No match → fall back to en

Dictionary integrity check

On server startup, dictionaryIntegrityCheck() compares all locale dictionaries and logs warnings for any missing keys:

⚠️  Dictionary Integrity Check: Missing translation keys detected
 
📝 Locale "es" is missing 2 key(s):
   - shared.newFeature
   - auth.passwordReset.info

This helps catch untranslated strings early during development.

Key files

Backend

FilePurpose
src/translation/locales.tsLocale list and default locale
src/translation/getDictionary.tsLoad dictionary by locale
src/translation/dictionaryMiddleware.tsAttach locale/dictionary to context
src/translation/dictionaryFormat.tsPlaceholder string formatting
src/translation/dictionaryEnumerator.tsEnum value → display string
src/translation/dictionaryValidateLocale.tsNormalize and validate locale strings
src/translation/dictionaryIntegrityCheck.tsWarn on missing translation keys
src/translation/applyDayjsTranslation.tsConfigure dayjs for active locale
src/translation/{locale}/{locale}.tsDictionary for each locale
src/translation/{locale}/zod{Locale}.tsZod error messages for each locale