Back to Readest

I18n

apps/readest-app/docs/i18n.md

0.11.13.5 KB
Original Source

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. 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. 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. 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. 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.