Bondery Docs

Internationalization (i18n)

How Bondery loads, types, and validates translated copy across apps.

User-facing strings live in @bondery/translations. Each app calls generated namespace hooks — not generic t(key, { ns }) — so TypeScript can autocomplete keys and prefixes.

Locales

CodeLanguageRole
enEnglishReference locale (DEFAULT_LOCALE)
csCzech
deGerman

Source of truth: packages/schemas/locale/supported-locales.json. Exported as SUPPORTED_LOCALES from @bondery/schemas/locale.

To add a language, follow Add a new language.

Layout

packages/translations/
  manifest.json          # namespaces + preload groups
  src/locales/{locale}/  # one JSON file per namespace

A namespace is the filename without .json (folders are organizational only). Examples: common, GroupsPage, MobileSettings.

Architecture decision: typed namespace hooks (not selectors)

Phase 0 spike (2026): We evaluated i18next enableSelector: true and selector syntax (t($ => $.AddGroupModal.Title)).

ApproachVerdict
SelectorsRejected for Bondery: incompatible with our dominant keyPrefix pattern (useT(ns, { keyPrefix })t("Title") maps to nested keys). Would require rewriting ~200 call sites to selector form and uncertain next-i18next support.
Literal-namespace codegen hooksAdopted (Phase 1): each namespace gets use{Namespace}Translations(keyPrefix?) with a hardcoded namespace literal so TypeScript only resolves keys within that namespace.
Catalog + branch-prefix unions (Phase 2)Adopted: manifest-aligned Catalog types, generated *Prefix unions, and TranslateFn<NS, Prefix> give compile-time checks on both namespace and keyPrefix.

Runtime behavior is unchanged (same JSON, preload groups, resourceLoader). Only the hook surface and generated types are stricter.

Type system (Phase 2)

Types are generated from manifest.json + English locale JSON:

ArtifactPathPurpose
Catalogpackages/translations/src/generated/i18n/catalog-types.tsPer-namespace JSON tree interfaces; NamespaceKey = keyof Catalog
*Prefix unionspackages/translations/src/generated/i18n/catalog-prefixes.tsValid object-branch keyPrefix values per namespace (e.g. SettingsPagePrefix)
TranslateFnpackages/translations/src/i18n-types.tsTFunction<NS, Prefix> — keys autocomplete under the chosen prefix
BranchPrefixFor<NS>sameAlias for a namespace's prefix union
PrefixedKeys<NS, Prefix>sameDot-joined key paths available under a prefix
i18next augmentpackages/translations/src/generated/i18next-cli/i18next.d.tsPatched at build time to use Catalog (manifest keys like "GroupsPage", not file paths)

Codegen order (pnpm run build -w @bondery/translations):

  1. i18next-cli types — lint/status tooling types (path-based resources.d.ts, kept for CLI only)
  2. generate-i18n-catalog.mjsCatalog, prefix unions, patches i18next.d.ts
  3. generate-client-i18n-hooks.mjs — per-app hooks with prefix overloads

Hooks (use these in app code)

Webapp

import { useGroupsPageTranslations, useCommonTranslations } from "@/lib/i18n/generated/hooks";

// With keyPrefix — keys are relative to the branch; prefix is type-checked
const t = useGroupsPageTranslations("AddGroupModal");
t("Title"); // → GroupsPage.AddGroupModal.Title

// Without keyPrefix — use full dotted paths within the namespace
const tPage = useGroupsPageTranslations();
tPage("AddGroupModal.Title");

const tCommon = useCommonTranslations();
tCommon("actions.cancel");

Prefix typo → compile error:

useSettingsPageTranslations("DataManagement.VCardImportt"); // not in SettingsPagePrefix

Server metadata:

import { getPeoplePageTranslations } from "@/lib/i18n/generated/hooks.server";

const t = await getPeoplePageTranslations();

Do not use: useWebTranslations("…"), getWebTranslations("…"), or t(key, { ns: "…" }).

Mobile

import { useMobileSettingsTranslations } from "@/lib/i18n/generated/hooks";

const t = useMobileSettingsTranslations();
t("Title");

Do not use: useMobileTranslations() with per-call { ns: "…" }.

Chrome extension

import { useExtensionPopupTranslations } from "@/lib/i18n/generated/hooks";

const t = useExtensionPopupTranslations("LoggedOut");
t("Title");

Hooks are regenerated from manifest.json when you run pnpm run build -w @bondery/translations. Each hook hardcodes its namespace and type-checks keyPrefix branches.

Escape hatches (dynamic keys)

Most call sites should use generated hooks and literal keys. Use these only when keys are computed at runtime.

LooseTranslateFn

For utilities that map runtime values to keys (interaction types, slash commands, social tooltips):

import type { LooseTranslateFn } from "@bondery/translations";
import { useInteractionTypesTranslations } from "@/lib/i18n/generated/hooks";

const t = useInteractionTypesTranslations() as LooseTranslateFn;
t(dynamicKey);

transT<Trans> component

next-i18next / react-i18next <Trans> expects a plain TFunction. Cast namespace-scoped t:

import { transT } from "@/lib/i18n/transT";
import { useSettingsPageTranslations } from "@/lib/i18n/generated/hooks";

const t = useSettingsPageTranslations("DataManagement.DeleteAccount");
<Trans t={transT(t)} i18nKey="ConfirmLabel" />

optionalPluralFragment

Plural fragments used as interpolation values inside a parent sentence:

import { optionalPluralFragment } from "@/lib/i18n/optionalPluralFragment";

t("Summary", {
  skippedDetails: optionalPluralFragment(t, "Skipped_one", skippedCount, { count: skippedCount }),
});

Namespaces (i18next best practices)

Follow i18next namespaces:

  • Semantic splits: common, validation, glossary
  • Per feature/page: GroupsPage, MobileSettings, …
  • Preload groups in manifest.json — load only what a route/screen needs

Follow interpolation best practices:

  • Prefer separate keys when grammar varies by interpolated value (e.g. payment type, gender)
  • Use context / plurals / formatting for locale-specific rules
  • Reserve interpolation for runtime-only values (user input, timestamps)

Regenerate types and hooks

After editing locale JSON or manifest.json:

pnpm run build -w @bondery/translations

This rebuilds Catalog types, prefix unions, i18next augmentations, and per-app generated/hooks*.ts.

CI checks

CommandWhat it verifies
pnpm run check:i18nAll i18n CI gates below (umbrella)
pnpm run check:i18n:structureLocale file parity, manifest rules, Languages exonyms
pnpm run check:i18n:typesGenerated i18next-cli types are current
pnpm run check:i18n:usageEvery key used in code (including TypedTrans i18nKey) exists in all locales; cs/de key parity vs en
pnpm run check:i18n:lintNo unexpected hardcoded UI strings (webapp + mobile)

API error code translations use pnpm run check:api-errors:translations (part of check:api-errors), not check:i18n.

For rich text with embedded links or markup, prefer TypedTrans (webapp) with a single translation key and component placeholders — see apps/webapp/src/lib/i18n/TypedTrans.tsx.

Run these before opening a PR that touches copy.

On this page