Bridge Training
i18n Messages

Overview

Translation JSON for every app, exposed as typed namespaces.

@workspace/i18n-messages holds the translation files of the monorepo. All translations live here, whether they are shared between apps or used by a single one. It depends only on @workspace/bridge (for the key registries and rotation entries).

It is the counterpart of @workspace/i18n:

PackageOwns
@workspace/i18nLanguage constants, validation, detection, language selector, Vite helpers that copy locale files and build load paths
@workspace/i18n-messagesThe messages themselves (JSON), their TypeScript types, and the tooling that generates and audits them

Supported languages

The package exports 11 language codes:

import {
  SUPPORTED_LANGUAGES,
  type SupportedLanguage,
} from '@workspace/i18n-messages/supported-languages';

// SupportedLanguage = "en" | "fr" | "de" | "es" | "it" | "nl" | "pl" | "pt" | "ca" | "el" | "sv"

Greek is el (not gr). Not every namespace ships every language: the namespaces the player uses (player, bridge, game, common, billing) plus primitives, rotation and web cover all 11; the others only have en and fr. See Namespaces.

English is the source of truth: every namespace has an en.json, and the tests and scripts compare the other locales against it.

Folder layout

One folder per namespace, one JSON file per language, and an index.ts that bundles them:

packages/i18n-messages/
  src/
    index.ts                  ← root export: every *Messages object + primitivesResources + supported languages
    types.ts                  ← XxxNamespace types (shape of en.json)
    supported-languages.ts
    primitives-resources.ts
    rotation/entries.ts       ← rotation language table built from the rotation namespace
    locales/
      common/
        en.json
        fr.json
        …
        index.ts              ← export const commonMessages = { ca, de, el, en, es, fr, it, nl, pl, pt, sv }
      bridge/
        …
        keys.ts               ← static key registries for the bridge namespace
      deals/
        en.json
        fr.json
        index.ts
      …
  scripts/
    generate-translations.ts
    audit-unused-keys.ts
    audit-keys.allowlist.ts
    translation-agent/        ← system prompt + bridge glossary for the generator

Each index.ts imports the JSON with import attributes:

import en from "./en.json" with { type: "json" };
import fr from "./fr.json" with { type: "json" };

export const usageMessages = {
  en,
  fr,
};

Conventions

The package's CLAUDE.md sets the rules for organising the files:

  • One file per functional domain, not per component or page. The test: if you delete the feature, you delete the whole file.
  • Split when a file goes past roughly 80–100 keys, or when unrelated keys end up grouped together.
  • Do not split to isolate a component (modal.json, dropdown.json), or when the new file would have fewer than about 20 keys with no room to grow.
  • Shared namespaces carry no business knowledge. common holds generic UI labels (actions, states); a key such as dashboard.users.empty-state does not belong there.
  • Do not repeat generic actions. Use common.actions.cancel instead of adding "cancel" to a domain file.
  • Keys are at most 2–3 levels deep, and each segment is kebab-case (no uppercase, no underscores). Name keys after their role, not their content: cta-primary rather than start-for-free.

Tests

pnpm --filter @workspace/i18n-messages test

__tests__/locale-validation.test.ts checks every namespace for:

  • an en locale
  • the same keys in every locale
  • the same {{placeholders}} in every locale
  • no empty strings
  • no values that just repeat the key path (a sign that a translation failed)

For player, bridge, game and common, a locale may have fewer keys than English, but it cannot have keys that English lacks. Other tests cover primitivesResources, the rotation namespace and entries, and the key-audit library.