Skip to content

Translation Resources

Translation resources live under packages/i18n/locales and are organized as one folder per locale with one JSON file per namespace.

packages/i18n/locales/<locale>/<namespace>.json

Examples:

packages/i18n/locales/en/page--login.json
packages/i18n/locales/fr/common--pagination.json
packages/i18n/locales/en/server--errors.json

The namespace must match the filename without .json. Registered namespace constants, translation filenames, generated asset paths, and runtime resource URLs all depend on that identity.

Backend-rendered API error copy uses the server--errors namespace. Error envelopes keep error.message as the display fallback and may add error.messageKey plus error.messageInterpolationValues for stable localization identity.

Ordinary copy files use flat dotted keys rather than nested objects.

{
"form.email.label": "Email",
"messages.rate-limit.title": "Too many attempts",
"errors.registration-unsuccessful.body": "Registration was unsuccessful."
}

Use lower-kebab key segments for translation path pieces:

  • Good: page-counter.input.label
  • Avoid: pageCounter.input.label, page_counter.input.label, and PageCounter.input.label

Prefer root groups that make the content easy to scan:

  • page.*
  • form.*
  • actions.*
  • messages.*
  • prompts.*
  • errors.*
  • aria-label.*

Runtime interpolation token names behave like code variables, so they should be descriptive camelCase:

{
"messages.confirm-delete.body": "This will permanently delete {entityName}."
}

Structured payload sheets can be explicit exceptions. For example, common--entities.json contains grammar metadata that is consumed by helpers rather than rendered as plain copy.

server--errors owns backend-rendered, user-facing error copy. Use it when the backend must produce display text for an API error response. Keep logs, developer diagnostics, operator messages, and raw third-party failure details out of this namespace unless they are intentionally rewritten for end users.

Server error envelopes preserve both display fallback text and stable localization identity:

{
"error": {
"message": "You must sign in to continue.",
"messageKey": "auth.unauthenticated",
"messageInterpolationValues": {}
}
}

Rules:

  • Keep message useful as fallback display copy for older clients, missing translations, and partially migrated routes.
  • Use namespace-local dotted keys such as auth.unauthenticated, artist.media.insert-failed, or query.limit.range.
  • Prefer domain-first root groups instead of page-owned error keys or generic server.errors.* prefixes.
  • Put dynamic interpolation values in messageInterpolationValues and keep them primitive JSON values: string, number, boolean, or null.
  • Render dynamic messages with ICU and mirror token names in _meta.interpolation.
  • Use the route fallback message for context, but avoid leaking raw exception text into localized user copy.

Backend error handling resolves keyed messages from Accept-Language, falls back to the default locale, and then falls back to the route’s original message when a key is missing.

Backend success message fields are still transport fallback copy unless a response is intentionally displayed directly. Normal success toasts and workflow confirmations remain page or component copy.

Automated email copy belongs in dedicated email--* namespaces. Current examples include:

email--password-reset
email--account-verification
email--admin-registration-invitation

Use backend copy adapters to resolve display-ready copy before rendering React Email templates. The template should receive strings, URLs, and structured copy objects; it should not import translation sheets or choose namespaces.

Rules:

  • Keep subject, preview text, headings, body copy, action labels, and fallback-link text in the same namespace family when they are translated together.
  • Prefer the recipient user’s saved language when the recipient already has Wavemap settings.
  • Use the authenticated sender’s saved language for admin invitation emails until the invitee has Wavemap settings.
  • Fall back to the default locale when there is no durable user locale signal.
  • Test copy adapters for supported locales and default-locale fallback; template rendering tests can focus on resolved copy and markup behavior.
  • Avoid asserting raw translation keys in React Email template tests. The adapter is the i18n boundary; the template test should prove the resolved copy is rendered correctly.

ICU formatting is enabled through i18next-icu. Use ICU when the whole phrase needs locale-aware interpolation, pluralization, or selection.

Simple interpolation:

{
"form.errors.required": "{field} is required."
}

Plural copy:

{
"page-counter.summary": "{count, plural, one {# page} other {# pages}}"
}

Keep sentence punctuation inside the translation value. Component code should not concatenate punctuation around translated text.

pnpm verify:i18n parses translation strings with intl-messageformat and treats English (en) as the reference contract. Non-English locales must preserve the same interpolation token names and the same plural/select runtime variables and branch keys.

Sheets use root _meta for translation maintenance data.

{
"_meta": {
"lastModified": "2026-05-13",
"interpolation": {
"messages.confirm-delete.body": ["entityName"]
}
}
}

Rules:

  • Keep _meta at the root.
  • Keep _meta first in each sheet.
  • Update _meta.lastModified when translation sheet contents change.
  • Keep _meta.interpolation keyed by translation key path.
  • Keep interpolation token arrays aligned with the ICU tokens parsed from the message body.

Translation sheets should be ordered predictably so review diffs focus on copy and contract changes.

The canonical ordering is:

  1. _meta first.
  2. Known semantic root groups in a stable project order.
  3. Unknown or equally ranked sibling keys alphabetically by dotted path.
  4. Nested structured objects sorted by their object-specific rules.

Common root group order includes:

title
page.*
card.*
section.*
sheet.*
panel.*
nav.*
tabs.*
labels.* / locale.* / theme.* / role.* / types.* / statuses.* / entities.*
query-controls.* / sort-and-filter-panel.* / saved-views.* / search.*
prompts.*
form.*
table.*
pagination.* / page-counter.* / page-input.* / items-per-page.* / primary-controls.*
actions.*
rules.*
import.*
messages.*
errors.*
aria-label.*

Use the package scripts instead of hand-sorting large sheets:

Terminal window
pnpm format:i18n
pnpm verify:i18n

format:i18n rewrites source sheets into canonical order. verify:i18n checks the same ordering in CI.