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.

INITIAL_LOAD_NAMESPACES stays intentionally narrow. When a component requests an ordinary page or domain namespace that has not loaded yet, useAppTranslation() suspends that consumer on the namespace request. The package-owned I18nProvider supplies the matching Suspense boundary and loading fallback, so components do not call t() against a pending namespace or emit strict missing-key warnings during the loading window.

import { tFunctionCopy } from "@wavemap/i18n"
const { t } = useAppTranslation(["page--registration", "common--form-validation"])
const tPage = tFunctionCopy(t, "page--registration")
const tValidation = tFunctionCopy(t, "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" })

Use the package-owned tFunctionCopy(t, namespace) when a component combines page copy with reusable validation or UI copy. It preserves the supplied translator’s locale binding and forwards interpolation values without mutating them. The helper is available from @wavemap/i18n and @wavemap/i18n/core in both server and client code.

Label builders and component callbacks use TScopedTranslation from the same package instead of defining a local translation signature or importing one from another feature. Keep the richer native i18next TFunction at runtime boundaries such as getServerT() and useAppTranslation(); the shared callback describes string-valued copy after namespace scoping. Translation semantics belong to @wavemap/i18n, which already owns the runtime and its types.

Pseudolocalization is a diagnostic runtime mode, not a separate locale. When enabled, @wavemap/i18n registers an i18next post-processor named wavemapPseudolocalization. Translated string results are transformed after normal lookup, interpolation, and ICU formatting.

The default character set is accented, which keeps text readable while making translated copy visually obvious:

Log In -> [Ļōĝ Īń]

Additional built-in character sets are available for stress and entertainment:

  • hieroglyphics
  • wingdings

The package also exposes a custom replacement-map interface for local experiments and future test cases:

import { pseudoLocalize } from "@wavemap/i18n/core"
pseudoLocalize("Login", {
replacementMap: {
L: "1",
o: "0",
g: "9",
i: "!",
n: "^",
},
})

Runtime switches:

Environment ValueScopeMeaning
PSEUDOLOCALIZATION_ACTIVE=trueServer/runtime helperEnables server-side isolated i18next processing.
NEXT_PUBLIC_PSEUDOLOCALIZATION_ACTIVE=trueBrowser bundleEnables client-side i18next processing.
PSEUDOLOCALIZATION_CHARACTER_SET=accentedServer/runtime helperSelects the server-side replacement set.
NEXT_PUBLIC_PSEUDOLOCALIZATION_CHARACTER_SETBrowser bundleSelects the client-side replacement set.

Pseudolocalization is most useful when the app is otherwise running normally. A translated string should show pseudolocalized characters. A naked JSX string remains ordinary text and can be detected by a browser sweep.

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.