From a fix to a check in one day: RapidRAW's Russian plurals
RapidRAW's Russian menus said \"Delete image\" with 21 images selected. A 13-line fix merged the next morning, and that afternoon the maintainer merged a 36-line addition to the project's own i18n check so the bug can't come back unnoticed.
Key takeaway
RapidRAW's Russian menus said \"Delete image\" with 21 images selected. A 13-line fix merged the next morning, and that afternoon the maintainer merged a 36-line addition to the project's own i18n check so the bug can't come back unnoticed.
RapidRAW is a GPU-accelerated RAW photo editor with about 10,600 GitHub
stars. Its interface is translated into 14 languages besides English, in hand-maintained i18next JSON under
src/i18n/locales/, and it moves fast: in the two weeks before I looked, about eight outside contributors had
PRs merged by its maintainer, Timon Käch (@CyberTimon).
In October I sent it a 13-line fix to the Russian file. It merged the next morning. When I offered a check that would catch the same bug in the future, he said yes, and the check merged about an hour after I opened it.
The bug: Russian one is not "one"
i18next picks a plural form by count, using the language's CLDR rules. In English the _one form is only ever
used for 1, so "deleteImage_one": "Delete Image" with no {{count}} is fine.
In Russian, _one also covers 21, 31, 41… (new Intl.PluralRules('ru').select(21) is "one"). Thirteen
_one strings in ru.json had copied the English shape and hard-coded "one image". Rendered with i18next on
main at the time (b39884b):
deleteImage 21 → Удалить изображение ("Delete image")
imagesCount 21 → 1 изображение ("1 image")
applyButton 21 → Применить к 1 изображению ("Apply to 1 image")
rateStars 21 → Оценить в 1 звезду ("Rate 1 star")
So with 21 photos selected, the context menu offered to delete one image, and the library said it held one.
I found it while scanning 34 open-source repos with my checker. It only knew how to find this class of bug
because of an earlier case: good_job's Russian and Ukrainian one forms had the same
problem, my checker missed them, and the fix taught it that where a language's one covers more than 1, that
form has to keep the count.
The fix
#1944 added {{count}} to the 13 strings and kept the singular
noun, which is the correct form for 1, 21, 31… The one trade-off, noted in the PR: for exactly 1, the menu now
reads "Удалить 1 изображение" ("Delete 1 image") rather than "Удалить изображение", the same pattern the English
_other uses.
The PR also listed what it didn't fix, because those need a native speaker: Russian _few (2–4) using the
genitive plural, and Czech having no _few forms at all, so for 2–4 items i18next falls back to English.
RapidRAW's template asks how AI was involved; I ticked "mostly AI-generated but edited by a human".
I opened it on October 17 at 11:30 UTC. Timon merged it the next morning at 07:54, with no changes requested.
Why the existing check missed it
RapidRAW already had an i18n check. npm run i18n:check runs i18next-cli extract --ci and then the project's own
src/i18n/check-runtime.mjs, which renders every plural key for every CLDR category in every locale and confirms
the right form resolves. CI runs it on every pull request and every push to main.
That script answers "does Russian have a _one form?". It never asks what the form says, so a _one with the count
missing passed.
The ask, and the second PR
After the merge I offered a ~35-line
addition to that same script rather than a new tool or dependency. It would flag any translation that drops a
{{variable}} the English passes. Plural forms would be compared with English _other, since English _one
legitimately has no count, and could drop {{count}} only where the plural category covers nothing but 0 and 1.
That way French or German one still passes, and Russian one doesn't.
Timon replied 37 minutes later:
Yes, feel free to add this. Thanks!
#1952 (+36/−1, one file, no new dependencies) went up at
13:52 UTC. Before opening it I ran it two ways with i18next 26.4.0, the version in their lockfile: on main it
found nothing new, and on the Russian file from before #1944 it found exactly the 13 strings that PR fixed.
Timon merged it at 14:55 UTC: "LGTM, thanks a lot!"
The bug was fixed upstream at 07:54 and the check that guards against it merged at 14:55, on the same day.
What held
Re-checked on 2026-10-24, main at
2ce7472:
- The fix held. All 13 strings still carry
{{count}}. Six other Russian_onestrings have no count, but their English_otherdoesn't either ("Auto Adjust", "Denoise"), so they're correct, and the check leaves them alone. - The check runs, and has stayed at 0. In the six days after it merged, 26 commits touched the locale files and 174 English keys were added. Running the merged script against the pre-#1944 Russian file still reports exactly the 13.
- It warns but doesn't block. The "Check Translation Sync" step in
lint.ymliscontinue-on-error: true, like the repo's format and lint steps. It also already fails on 198 missing plural forms that predate all of this (Czech 118, Catalan 55, the rest in single digits). A new dropped placeholder would show up in that step's log, next to those 198, and wouldn't stop a merge. - Not fixed: the Russian
_fewwording and the Czech_fewforms. They need native speakers, and the PR said so.
Timeline
| date (UTC) | event |
|---|---|
| Oct 16 | found in a 34-repo scan; strings rendered with i18next 26 to confirm |
| Oct 17 11:30 | #1944 opened: 13 ru _one strings missing {{count}} |
| Oct 18 07:54 | #1944 merged by CyberTimon |
| Oct 18 10:47 | CI check offered as an addition to their own script |
| Oct 18 11:24 | "Yes, feel free to add this." |
| Oct 18 13:52 | #1952 opened: +36/−1 to check-runtime.mjs |
| Oct 18 14:55 | #1952 merged |
| Oct 24 | re-checked on main: fix held, check at 0 after 26 locale commits |
I maintain Shipi18n, the open-source checker that found these strings.
The check RapidRAW merged is their own code in their own script, with no Shipi18n dependency. Every finding was
verified by hand against main before filing. The full list of repos and strings is at
shipi18n.com/oss.