| name | i18n |
| description | i18n patterns for an AdonisJS + Inertia app. Keys namespaced by module, `useTranslation()` on the frontend and `ctx.i18n.t(...)` on the backend, locale persisted per user with a cookie fallback. Every new key needs an entry in every supported locale JSON. Use when adding UI strings, wiring a new module's translations, or debugging locale detection. Trigger on: "add translation", "i18n", "locale", "translate", "add key". |
| license | MIT |
| allowed-tools | Read, Edit, Write, Bash, Grep, Glob |
i18n
Locales are declared in config/i18n.ts (supportedLocales, defaultLocale, loaders). Translation JSON lives per module at app/<mod>/resources/lang/<locale>/<mod>.json. Keys are namespaced by module (common.layout.navMain.dashboard), so the file at app/common/resources/lang/en/common.json doesn't repeat common. inside — the loader adds it. Backend uses ctx.i18n.t('key'); frontend uses useTranslation(). Locale detection runs as an HTTP middleware that prefers an X-User-Language header → a user-locale cookie → the browser's Accept-Language. Authenticated users have User.locale as the authoritative source: the switch endpoint writes both cookie and DB; every login flow syncs the cookie from user.locale if set.
Rules
Repo refs
- Locale config + per-module loaders:
config/i18n.ts. Keys: app/users/resources/lang/{en,fr,pt}/users.json.
- Frontend hook:
app/common/ui/hooks/use_translation.ts. Detection middleware: app/core/middleware/detect_user_locale_middleware.ts.
Doc refs
Workflow
Add a translation key
- Pick the module. If the string is shared UI (nav, buttons, layout), put it in
common.
- Open the JSON file for every supported locale:
app/<mod>/resources/lang/<locale>/<mod>.json.
- Add the same key path in every file with the localized value.
- Frontend:
t('<mod>.<path>') via useTranslation().
- Backend:
ctx.i18n.t('<mod>.<path>').
- Verify parity: grep the key across
app/<mod>/resources/lang/*/ — it must appear in all three locales. Present in only one → it falls back silently and ships a mixed-language UI.
Add a new module's translations
- Create
app/<mod>/resources/lang/<locale>/<mod>.json for every supported locale.
- Add an
fs loader entry in config/i18n.ts:
loaders.fs({ location: app.makePath('app/<mod>/resources/lang') }),
- The frontend glob picks the files up automatically.
Interpolation
{ "welcome": "Welcome, {name}!" }
t('common.welcome', { name: user.fullName })
Plural / selector:
{ "items": "{count, plural, =0 {No items} one {# item} other {# items}}" }
Localize a validator message
Add to app/<mod>/resources/lang/<locale>/validator.json:
{ "required": "This field is required" }
VineJS looks these up automatically because the messages provider is bound in the locale-detection middleware.
Set a user's default locale programmatically
Use during invite / sign-up when the target's language is known:
await User.create({ email, locale: 'pt' })
The next login flow writes the cookie for them.
Anti-patterns
- ❌ Adding a key in one locale but not the others — runtime falls back silently to the default and users see mixed languages.
- ❌ Hardcoding strings in components (
<span>Save</span> instead of t('common.save')).
- ❌ Reading
req.cookies['user-locale'] directly instead of ctx.i18n.locale — bypasses the detection priority (header → cookie → Accept-Language).
- ❌ Persisting locale to the cookie without also updating
user.locale when the user is authenticated — the switch endpoint handles both; don't reimplement.
- ❌ Deep key paths (
common.a.b.c.d.e.f) — collapse the JSON tree, keep depth ≤ 3.
- ❌ Repeating the module prefix inside the JSON file (
common.json with a common root) — the loader adds it.
Related skills
[[inertia]] · [[layout-shells]] · [[mail]] · [[crud]] · [[module-scaffolding]]