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>.jsonExamples:
packages/i18n/locales/en/page--login.jsonpackages/i18n/locales/fr/common--pagination.jsonpackages/i18n/locales/en/server--errors.jsonThe 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.
Key Style
Section titled “Key Style”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, andPageCounter.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 Error Resources
Section titled “Server Error Resources”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
messageuseful 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, orquery.limit.range. - Prefer domain-first root groups instead of page-owned error keys or generic
server.errors.*prefixes. - Put dynamic interpolation values in
messageInterpolationValuesand keep them primitive JSON values: string, number, boolean, ornull. - Render dynamic messages with ICU and mirror token names in
_meta.interpolation. - Use the route fallback
messagefor 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.
Email Resources
Section titled “Email Resources”Automated email copy belongs in dedicated email--* namespaces. Current examples include:
email--password-resetemail--account-verificationemail--admin-registration-invitationUse 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 Usage
Section titled “ICU Usage”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.
Metadata
Section titled “Metadata”Sheets use root _meta for translation maintenance data.
{ "_meta": { "lastModified": "2026-05-13", "interpolation": { "messages.confirm-delete.body": ["entityName"] } }}Rules:
- Keep
_metaat the root. - Keep
_metafirst in each sheet. - Update
_meta.lastModifiedwhen translation sheet contents change. - Keep
_meta.interpolationkeyed by translation key path. - Keep interpolation token arrays aligned with the ICU tokens parsed from the message body.
Sheet Ordering
Section titled “Sheet Ordering”Translation sheets should be ordered predictably so review diffs focus on copy and contract changes.
The canonical ordering is:
_metafirst.- Known semantic root groups in a stable project order.
- Unknown or equally ranked sibling keys alphabetically by dotted path.
- Nested structured objects sorted by their object-specific rules.
Common root group order includes:
titlepage.*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:
pnpm format:i18npnpm verify:i18nformat:i18n rewrites source sheets into canonical order. verify:i18n checks the same ordering in CI.