Namespaces
Subpath exports, message objects, types and key registries.
Each namespace is a folder under src/locales/. Its index.ts exports a <camelCase>Messages object keyed by language code, and each value is the parsed JSON for that language.
import { dealsMessages } from '@workspace/i18n-messages/deals';
dealsMessages.en; // content of src/locales/deals/en.json
dealsMessages.fr; // content of src/locales/deals/fr.jsonNamespace list
| Namespace | Subpath export | Messages export | Languages |
|---|---|---|---|
account | ./account | accountMessages | en, fr |
billing | ./billing | billingMessages | all 11 |
bridge | ./bridge | bridgeMessages | all 11 |
calendar | ./calendar | calendarMessages | en, fr |
common | ./common | commonMessages | all 11 |
deal-collections | none (root only) | dealCollectionsMessages | en, fr |
deal-editor | ./deal-editor | dealEditorMessages | en, fr |
deals | ./deals | dealsMessages | en, fr |
dev-tools | ./dev-tools | devToolsMessages | en, fr |
errors | ./errors | errorsMessages | en, fr |
game | ./game | gameMessages | all 11 |
metadata | ./metadata | metadataMessages | en, fr |
player | ./player | playerMessages | all 11 |
primitives | ./primitives | primitivesMessages | all 11 |
public-sessions | ./public-sessions | publicSessionsMessages | en, fr |
rich-editor | ./rich-editor | richEditorMessages | en, fr |
rotation | ./rotation | rotationMessages | all 11 |
sharing-group | ./sharing-group | sharingGroupMessages | en, fr |
students | ./students | studentsMessages | en, fr |
teaching | ./teaching | teachingMessages | en, fr |
tour | none | tourMessages (not re-exported from the root) | en, fr |
usage | none (root only) | usageMessages | en, fr |
web | ./web | webMessages | all 11 |
The root export @workspace/i18n-messages re-exports every *Messages object except tourMessages, plus primitivesResources, PrimitivesTranslations, SUPPORTED_LANGUAGES and SupportedLanguage. Apps that only need one namespace import from its subpath.
Other subpath exports
| Subpath | Content |
|---|---|
./types | One XxxNamespace type per namespace (the shape of its en.json) |
./supported-languages | SUPPORTED_LANGUAGES and SupportedLanguage (11 codes) |
./primitives-resources | primitivesResources and PrimitivesTranslations |
./bridge/keys | Static key registries for the bridge namespace |
./rotation/entries | buildRotationEntry and buildAllRotationEntries for the @workspace/bridge rotater |
Types
@workspace/i18n-messages/types derives each namespace type from its English file:
export type DealsNamespace = (typeof dealsMessages)["en"];
export type PlayerNamespace = (typeof playerMessages)["en"];Apps use these types to give i18next typed keys. From apps/app/src/lib/i18n/i18next.d.ts:
import type {
CommonNamespace,
DealsNamespace,
// …
} from "@workspace/i18n-messages/types";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: {
common: CommonNamespace;
deals: DealsNamespace;
// …
};
}
}primitivesResources
primitivesResources groups the primitives and rotation namespaces for each of the 11 languages, ready to pass to i18next resources:
import {
primitivesResources,
type PrimitivesTranslations,
} from '@workspace/i18n-messages/primitives-resources';
primitivesResources.en; // { primitives: primitivesMessages.en, rotation: rotationMessages.en }
primitivesResources.el; // { primitives: primitivesMessages.el, rotation: rotationMessages.el }
// PrimitivesTranslations = (typeof primitivesMessages)["en"]Bridge key registries
./bridge/keys maps @workspace/bridge values to keys in the bridge namespace. Callers look up a key with a typed value instead of building it in a template literal, so every key stays a string literal that you can grep for and that the key audit can see. Each registry uses as const satisfies Record<…, string>, so adding a value to the @workspace/bridge type without adding its key is a type error.
// apps/app/src/features/deals/explorer/sidebar-filters/pedagogy-filter.tsx
import { BRIDGE_TEACHING_MODE_KEY } from "@workspace/i18n-messages/bridge/keys";
const { t: tBridge } = useBridgeTranslation();
// …
label={tBridge(BRIDGE_TEACHING_MODE_KEY[mode])}Available registries: BRIDGE_PLAYER_KEY, BRIDGE_SUIT_KEY, BRIDGE_RANK_KEY, BRIDGE_SPECIAL_BID_LONG_KEY, BRIDGE_SUIT_BID_LONG_KEY, BRIDGE_POSITION_KEY, BRIDGE_VULNERABILITY_KEY, BRIDGE_VULNERABILITY_PHRASE_KEY, BRIDGE_SCORING_KEY, BRIDGE_TEACHING_MODE_KEY.
Rotation entries
The rotation namespace holds the four compass directions (direction.N/E/S/W) in each language. ./rotation/entries turns them into the LanguageEntry table that the @workspace/bridge rotater uses. It adds French elision (de → d') and a guard so the French and Catalan "Est" does not match Est-ce, Est-il and similar forms. It also appends a language-independent N/E/S/W abbreviation entry.
import { buildAllRotationEntries } from "@workspace/i18n-messages/rotation/entries";packages/training, apps/app and apps/admin build their rotaters from this table.
Adding a namespace
- Create
src/locales/<namespace>/en.json(plusfr.json, and the other languages if the namespace is player-facing). - Add
index.tsexporting<camelCase>Messages. - Re-export it from
src/index.ts, add anXxxNamespacetype insrc/types.ts, and add a subpath inpackage.jsonexportsif an app imports it directly. - Register it in the consuming app's i18next
nslist and in its ViteI18N_LOCALESconfig (see Consumption).