## i18n Guide Readest uses a **key-as-content** approach — English strings are the translation keys. The English locale (`en/translation.json`) is empty because keys serve as content. Other locales contain actual translations. ### In React Components ```tsx import { useTranslation } from '@/hooks/useTranslation'; const _ = useTranslation(); _('Progress synced'); ``` ### In Non-React Modules Two-step process: **1. Declaration** — Use `stubTranslation` to mark strings for scanner extraction (returns key as-is, does NOT translate): ```ts import { stubTranslation as _ } from '@/utils/misc'; // These calls only register keys for extraction _('Reveal in Finder'); _('Reveal in Explorer'); ``` **2. Usage** — In the React component that consumes the value, apply the real `_()` from `useTranslation`: ```tsx const _ = useTranslation(); const label = _(getRevealLabel()); // translates at runtime ``` ### Extraction & Translation ```bash pnpm i18n:extract # Scans codebase, adds new keys with __STRING_NOT_TRANSLATED__ ``` - Translation files: `public/locales//translation.json` - Only `_('KEY')` and `_('KEY', options)` patterns are recognized by i18next-scanner ### Adding a New Translation Language The supported language set has a single ground truth: [`i18n-langs.json`](../i18n-langs.json). Both the i18next runtime (`src/i18n/i18n.ts`) and the extractor (`i18next-scanner.config.cjs`) read from it, so adding a locale is a two-file change plus a translation pass. 1. **Add the locale code** to [`i18n-langs.json`](../i18n-langs.json). Use the exact code i18next will emit (e.g. `hu`, `zh-CN`). Do not add `en` — it's the source language and lives outside this list. 2. **Add a display label** to `TRANSLATED_LANGS` in [`src/services/constants.ts`](../src/services/constants.ts). The key is the locale code, the value is the language's native name (e.g. `hu: 'Magyar'`). This is what users see in the language picker. 3. **Generate the translation file**: ```bash pnpm i18n:extract ``` This creates `public/locales//translation.json` with every key set to `__STRING_NOT_TRANSLATED__`. 4. **Translate** every `__STRING_NOT_TRANSLATED__` placeholder in the new file. The `/i18n` skill automates this; the singular `en/translation.json` only holds plural variants and proper nouns, so use the JSON keys themselves as the English source. 5. **Verify** with `grep -r "__STRING_NOT_TRANSLATED__" public/locales//` — the result should be empty. 6. **Translate the KOReader companion plugin** (`apps/readest.koplugin`). It pulls the locale set from the same `i18n-langs.json` via the scanner config, but the catalog format is gettext `.po`, not JSON. Steps: - Add a `LANG_META` entry (label + Plural-Forms) for the new code in [`apps/readest.koplugin/scripts/extract-i18n.js`](../../readest.koplugin/scripts/extract-i18n.js). Without it the extractor prints ` skipped (no metadata in extract-i18n.js)` and the catalog is never created. - Trigger the `/i18n-koplugin` skill to run extraction and fill every empty `msgstr ""` in `apps/readest.koplugin/locales//translation.po`. ### Rules - `stubTranslation` is for extraction only — always apply `_()` from `useTranslation` in the component for runtime translation. - Fallback: when no translation exists, the English key itself is displayed. - Error messages: register keys with `stubTranslation` in utility modules (e.g. `src/services/errors.ts`), return the English key from helpers, wrap with `_()` in the component.