Repository navigation
i18n: languages with more than two plural forms cannot be translated #12106
Description
Summary
Languages with more than two plural forms cannot be translated correctly today. Russian needs four (one / few / many / other); every plural site in the renderer can express two. This is not a catalog gap — the selection happens in JavaScript, so no translation can fix it.
Two independent causes, both measurable.
1. Call sites pick the form in JS, not through i18next
Every plural family in the renderer is resolved with a ternary:
// ComputerUsePane.tsx
: missingCount === 1
? translate('…permissionsRequired_one', '1 permission required…')
: translate('…permissionsRequired_other', '{{value0}} permissions…', { value0: missingCount })All 7 plural families follow this shape (found_theme, import_theme, imported, over_limit, fullCoverage, permissionsRequired, plus the one in WarpThemeImportModal). None passes count, so i18next's CLDR selection never runs and _few / _many are unreachable regardless of what a catalog provides.
This is worth flagging because config/i18n-translation-source.md assumes the opposite:
Plurals use Orca's existing per-key convention: each i18next plural-suffixed key (
…_one,…_other) is its own PO entry. The profile must not usemsgid_plural/msgstr[n], which would collide with i18next's own CLDR plural selection.
The PO profile is designed around a selector that is currently bypassed. Worth reconciling before PR B lands, since the migration inherits whatever convention the call sites use.
2. Plugin language packs never reach CLDR rules
Even after a call site passes count, plugin catalogs still get English plural behaviour. They register under the synthetic plugin<hex> resource language from pluginLanguageResourceId(), which i18next cannot map to CLDR. Measured against a real catalog containing all four Russian forms:
| count | ru |
plugin<hex> |
|---|---|---|
| 1 | 1 разрешение | 1 разрешение |
| 2 | 2 разрешения | 2 разрешения |
| 5 | 5 разрешений ✅ | 5 разрешения ❌ |
At 5 the synthetic language falls back to _other instead of _many.
Why this matters beyond correctness
The current shape produced real defects in shipped catalogs. Before #11826, permissionsRequired used suffix interpolation, and the translations read:
ko: 'agents가 앱 창을 조작하려면 먼저 {{value0}} 권한{{value1}}이 필요합니다.'
zh: '在智能体可操作应用窗口前需要 {{value0}} 项权限{{value1}}。'
Korean and Chinese have no plural suffix, so every count above one rendered 권한s이 and 权限s — an English s spliced into the sentence. That one is fixed, but the pattern that produced it is still the default.
Suggested direction
Not sending a PR for this: it changes a convention used across the renderer, and the right shape depends on how PR B wants to model plurals. Two pieces, in order:
- Route plural resolution through the declared locale.
getIntlLocale()(proposed in fix(i18n): format the stats date with the selected UI language #12105) already maps a plugin pack's synthetic tag back to its BCP-47 locale. Feeding that to i18next'spluralResolverwould fix everycount-based call site at once, without touching them individually — suggested by @greptile-apps in review. - Move call sites to
count. Mechanical, 7 families, but only worth doing once (1) makes the extra forms reachable.
Happy to implement either once there is a preferred direction.
Environment
- Orca
main@0d38053(v1.4.163) - Measured with i18next as bundled, catalogs from
src/renderer/src/i18n/locales/
Activity
smwbev commentedon Aug 4, 2026
A concrete instance of this came up today in #12478, from the built-in-catalog side rather than the plugin side.
@nwparker asks there, as an open question for a maintainer:
That is the same constraint this issue describes, seen from the other end. For ja/ko/zh the duplicate values are harmless and arguably the honest encoding. For a language with three or more CLDR categories it is a ceiling: Russian needs
one(1, 21, 31…),few(2–4, 22–24…) andmany(0, 5–20…), and a=== 1branch in the code can only express two of them —2 сессииand5 сессийare different words, so one of the two is always wrong. Polish, Czech, Ukrainian, Arabic and Welsh have the same shape.So the question is not only which of
countversus a ternary the codebase prefers, but whether plural selection lives in the code at all. While it does, a plugin language pack cannot express the difference no matter what it puts in the catalog — the syntheticplugin<hex>resource language never reaches CLDR selection.Two more data points since this was filed:
=== 1ternary aftercountwas rewritten out of it; fix(i18n): localize the status bar Resource Manager tooltip and remote-host count #12478 uses{{count}}with_one/_other. Whichever way this is settled, it would help to settle it explicitly.сессий терминала: {{count}}reads correctly for 2, 5 and 21). It works, but it constrains translators to a register that reads like a status line, which is not always what the surrounding copy wants.Happy to prototype whichever direction a maintainer prefers — including the null result, that plugin packs stay two-form by design and the constraint just gets documented for pack authors.
nwparker commentedon Aug 4, 2026
down for a prototype! 🙏
Honestly down for either way. A is fine is perf isn't weird and we can ensure agents call the right thing. Otherwise B w/ machnical works as well. Not strong feelings TBH
smwbev commentedon Aug 4, 2026
Prototype for option (1) is up: #12505.
It does what the issue proposed — feeds the locale a pack declares to the plural resolver, and only to the resolver, so resource lookup keeps running on the synthetic
plugin<hex>tag. No call site changes; everycount-based one starts selecting real categories.@nwparker — on your "if perf isn't weird" condition, the measurement went the other way and the reason is worth a look.
PluralResolver.getRulewrites its cache at the end of the method, but the no-separator fallback returns early:A synthetic tag has no
-or_, so today every single call constructs a freshIntl.PluralRulesover a 90-character invalid tag and catches theRangeError. PergetRule: 3.549 µs forplugin<hex>versus 0.129 µs forru-RU, which is cached. End to end ont()withcount, 200k calls: 15.249 → 3.580 µs/call.So the honest answer is that the hot path is throwing once per lookup today, and mapping the code to a real locale puts it back on the cached path. That is an i18next-side bug affecting any consumer with a non-BCP-47 resource language — happy to file it upstream separately if the reading looks right to you.
Option (2), moving the seven ternary call sites to
count, is deliberately not in that PR. It only pays off once (1) makes the extra categories reachable, and keeping them apart means (1) can be judged on its own.fix(i18n): resolve plugin-pack plurals with the declared locale
smwbev commentedon Sep 9, 2026
A third pattern turned up while checking
v1.4.198, and it is worse than the two above: the plural suffix is concatenated outsidetranslate(), so the string is not merely limited to two forms — it cannot be translated at all.components/skills/SkillInstallReviewContent.tsx:84:Both keys are in the catalog and look translatable. Translate
placementinto Russian and the rendered result is3 размещениеs checked.— an Englishswelded onto a Russian noun. The only way a translator can avoid that is to leave the key in English, which is what our pack does today: these two keys are deliberately unfilled.Eight sites in six files share this shape in
v1.4.198:components/editor/ConflictComponents.tsxcomponents/right-sidebar/checks-panel/conflict-summary.tsxcomponents/right-sidebar/checks-panel/triage-strip.tsxcomponents/right-sidebar/source-control/listing/section-header.tsxcomponents/settings/McpConfigSection.tsxcomponents/skills/SkillInstallReviewContent.tsxThe codebase already has a shape that works. Fourteen other sites pass the suffix through the catalog as a placeholder:
Here a translator can ignore
{{value1}}and writeПодключено сайтов: {{value0}}, which reads correctly for every count. Same two-form limitation as the ternary sites, but at least the sentence survives translation. Moving the eight sites above to this shape would cost little and would not require resolving the largercount/CLDR question in this issue.Separately, and outside this issue's scope: sixteen sites in nine files build the same
N item(s)phrase in a bare template literal with notranslate()at all —source-control-dropdown-labels.ts(5),linear-scope-selector.tsx:117,GhAuthErrorHelp.tsx:241among them. Those are invisible to the coverage audit for the reason described in #11826, not a plural problem.Happy to send a PR for the eight sites if the placeholder shape is the direction you want.
Add the Russian plural forms ahead of the resolver fix and publish th…