Bridge Training
i18n Messages

Consumption

How the app, the player, the website and the admin load these messages.

Apps use the package in one of two ways:

  • Over HTTP, through i18next (apps/app, apps/player). The JSON files are served as locales/<lang>/<ns>.json and loaded at runtime by i18next-http-backend. The package is only imported for its types (and key registries).
  • As a direct import (apps/web, packages/training, apps/app, apps/admin). The *Messages objects or the rotation entries are imported like any other module.

Vite apps (app and player)

Both Vite apps declare an I18N_LOCALES object in vite.config.ts that lists the namespaces they serve, each pointing at a folder of this package:

// apps/player/vite.config.ts
const I18N_LOCALES = {
  languages: [...SUPPORTED_LANGUAGES],
  packages: [
    {
      localesDir: "../../packages/i18n-messages/src/locales/common",
      namespace: "common",
    },
    // bridge, game, player, billing …
  ],
};

I18N_LOCALES is used in three places:

PhaseMechanism
DevAn i18n-dev-server middleware serves /locales/<lang>/<ns>.json straight from packages/i18n-messages/src/locales, and i18nextHMRPlugin hot-reloads edits
BuildcreateI18nCopyTargets(I18N_LOCALES) (from @workspace/i18n/vite) copies each file to dist/locales/<lang>/<ns>.json
BuildcreateI18nLocaleHashes(...) hashes each file's content; createI18nLoadPath appends ?v=<hash> to its URL, so a changed file gets a new URL

The namespace names in I18N_LOCALES, in the i18next ns option and in the i18next.d.ts resources must match.

apps/app

  • Languages: fr and en only (DEFAULT_LANGUAGE is fr).
  • Namespaces: common, errors, teaching, students, deal-collections, public-sessions, calendar, deals, deal-editor, metadata, game, account, billing, sharing-group, bridge, tour, usage, dev-tools, primitives, rotation, rich-editor.
  • fallbackLng: DEFAULT_LANGUAGE: a missing key falls back to French.
  • Typed keys come from @workspace/i18n-messages/types (see Namespaces).

apps/player

  • Languages: all 11 (apps/player/src/lib/i18n/constants.ts).
  • Namespaces: player (default), bridge, game, common, billing.
  • fallbackLng: false. A key missing in the target language renders as the raw key, not as the English text. The runtime never hides gaps; the translation generator is what fills them.
  • Unsupported detected languages are filtered out by supportedLngs and fall back to SUPPORTED_LANGUAGES[0] (en).
  • Translations are cached in localStorage (i18next-localstorage-cache). Bump the per-language cache.versions in apps/player/src/lib/i18n/instance.ts when a namespace gains or renames keys.
  • At build time, apps/player/vite/unsupported-page.ts reads player/<lang>.json from disk to inline unsupported.* into unsupported.html. That is why the key audit has an allowlist entry for it.

apps/web

The Next.js site does not use i18next. It imports the web namespace as an object and reads it by property:

// apps/web/src/lib/get-dictionary.ts
import { webMessages } from "@workspace/i18n-messages/web"

export type Dictionary = (typeof webMessages)["en"]

export function getDictionary(lang: LanguageCode | (string & {})): Dictionary {
  const locale = parseLocale(lang)
  return webMessages[locale]
}

It also reads bridgeMessages[locale].labels.bids directly for its bid labels (apps/web/src/lib/bridge-strings.ts).

Rotation (domain and admin)

packages/training/src/domain/shared/rotater.ts, apps/app/src/lib/rotater.ts and apps/admin/src/lib/rotater.ts build their rotaters with buildAllRotationEntries() from @workspace/i18n-messages/rotation/entries. @workspace/bridge stays free of translation data: the caller supplies the language table.