Skip to content

Contributor Workflows

Treat i18n changes as cross-cutting changes. A small locale or namespace edit can touch runtime config, generated assets, tests, CI, and domain-code display contracts.

Identify which surface owns the change:

  • Ordinary UI copy: update locale sheets under packages/i18n/locales.
  • New reusable namespace: add constants in the correct common or wavemap layer and include them through merge.
  • New code-driven display value: update source constants, translations, and the domain contract manifest.
  • New backend error copy: use server--errors and preserve route fallback message text.
  • New automated email copy: prefer a dedicated email--* namespace over reusing a page namespace.
  1. Add the locale code to the declared locale constants and any required environment allow-list.
  2. Add a full namespace JSON set under packages/i18n/locales/<new-locale>/.
  3. Mirror English (en) key paths, value shapes, and root _meta shape.
  4. Preserve the same ICU interpolation, plural, and select contracts as English.
  5. Keep ordinary key segments lower-kebab.
  6. Run pnpm format:i18n.
  7. Run pnpm verify:i18n.
  8. Smoke the locale redirect, locale switcher, and representative UI flows with interpolation and plurals.
  1. Add a namespace constant in the owning layer:
    • common for reusable cross-project concepts.
    • wavemap for product-specific copy.
  2. Include the namespace in the merged namespace set if it should be runtime-visible.
  3. Confirm the namespace string exactly matches the locale sheet filename stem.
  4. Add <namespace>.json for every supported locale.
  5. Start each sheet with _meta.
  6. Follow the canonical sheet ordering rules.
  7. Add the namespace to INITIAL_LOAD_NAMESPACES only when it is needed at startup.
  8. Run pnpm format:i18n.
  9. Run pnpm verify:i18n.

Use this flow when a source code value needs a localized label.

  1. Define or update the canonical source constant.
  2. Add the value to the exported AVAILABLE_* array.
  3. Ensure backend lookup seeds map over the canonical array instead of hardcoding row values.
  4. Add or update the seed-source contract entry when the code set has a backend base seed.
  5. Choose a predictable translation key prefix, such as statuses.<code> or types.<code>.
  6. Add or update the domain translation contract entry.
  7. Add the required keys to every locale.
  8. Run pnpm verify:i18n.
  9. Add runtime mapping tests where UI code derives translation keys dynamically.

Use this flow when an API error response should include localized display copy.

  1. Confirm the message is user-facing backend copy, not a log line, operator diagnostic, or raw exception detail.
  2. Choose a server--errors key with a domain-first root, such as auth.*, artist.*, or query.*.
  3. Keep the existing fallback message useful and add messageKey plus messageInterpolationValues to the thrown HTTP error.
  4. Keep messageInterpolationValues entries primitive: string, number, boolean, or null.
  5. Add the key to every locale’s server--errors.json.
  6. Use ICU for dynamic values and update _meta.interpolation for every affected locale.
  7. Add or update route, middleware, or resolver tests when the key covers meaningful behavior.
  8. Run pnpm format:i18n.
  9. Run pnpm verify:i18n.

Backend error middleware resolves the localized display message from Accept-Language. Clients should treat messageKey as stable identity and message as display fallback or already-localized copy.

Use this flow when backend email composition needs localized subject, preview, body, action, or fallback-link text.

  1. Choose an existing dedicated email--* namespace, or add a new one for the email family.
  2. Keep related subject, preview, headings, body copy, action labels, and fallback-link text together.
  3. Add a backend copy adapter that resolves the locale, formats ICU values, and returns display-ready copy.
  4. Pass resolved copy into the React Email template; keep the template independent from translation loading.
  5. Choose a durable locale source:
    • Recipient saved language when the recipient user already exists.
    • Authenticated sender saved language for invitations to people without Wavemap settings.
    • Registration-time locale when creating a user before saved settings are available.
    • Default locale when no user preference exists yet.
  6. Add or update adapter tests for supported locales, ICU values, and default-locale fallback.
  7. Run pnpm format:i18n.
  8. Run pnpm verify:i18n.

Use this flow when a reusable component owns internal visible labels, ARIA labels, status text, or dynamic formatter copy across several subcomponents.

  1. Keep the component independent from Wavemap translation keys and namespaces.
  2. Translate at the call site or in a small page/domain adapter, then pass resolved strings and formatter functions.
  3. Use a common--* namespace for product-neutral component chrome.
  4. Use the page or domain namespace for workflow-specific copy, fallback errors, and context-sensitive labels.
  5. Add a grouped labels prop whose groups mirror the component’s internal label supply chain.
  6. Include formatter functions for dynamic strings, such as page numbers, item counts, toast bodies, or grouped overflow labels.
  7. Resolve defaults and any legacy scalar label props in one adapter near the top-level component.
  8. Pass complete label groups down to internal subcomponents instead of making each call site know the subcomponent tree.
  9. Add tests for defaults, legacy scalar props if they remain supported, and one fully customized label map.

Prefer small helper adapters when a component needs a large or nested label map. The adapter should sit near the page or domain wrapper that owns the translation context, resolve t() once, and return the complete display-ready label shape expected by the reusable component. This keeps namespace details out of generic UI code without forcing every call site to know the component’s full internal subcomponent tree.

Components like <Pagination />, <Carousel />, <FileUploadSurface />, <SortAndFilterPanel />, <TypeaheadSearch />, <CompactTypeaheadSearch />, <Breadcrumbs />, date/time/numeric inputs, and the table filtering stack can serve as good examples of this approach.

Example label-map shapes:

SurfacePattern
PaginationResolve one top-level labels map, then slice it into primary controls, page counter, page input, and items-per-page labels.
CarouselPass formatter functions for live regions, slide labels, dot labels, previous/next controls, and close controls.
MediaCarouselLayer media dialog copy and media-specific defaults on top of the generic carousel label contract.
FileUploadSurfaceResolve one label map for surface controls, preview items, sheet chrome, gallery/table views, row menus, alt-text popovers, and toast copy.
Table filteringLet Table resolve table-filter labels once, then pass popover, clause-row, and dynamic argument-input labels through the stack.
SortAndFilterPanelResolve sidebar labels once, then pass header/footer, active sort, active filter group, and nested filter-clause labels downward.
Typeahead/combo controlsKeep English defaults for reuse while page/domain wrappers supply grouped labels for artist/event search and filter argument inputs.
BreadcrumbsLet the component own fallback navigation and overflow chrome labels while breadcrumb item names remain caller-provided content.
Date/time/numeric inputsKeep field names caller-owned, while internal control groups, picker triggers, and picker calendar labels come from small label maps.

Reusable components can keep English defaults for non-Wavemap reuse, but product surfaces should supply localized label maps. The component should receive display-ready text, not t, namespace names, or Wavemap translation keys.

When changing existing copy:

  1. Update all affected locales, or intentionally leave the non-English copy as a coordinated follow-up.
  2. Update _meta.lastModified.
  3. Update _meta.interpolation if ICU tokens changed.
  4. Run pnpm format:i18n.
  5. Run pnpm verify:i18n.

Before merging i18n-related changes, check:

  • Namespace constants match locale sheet filename stems.
  • Every locale has the same namespace set.
  • Key paths and value shapes match English.
  • ICU runtime variables and branch keys match English.
  • _meta.interpolation matches parsed message tokens.
  • Translation sheet ordering is canonical.
  • Dynamic domain-code values have translation keys in every locale.
  • Backend base seed files still map over canonical source arrays.
  • Backend error messageKey values have matching server--errors entries when the error is user-facing.
  • Email templates receive resolved copy instead of importing translation resources directly.
  • Reusable components receive grouped label maps when they own internal visible copy.
  • Generated static assets still validate if the change affects deployment or smoke runtime behavior.