feat(client): add nine locales with English fallback catalogs

Register Czech, Danish, Finnish, Hungarian, Korean, Russian, Simplified Chinese, Swedish, and
Ukrainian so the language picker, browser detection, and server preference validation accept them.
Catalogs hold English fallback values; Crowdin supplies translations through the normal export flow.

Give each catalog exactly the CLDR plural categories its locale defines rather than copying the
English branches. Czech, Russian, and Ukrainian gain `few` and `many` seeded from `other` because
the source has no matching text. Korean and Chinese drop `one`, which they have no rule for and
Crowdin cannot store. Those synthesized branches all start from the same English string, so machine
translation returns identical forms where the grammar differs and a human has to resolve them.

Ship Chinese as Simplified only under the bare `zh` locale so every zh-* browser resolves to it.
Traditional Chinese, also requested in #86, would arrive later as a separate zh-Hant catalog.

Use `sv-SE` for Swedish in the Crowdin config, because Crowdin has no bare `sv` language and would
have skipped the export. Tolerate a wrapped SUPPORTED_LOCALES array in validate-locales, which
prettier now produces since the list outgrew the print width, and extend the exhaustive ICU test to
reach Polish `other`, a category its counts never selected.

Add the repository activity embed to the README alongside the existing translation progress chart.

Repair the KOReader plugin source assertions that 40e366a4 left stale after reshaping the
dashboard streak block and the downloaded badge icon lookup, and watch `koreader-plugin/**` in
the server CI path filter so plugin-only changes run the server suite that reads those files.

Closes #86
Closes #363
Closes #576
This commit is contained in:
neon
2026-07-31 19:20:35 +00:00
parent c9c5df7a87
commit 1215a45f93
19 changed files with 62321 additions and 11 deletions
+24 -4
View File
@@ -1,22 +1,35 @@
# Localization
BookOrbit uses Vue I18n catalogs under `client/src/locales/`. English is the source language, and Crowdin is the source of truth for Dutch, French, German, Italian, Polish, Brazilian Portuguese, Slovenian, and Spanish translations.
BookOrbit uses Vue I18n catalogs under `client/src/locales/`. English is the source language, and Crowdin is the source of truth for every other catalog listed below.
## Supported Catalogs
| Application locale | Crowdin language ID | Catalog |
| -------------------- | ------------------- | --------- |
| English | Source language | `en.json` |
| Czech | `cs` | `cs.json` |
| Danish | `da` | `da.json` |
| German | `de` | `de.json` |
| Spanish | `es-ES` | `es.json` |
| Finnish | `fi` | `fi.json` |
| French | `fr` | `fr.json` |
| Hungarian | `hu` | `hu.json` |
| Italian | `it` | `it.json` |
| Korean | `ko` | `ko.json` |
| Dutch | `nl` | `nl.json` |
| Polish | `pl` | `pl.json` |
| Brazilian Portuguese | `pt-BR` | `pt.json` |
| Russian | `ru` | `ru.json` |
| Slovenian | `sl` | `sl.json` |
| Swedish | `sv-SE` | `sv.json` |
| Ukrainian | `uk` | `uk.json` |
| Simplified Chinese | `zh-CN` | `zh.json` |
Crowdin's `%two_letters_code%` placeholder maps Brazilian Portuguese to `pt`, which matches BookOrbit's existing application locale and filename.
Crowdin's `%two_letters_code%` placeholder reduces the regional IDs above to BookOrbit's application locale and filename: `es-ES` to `es`, `pt-BR` to `pt`, `sv-SE` to `sv`, and `zh-CN` to `zh`.
Verify every ID against Crowdin's language list rather than assuming the two-letter form exists. Several languages have no bare two-letter ID at all: Swedish is `sv-SE`, and `sv-FI` reduces to the same `sv.json`, so enabling both would collide.
Chinese ships as Simplified only under the bare `zh` locale, so every `zh-*` browser resolves to it, including Traditional readers. Adding Traditional later means a new `zh-Hant` catalog with a `languages_mapping` entry for `zh-TW`; `zh` keeps its Simplified meaning, so no rename or stored-preference migration is needed. That addition also requires matching by script subtag in `client/src/stores/locale.ts`, because the current base-language fallback would send `zh-Hant` to `zh`.
## Adding User-Facing Copy
@@ -76,7 +89,7 @@ After an English source change reaches `main`:
2. Translators update target strings in Crowdin.
3. Crowdin exports the target catalogs to `l10n_main`.
4. Crowdin opens a pull request to `main`.
5. CI verifies that the pull request changes only the eight target catalogs and runs the normal client checks.
5. CI verifies that the pull request changes only the seventeen target catalogs and runs the normal client checks.
6. A maintainer reviews and squash-merges the pull request.
7. The `l10n_main` service branch is deleted. Crowdin recreates it for the next export.
@@ -122,7 +135,14 @@ Create `client/src/locales/<locale>.json` with the exact key structure from `en.
A catalog may instead arrive already translated, for example from a bulk translation pass. That is allowed, but the translations then exist only in Git and Crowdin will overwrite them unless they are imported first. Follow "Seed existing translations into Crowdin" below before enabling export.
Review `client/src/stores/locale.ts` and add browser-locale matching coverage to `client/src/stores/__tests__/locale.spec.ts`, especially for regional variants. Add every CLDR plural category reported for the locale to each ICU plural message and extend the exhaustive ICU runtime tests in `client/src/i18n/icu.spec.ts`.
Review `client/src/stores/locale.ts` and add browser-locale matching coverage to `client/src/stores/__tests__/locale.spec.ts`, especially for regional variants.
Each ICU plural message must carry exactly the CLDR categories reported for the locale, plus every exact selector such as `=0` from the English source. Both directions matter:
- A missing category fails `validate:locales`. Czech, Russian, and Ukrainian need `few` and `many` added to every plural message, seeded from the English `other` branch because the source has no corresponding text.
- An extra category leaves Crowdin nowhere to store it, because the editor renders only the categories the locale defines. Chinese has just `other`, so an inherited `one` branch produces a permanent difference that `scripts/crowdin-verify/verify.mjs --reconcile` can never close.
Extend the exhaustive ICU runtime tests in `client/src/i18n/icu.spec.ts` with counts that reach every category. Derive them from `Intl.PluralRules` rather than by analogy, because languages that report the same category set do not map counts the same way: Russian and Ukrainian select `many` for 5 and reach `other` only through a fraction such as 1.5, while Czech selects `other` for 5 and reserves `many` for fractions.
### 4. Seed existing translations into Crowdin