## 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/<locale>/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/<code>/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/<code>/` — 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 `<code> 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/<code>/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.
