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:
| Locale | Language |
|---|---|
en | English (default) |
es | Spanish |
fr | French |
de | German |
pt-BR | Portuguese (Brazil) |
How it works
Backend
- The browser sends an
Accept-Languageheader with each request. - A middleware extracts the preferred locale and validates it against the supported list (falling back to
enif no match). - The matching dictionary and locale are set on the request context, making them available in every route handler via
context.dictionaryandcontext.locale. dayjsis configured for the active locale so date formatting respects locale conventions.
Frontend
- The locale is stored in a Zustand auth store and persisted to
localStorage. - On app init, the store loads the matching dictionary and configures Zod error messages for the active locale.
- All components read translations from the store:
const { dictionary } = useAuthStore();
<Button>{dictionary.shared.save}</Button>
<span>{dictionary.auth.signIn.title}</span>- 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 prefix | Content |
|---|---|
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:
| Locale | Date | Datetime |
|---|---|---|
en | MMM DD, YYYY | MMM DD, YYYY hh:mma |
es | DD MMM YYYY | DD MMM YYYY HH:mm |
fr | DD MMM YYYY | DD MMM YYYY HH:mm |
de | DD.MM.YYYY | DD.MM.YYYY HH:mm |
pt-BR | DD/MM/YYYY | DD/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-BR→pt-BR) - Language-only match → strip region (
en-US→en) - Partial match → find closest (
pt-XX→pt-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.infoThis helps catch untranslated strings early during development.
Key files
Backend
| File | Purpose |
|---|---|
src/translation/locales.ts | Locale list and default locale |
src/translation/getDictionary.ts | Load dictionary by locale |
src/translation/dictionaryMiddleware.ts | Attach locale/dictionary to context |
src/translation/dictionaryFormat.ts | Placeholder string formatting |
src/translation/dictionaryEnumerator.ts | Enum value → display string |
src/translation/dictionaryValidateLocale.ts | Normalize and validate locale strings |
src/translation/dictionaryIntegrityCheck.ts | Warn on missing translation keys |
src/translation/applyDayjsTranslation.ts | Configure dayjs for active locale |
src/translation/{locale}/{locale}.ts | Dictionary for each locale |
src/translation/{locale}/zod{Locale}.ts | Zod error messages for each locale |