Skip to content

Assets And Delivery

Source translations live in packages/i18n/locales/**. Production-like runtimes do not read those source files directly. They consume generated assets under the frontend public directory.

apps/wavemap-front-end/public/i18n/{I18N_VERSION}/...

The generated snapshot includes per-locale namespace JSON files and a manifest.json.

The runtime loader always asks for resources through the production-shaped URL path:

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

That is true even in next dev. The development behavior comes from the frontend rewrite layer, not from a separate development-only resource path inside @wavemap/i18n.

During local development, next.config.js rewrites i18n asset requests to API routes before the request reaches static asset handling:

/i18n/:version/manifest.json -> /api/i18n/:version/manifest
/i18n/:version/:locale/:namespace*.json -> /api/i18n/:version/:locale/:namespace*

Those API routes are development-only routes. They reject non-development requests, compare the requested version against I18N_VERSION when it is present, and read from packages/i18n/locales on disk. Namespace requests return the matching source JSON file with Cache-Control: no-store; manifest requests discover locale directories, namespace filenames, the requested version, and the configured default locale.

This gives local development two useful properties:

  • The browser and React/i18next runtime exercise the same public URL shape that built runtimes use.
  • Ordinary translation JSON edits are available on refresh without regenerating apps/wavemap-front-end/public/i18n/**.

The development manifest validator compares expected locales and namespaces against discovered source resources so namespace or locale drift is visible early.

The loader selection still runs in development. I18nProvider reads NEXT_PUBLIC_I18N_VERSION and NEXT_PUBLIC_I18N_CDN_BASE_URL, then calls initI18nCommon(). Server-side isolated i18n helpers read I18N_VERSION and I18N_CDN_BASE_URL through getI18nServerEnv(). Both paths pass those values to createI18nResourceLoader() through i18next-resources-to-backend, but they also set preferSameOrigin when NODE_ENV=development.

preferSameOrigin is the important local switch. Even if a CDN base URL is present in the environment, the loader uses the app-relative /i18n/{version}/... path first. In next dev, that app-relative path lands on the source-backed API routes above, so local work does not accidentally bypass the rewrites and fetch stale generated or remote assets.

Production-like builds keep the same runtime URL shape:

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

The difference is that the development rewrites are not installed. The same-origin path now resolves to generated static files in the frontend image:

apps/wavemap-front-end/public/i18n/{I18N_VERSION}/{locale}/{namespace}.json
apps/wavemap-front-end/public/i18n/{I18N_VERSION}/manifest.json

build:i18n-assets creates that versioned snapshot from packages/i18n/locales: it validates each source JSON file, copies each locale namespace into the matching public asset folder, and writes a manifest containing the version, generation timestamp, locale list, namespace list, and default locale.

The runtime loader still selects the delivery source. createI18nResourceLoader() receives:

  • version, from NEXT_PUBLIC_I18N_VERSION on the client and I18N_VERSION in server-side isolated i18n helpers.
  • assetBaseURL, from NEXT_PUBLIC_I18N_CDN_BASE_URL on the client and I18N_CDN_BASE_URL on the server.
  • preferSameOrigin, which is false outside development.
  • fallbackToSameOriginOnError, which current client and isolated server initialization leave enabled.

That produces these production-like branches:

  • No CDN base URL: load same-origin assets from the deployed frontend.
  • CDN base URL configured: strip trailing slashes from the CDN base URL and try {cdnBase}/i18n/{version}/{locale}/{namespace}.json first.
  • CDN failure with fallback enabled: retry /i18n/{version}/{locale}/{namespace}.json on the deployed frontend.
  • CDN failure with fallback disabled: fail immediately with the CDN fetch error.

Every resource fetch uses cache: "no-store" at the loader level. CDN and platform caching can still sit in front of the asset files, but the app-side fetch call does not request browser or server fetch-cache reuse.

Same-origin fallback only works when the deployed frontend image includes the matching generated asset version. If the CDN contains I18N_VERSION=x but the frontend image only includes public/i18n/y, the fallback path cannot repair a CDN miss for version x. That is why build, deploy, and CDN publish workflows must keep I18N_VERSION, NEXT_PUBLIC_I18N_VERSION, generated assets, and any CDN upload target aligned.

The production-like path is therefore not a separate i18n runtime. It is the same i18next backend loader pointed at a different delivery layer: generated same-origin files by default, optional CDN first, and same-origin generated files as the fallback lane.

The i18n package owns asset generation and validation:

Terminal window
pnpm -C packages/i18n build:i18n-assets
pnpm -C packages/i18n validate:i18n-assets
pnpm -C packages/i18n build-and-validate:i18n-assets

Validation checks that:

  • The generated root exists.
  • The manifest exists.
  • The manifest version matches the package/runtime i18n version.
  • Manifest locales and namespaces match the source translation surface.
  • Every generated locale contains the expected namespace set.

The root CI front-end smoke lane generates and validates the static asset snapshot before building the frontend runtime. That keeps browser smoke focused on whether the built app can boot and behave with the exact assets the image contains.

CDN publishing is modeled as an optional delivery layer over the same generated snapshot.

Package entry points:

Terminal window
pnpm -C packages/i18n publish:i18n-assets:cdn
pnpm -C packages/i18n prune:i18n-assets:cdn

The current provider adapter model is intentionally narrow:

  • Azure provider support exists.
  • AWS provider support is stubbed and errors as unsupported for now.
  • Publish uploads generated files for one version.
  • Prune keeps the configured number of recent versions plus the current version.

AWS S3 / CloudFront locale hosting should update the same provider-neutral delivery contract rather than changing the frontend environment vocabulary.

The first deployed-dev path uses same-origin generated frontend assets. Pulumi outputs expose an i18nAssets contract so future CDN delivery can fit into the same shape:

  • i18nAssets.deliveryMode
  • i18nAssets.publicBaseURL
  • i18nAssets.version

Deploy validation derives frontend build inputs from Pulumi-shaped outputs and repo-owned i18n sources. If i18n changes alter asset generation, public route shape, locale defaults, or CDN semantics, update deployed-dev contract checks in the same work pass.