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:
| Package | Owns |
|---|---|
@workspace/i18n | Language constants, validation, detection, language selector, Vite helpers that copy locale files and build load paths |
@workspace/i18n-messages | The 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 generatorEach 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.
commonholds generic UI labels (actions, states); a key such asdashboard.users.empty-statedoes not belong there. - Do not repeat generic actions. Use
common.actions.cancelinstead 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-primaryrather thanstart-for-free.
Tests
pnpm --filter @workspace/i18n-messages test__tests__/locale-validation.test.ts checks every namespace for:
- an
enlocale - 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.