Skip to content

feat(i18n): complete every shipped locale, and hold them there - #31

Merged
i7Gamer merged 6 commits into
developmentfrom
feat/complete-translations
Aug 25, 2026
Merged

feat(i18n): complete every shipped locale, and hold them there#31
i7Gamer merged 6 commits into
developmentfrom
feat/complete-translations

Conversation

@i7Gamer

@i7Gamer i7Gamer commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Every locale but German shipped incomplete. Twelve sat at 434 of English's 599 keys; Ukrainian at 234. i18next falls back to English, so nothing failed and nothing was logged — the gap was only visible by comparing the files.

This closes it. All 14 locales are now at 100%, ~2,900 strings written, each translated against the vocabulary already in its own file rather than from the English alone.

The half a key count can't see

A value that is present and still holds the English text is indistinguishable from a missing key to a reader. Bulgarian, Turkish and Indonesian carried ~160 each — not scattered, but whole panes: the preferences dialog, the retention presets, the cron descriptions, the about box, the not-found and route-error pages, and every day of the week.

Bugs found and fixed on the way

  • The weekday row. mon and thu were untranslated in 11 of 14 locales, sitting between correctly translated neighbours. French had "Mai" for Tuesday — the month of May. German had "Sonne" for Sunday — the star. Spanish had "Sol". Every date picker showed this.
  • Nine Ukrainian strings had stripped every <Trans> tag. The demo banner lost its "install MySpeed" link; the Ookla consent line lost all three of its links while still saying the reader accepts them; the test summaries lost every bold figure.
  • Four Ukrainian notification templates had lost their timestamp, showing %ping% мс, %download% Мбіт/с with %year%-%month%-%day% %hour%:%minute% dropped.
  • Brand names that were handed to translators and got translated: ntfy became "nackt" (de) and "kant-en-klare" (nl); Discord became "Discórdia" (pt).
  • Words translated in the wrong sense: Chinese titled the ping panel "平" ("flat"); French called InfluxDB's bucket a "Seau" (a pail); Russian and Bulgarian rendered the time preposition "At" as "on the website" and "at the address"; Turkish read the Export/Import buttons as foreign trade.

What stops it recurring

germanLocale.test.js held German alone to parity — correctly, since the others came through Crowdin and would have failed on hundreds of strings. They have all been round now, so tests/client/localeParity.test.js holds all fourteen. A new key in en.json now fails in fourteen languages at once.

Two checks the old test lacked, both of which found live faults: interpolation parity ({{name}}) and <Trans> tag parity. The old test's untranslated check only looked at values containing a space, which is why "Ping", "Server" and "Tests" sat in a dozen files unnoticed.

scripts/i18n-audit.js reports the gaps; scripts/i18n-apply.js writes a patch back in English's key order so a translation pass diffs as pure insertions.

Reviewer note

bg, uk, id and tr have not been reviewed by a native speaker — they were also the emptiest files, so they carry the most new text. The other ten build on substantial existing translation.

Crowdin is upstream-only (gnmyt/MySpeed), so these translations live in this fork and will want re-applying after an upstream merge.


4,138 tests pass, lint clean, vite build succeeds.

🤖 Generated with Claude Code

i7Gamer and others added 6 commits August 24, 2026 19:34
…afely

Every locale but German comes from upstream's Crowdin project, which exports
only what has been translated: a language sits at 434 of English's 599 keys and
nothing anywhere says so. i18next falls back to English, so the gap is invisible
from inside the running interface and only shows by comparing the files.

The half that does not show in a key count at all is a value that is present and
still holds the English text - Crowdin writes those when a string was approved
unchanged, and three locales carry about a hundred and sixty each. To a reader
they are indistinguishable from a missing key.

localeGaps.js is the pure half, kept apart from the scripts for the same reason
registrySource.js is: a test can import it without a readdirSync happening on
the way in. The writer follows English's key order, which every Crowdin-managed
locale is already in, so a translation pass diffs as the lines it added.

The shared-value registry is what stops the report crying wolf over "Mbps". It
is split in two because the naive version is wrong: brand names and placeholder
URLs really are invariant, but units are not - "ms" is "мс" in Russian and "毫秒"
in Chinese, "Mbps" is "Mbit/s" across much of Europe - and the notification
templates carry those units in among their %variables%.

Handing an invariant string to a translator is how it stops being invariant:
de and nl name the ntfy integration "nackt" and "kant-en-klare", pt renders
Discord as "Discórdia", and uk dropped the "<uuid>" that showed the healthchecks
URL's shape. Those are on the list as the values they should have been.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 165 strings each of these was missing - the integrations, the test detail
pane, the statistics overview, the live status line, the setup-token and quiet
hours dialogs - plus the values that were sitting in the file still holding
their English text.

Each was translated against the vocabulary already in its own file rather than
from the English alone, so "Download" stays "Téléchargement" in French and
"Download" in Portuguese, and a metric keeps the name its neighbours gave it.
Where a language really does use the English word - "Ping", "Jitter",
"Bufferbloat", "Interface", "Port" - the English is written and the key will be
registered as shared rather than left to look like an oversight.

Three fixes fall out of doing the pass, all of them visible:

  - the weekday row. "Mon" and "Thu" were untranslated in every one of these
    locales, sitting between correctly translated neighbours; French had "Mai"
    for Tuesday, which is the month of May; Spanish and Italian had "Sol" and
    "Sun" for Sunday, one of which is the star.
  - Portuguese named the Discord integration "Discórdia", and Dutch named ntfy
    "kant-en-klare". Both are brand names that were handed to a translator.
  - the Dutch example hostnames pointed at ".nt", which is not a domain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same 165 strings again, plus each locale's own backlog of values that were
present and still English. Turkish carried by far the most: 327 strings in all,
because two thirds of its file had never been past a translator - the whole
preferences pane, the not-found and route-error pages, the retention presets,
the cron descriptions and the entire calendar were English inside a Turkish
interface.

Where a language writes its own unit, the new strings use it: Russian gets
"мс" and "Мбит/с" throughout, including in the notification templates, which
Ukrainian had already localised and the others had not. Chinese gets "毫秒".
The three that use the English symbols keep them.

Two more mistranslations fixed on the way past. Chinese titled the ping panel
"平" - the character for "flat", which is what a translator reaches for when
"Ping" arrives with no context - while every neighbouring string called it
"ping 延迟". Russian named its export section "Speedtests" in English between
two Cyrillic siblings.

The weekday row is now complete and correct in every language that has one:
Turkish had none of the seven, Russian and Danish were missing Monday and
Thursday.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last two locales carrying the large untranslated block: about 320 strings
each, the 165 that were missing outright plus roughly 160 that were sitting in
the file still in English. As in Turkish, that block was not scattered - it was
whole panes. The preferences dialog, the retention presets, the cron
descriptions, the about box, the not-found and route-error pages, most of the
ntfy and influxdb fields, and every day of the week.

Both files now read in their own language throughout, and both weekday rows are
written out in full: Пон-Нед and Sen-Min.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
388 strings - the largest single gap in the tree. Ukrainian shipped 234 of
English's 599 keys, so nearly two thirds of the interface fell back to English:
the whole preferences and optimal-values dialogs, the settings pane, the
statistics page, the date picker, every day of the week, the not-found and
route-error pages, and the influxdb and ntfy integrations entire.

Units follow what the file already used - "мс" and "Мбіт/с" - which the
notification templates here had localised long before the rest.

Four of those templates were also truncated: discord, gotify, pushover and
telegram each showed "%ping% мс, %download% Мбіт/с" with the leading
"%year%-%month%-%day% %hour%:%minute%" dropped, and the error templates had
lost the bracketed timestamp too, so the hint no longer described the message
it was hinting at. The healthchecks URL placeholder had lost its "<uuid>" and
read as a bare hostname.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
germanLocale.test.js asserted that de.json translated every English key and
left no value as the English sentence. Only German, because the rest came
through Crowdin and would have failed on hundreds of strings - a normal state
for them rather than a broken build.

They have all been round now, so the floor is the same for all fourteen and the
interesting question changes: not "how far behind is this language" but "did the
string somebody just added get translated". A new key in en.json now fails in
fourteen languages at once, which is the point - the alternative is what this
replaces, where four German strings went missing for a release.

Two checks the German test did not have, both of which found live faults:

  - the interpolations. i18next fills {{name}} from what the component passes,
    so a translation that drops one renders "Average latency, between and".
  - the <Trans> tags. They bind to real React children by name, so one the
    translation does not carry takes its content with it. Nine Ukrainian strings
    had stripped every tag: the demo banner lost its "install MySpeed" link, the
    Ookla consent line lost all three of its links while still saying the reader
    accepts them, and the test summaries lost every bold figure.

The old test's third check only looked at values containing a space, which is
why "Ping", "Server" and "Tests" sat in a dozen files unnoticed. This one has no
such hole, so the allowance list is explicit - and asserted to be live, so an
entry for a string that has since been translated fails rather than rotting.

Also corrects fourteen values translated in the wrong sense: German named Sunday
"Sonne", the star; French called InfluxDB's bucket a "Seau", a pail; Russian and
Bulgarian rendered the time preposition "At" as "on the website" and "at the
address"; Turkish read the Export and Import buttons as foreign trade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@i7Gamer
i7Gamer merged commit 2448859 into development Aug 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant