--- name: i18n description: Rules for user-visible text in Qortal Hub. Read BEFORE writing or editing any component, dialog, menu, toast, tooltip, aria-label, placeholder, or error message. Every user-facing string must go through i18next — never hardcode English. Triggers on any UI work, any new component, any string a user could read. --- # i18n is mandatory for every user-visible string Qortal Hub ships in 12 languages. A hardcoded English literal is a bug, not a shortcut. This applies to **new code and to any line you touch in existing code**. ## What counts as user-visible Translate all of these — the last three are the ones most often missed: - JSX text content, `Typography`, `Button`, `MenuItem` children - `label`, `placeholder`, `title`, `helperText`, `alt` - **`aria-label` and every other accessibility string** - **Error and status messages**, including ones thrown/rejected from async code - **Strings built in plain `.ts` helper modules** that end up on screen Do NOT translate: log messages, event names passed to `executeEvent` / `subscribeToEvent`, API field names, test fixtures, `data-*` attributes. ## The pattern in components ```tsx import { useTranslation } from 'react-i18next'; const { t } = useTranslation(['auth', 'core', 'group', 'question', 'tutorial']); ``` Declare only the namespaces the component actually uses. Always prefix the key with its namespace (`core:`, `group:`, …) — the default namespace is `core`, but be explicit anyway. ## The pattern outside components Non-React modules import the instance directly rather than using the hook: ```ts import i18n from '../i18n/i18n'; throw new Error( i18n.t('auth:message.error.invalid_uint8', { postProcess: 'capitalizeFirstChar' }) ); ``` For pure helper modules that return display text (see `src/components/QortalLand/games/gameDialogText.ts` for the anti-pattern), prefer **taking `t` as a parameter** so the caller's namespaces and language reactivity apply. Return a key + params object instead of a sentence when that is cleaner. ## Casing is done by post-processors, not by the source string Values in the JSON files are stored **lowercase**. Casing is applied at render time via `postProcess`. Never capitalize inside the JSON, and never do `t(...).toUpperCase()` in JSX. Available processors (see `src/i18n/processors.ts`): | Processor | Effect | |---|---| | `capitalizeFirstChar` | `close` → `Close` — the default choice | | `capitalizeFirstWord` | first word uppercased entirely | | `capitalizeEachFirstChar` | Title Case | | `capitalizeSentenceStarts` | capitalizes after `.`/`!`/`?` and newlines | | `capitalizeAll` | ALL CAPS — for stat labels and similar | ## Adding a new key 1. Add it to `src/i18n/locales/en/.json`, lowercase, nested by topic. 2. **Add the same key to all 12 locale dirs** under `src/i18n/locales/` — `ar`, `de`, `en`, `es`, `et`, `fi`, `fr`, `it`, `ja`, `pt`, `ru`, `zh`. Every locale must carry the identical key set and the identical file set (`auth`, `core`, `group`, `node`, `question`, `tutorial`). English text is an acceptable placeholder for a language you cannot translate — a *missing* key is not. 3. Pick the namespace by domain: `core` for shared UI vocabulary and generic messages, `group` for group/chat/Reticulum features, `auth`, `node`, `question`, `tutorial` for their own areas. Adding a *new* namespace also requires registering it in the `namespaces` array in `src/i18n/i18n.ts` — the locale files are glob-loaded, but the namespace list is explicit. ## Key naming Follow the existing shape in the JSON files: - Actions: `core:action.close`, `core:action.add_reaction` - Errors: `core:message.error.generic`, `group:message.error.qortals_required` - Questions/confirms: `core:message.question.delete_chat_image` - Plurals: base key plus `_other` (`core:admin` / `core:admin_other`) — i18next selects via the `count` option. Use `snake_case`, no abbreviations, name by meaning rather than by the English wording so the key survives a copy edit. ## Interpolation ```tsx t('group:message.error.qortals_required', { quantity: 4, postProcess: 'capitalizeFirstChar', }) ``` Never concatenate translated fragments to build a sentence — word order differs across languages. Put the whole sentence in one key with placeholders. ## Known gaps — do not imitate the neighbours Large parts of the newer UI were written **without** i18n and are being migrated: - Most `Reticulum*` components in `src/components/Chat/` - The whole of `src/components/QortalLand/` (including `games/` and `proximity/`) - Leftover literals inside otherwise-translated files such as `ChatGroup.tsx` and `MessageItem.tsx` "Match the surrounding code" does not apply here. When editing these files, use `t()` for the lines you touch, and mention any remaining hardcoded strings you noticed rather than silently leaving them. ## Before you finish Re-read your diff for quoted English. A quick check: ```bash grep -nE '(aria-)?label="[A-Za-z]|placeholder="[A-Za-z]|title="[A-Za-z]|>[A-Z][a-z]+ ' ``` Anything it returns that a user can read must be a `t()` call.