Skip to content

Runtime And Frontend

The i18n runtime is split by execution context.

ContextRuntime ShapePrimary Entry Point
ClientSingleton i18next instance attached to React.@wavemap/i18n/core/client
Server utilityIsolated i18next instance per request or render operation.@wavemap/i18n/core/server
Backend errorServer-only resolver over server--errors JSON resources.@wavemap/i18n/server
Email copyServer-only adapters over dedicated email--* JSON resources.@wavemap/i18n/email

This split prevents request locale bleed on the server and avoids pulling server-only code into client bundles.

Use explicit subpath exports:

import { I18nProvider, useAppTranslation } from "@wavemap/i18n/core/client"
import { getI18nClientEnv } from "@wavemap/i18n/env/client"
import { getServerT } from "@wavemap/i18n/core/server"
import { getI18nServerEnv } from "@wavemap/i18n/env/server"
import { resolveServerErrorMessage } from "@wavemap/i18n/server"
import { resolvePasswordResetEmailCopy } from "@wavemap/i18n/email"

getI18nServerEnv() has a hard browser guard. New helpers should keep this boundary clear: client exports should not import server-only dependencies, and server exports should not rely on browser globals. Maintaining this curated separation of i18n resources will avoid errors where the front end tries to import code which is only able to run on the server runtime.

Runtime resource requests use this shape:

/i18n/{version}/{locale}/{namespace}.json

createI18nResourceLoader() decides whether that path is loaded from same-origin assets or from a configured CDN base URL. Current client and server initialization enable same-origin fallback if CDN loading fails.

In practice:

  • Development prefers same-origin API-backed resources.
  • Production-like runtimes can use same-origin static assets or a CDN.
  • If a CDN base URL is configured and fallback is enabled, a CDN failure retries the same path on the deployed frontend.

I18nProvider creates the client singleton, initializes shared i18n configuration, preloads the initial namespaces, and changes language when the route locale changes.

The front end currently passes INITIAL_LOAD_NAMESPACES from the merged package layer. Components then use useAppTranslation() with the namespaces they need.

const { t } = useAppTranslation(["page--registration", "common--form-validation"])
const tPage = (key: string, values?: Record<string, unknown>) => t(key, { ...values, ns: "page--registration" })
const tValidation = (key: string, values?: Record<string, unknown>) =>
t(key, { ...values, ns: "common--form-validation" })

Both prefixed keys and namespace options appear in the codebase:

t("page--login:form.email.label")
t("form.email.label", { ns: "page--login" })

Namespace helper wrappers are useful when a component combines page copy with reusable validation or UI copy.

getServerT(locale, namespaces) creates an isolated i18n instance through createIsolatedI18nInstance().

That shape is intended for:

  • Server handlers that need localized user-facing messages.
  • SSR utilities that need request-scoped locale state.
  • Automated email composition.

Server-side localization should keep namespace choices explicit and avoid using page namespaces for backend-only copy when a dedicated email or server namespace would be clearer.

Backend error localization uses @wavemap/i18n/server, not the client singleton. Error classes may carry messageKey and messageInterpolationValues alongside the existing fallback message. The backend error middleware builds the error envelope, reads the request Accept-Language header, resolves the localized message from server--errors, and writes the localized display message back into error.message.

The envelope still keeps messageKey and messageInterpolationValues so clients can distinguish stable error identity from display copy. Locale resolution prefers the best supported Accept-Language entry, supports base-locale matching such as fr-CA to fr, falls back to the default locale, and finally uses the original route message if the translation key is missing.

Automated emails use @wavemap/i18n/email copy adapters. Those adapters import the dedicated email resources, resolve a declared locale, format ICU values, and return display-ready copy objects for the backend to pass into React Email templates.

Locale source depends on the email:

  • Password reset and resend verification use the recipient user’s saved language when available.
  • Admin invitation uses the authenticated sender’s saved language because the invitee does not have Wavemap settings yet.
  • Registration-time emails fall back to the default locale when no durable user locale exists.

For future non-user emails, choose the durable locale source before adding the namespace or adapter. Prefer recipient settings when they exist, sender settings when the sender is the only known Wavemap user, request/route locale only when that signal is intentionally part of the flow, and the default locale when no durable user preference exists.

React Email templates should remain renderers. They receive resolved copy and should not load translation resources or choose fallback locales.

Wavemap is URL-locale-first:

/{locale}/...

The flow is:

  1. apps/wavemap-front-end/src/middleware/localeRedirect.ts redirects unlocalized app routes to the default locale.
  2. src/app/[locale]/layout.tsx owns <html lang={locale}> and the locale-aware provider stack.
  3. LocaleContextProvider establishes active/default/available locale state.
  4. I18nProvider initializes translation resources.
  5. Components call useAppTranslation() with namespace-scoped keys.

Static assets, API routes, and Next internals are skipped by the locale redirect middleware.

LocaleSwitcher reads labels from common--locales, writes the wm-locale cookie, replaces the first URL segment, and preserves query parameters while navigating.

The route remains the source of truth for the active locale. The cookie is a preference used when a request needs a remembered locale.