i18n: complete all 32 locales and fix the defects shipping in every language - #936
Merged
Conversation
Every non-English locale was missing the exact same 292 keys, so canvas, tag hub, project hub, diagnostics, the inbox review reminder and the account-vault surfaces fell back to English in 31 languages. Each locale was translated and then reviewed by a separate native pass, and machine-validated: placeholders preserved, ICU single-brace only, and plural blocks carrying every CLDR category the locale actually requires (so ru/pl/cs/ar get few/many rather than a copy of English's two forms). pnpm i18n:check now reports 0 missing keys across all 32 locales.
setup.linking.vaultRow and home.widget.resizeAria used i18next double-brace
under the ICU formatter. IntlMessageFormat throws on {{count}}, IcuFormatter
catches it and returns the raw template, so the vault picker showed the
literal 'Vault — {{count}} items · {{date}}' in every locale, English
included. The guard test allowlisted both keys, so CI stayed green.
Hebrew home.widget.startsIn hit the same silent failure a different way: the
ASCII apostrophe in דק' is ICU's escape character, so it quoted away the
plural block's closing brace. Replaced with the Hebrew geresh ׳, which is
also the correct punctuation for the abbreviation.
Dropped the allowlist and added a guard that compiles every locale string
with the parser the formatter actually uses, so the whole class is covered
rather than just double braces. Verified it fails when the Hebrew bug is
reintroduced.
Repo-wide scan is now clean: 0 double-brace, 0 ICU parse failures,
0 placeholder drift across all 32 locales.
Pure helpers under lib/ format dates and times with Intl but had no way to reach the language the user picked, so they hardcoded 'en-US' or passed undefined (which follows the OS locale, not the app's). Adds getActiveLocale(), kept in sync from i18next's languageChanged event rather than from each call site, so settings, onboarding and a locale synced from another device all update it. Mirrors the existing setDateFormatPref pattern in format-date.ts.
1779 values across the 31 locales were byte-identical to the English source
— the whole Agent Chat / Agent MCP / Agent Providers settings surface had
never been translated in any language, plus scattered leftovers elsewhere.
1304 are now translated. The remaining 475 are deliberately unchanged and
each was justified per locale: product names (Google Calendar, LM Studio,
Ollama), technical literals (OAuth 2.0, PNG/JPEG/GIF/WebP/SVG, A4
(210 x 297 mm), the dateDiff() formula sample), pure placeholder
arrangements with no words ('{type}: {title}', '{count} {label}'), and unit
abbreviations that are genuinely identical in the target language ('min' in
Czech, Finnish, French, Spanish).
Applied with a re-check that refused to overwrite any value that was no
longer identical to English; 0 refusals.
…ow needs getMainI18n() threw when a translation was requested before boot installed the real instance. With main-process copy now going through i18n, that turns an IPC error the user is meant to read into an internal initialization message. It now falls back to a synchronous English-only instance and reports the boot-order problem to the log instead. Also lands the 453 new English keys the localisation pass introduced across menu, errors, system, common, notes, tasks and settings.
…ale on first run The main process shipped English regardless of the selected language: - both browser-extension pairing consent dialogs, including their Allow/Deny buttons, and the Windows About dialog's version line - Electron role menu items with no explicit label, which render Electron's own English defaults next to translated siblings (View zoom items, Edit paste-and-match/delete, macOS services/hide/unhide, Window submenu) - every user-facing Google Calendar OAuth error, the device-registration rollback message, sync network errors, and the user-visible IPC handler errors Startup locale: a fresh install always began in English even though app.getLocale() was already read and handed to telemetry. It is now detected and mapped onto a supported locale (de-AT→de, pt-BR→pt, zh-Hans→zh-CN, …), gated on a real first-run signal — no current vault AND an empty vault registry — so an existing install can never flip language on update, and persisted immediately so it stays a one-time decision. A locale synced from another device was written to disk but never applied at runtime, leaving the UI, the native menu and activeLocale stale until restart; it now goes through the same path as a local language change.
…locale-aware
Hardcoded English the key scanner could not see, because it was in props,
toasts and plain .ts data modules rather than JSX text: the whole keyboard
shortcuts dialog (7 groups, ~90 descriptions), task and kanban empty states,
reminder and snooze presets, priority labels, due-date filter options,
project validation errors, bulk-action/drag/subtask toasts, and the
save-filter summary — which was concatenated from English fragments and is
now one ICU message per clause so other languages can reorder it.
Counts that were pluralized by hand (`${n} task${n === 1 ? '' : 's'}`) are
now single ICU plural messages.
Dates and times were formatted with a hardcoded 'en-US', or with no locale
at all — which silently follows the OS, not the language the user picked.
They now use getActiveLocale(): due dates across Tasks, the calendar hour
gutter (which built 'AM'/'PM' by hand), the repeat dialog's weekday names,
the date picker, the capture heatmap and canvas cards. Date autocomplete now
matches localized weekday and month names while still accepting English.
Built-in names that originate in packages/contracts — folder-view column
names, status categories, the default view name — are translated at the
display layer, leaving the persisted identifiers untouched.
The first-run tour's own copy was localized but driver.js's Next/Previous/
Done chrome was not; it is now passed through, keeping driver.js's own
double-brace template syntax away from the ICU formatter.
… translate them Importer errors and warnings were built as English sentences inside @memry/importers and rendered verbatim in the import dialog. The package cannot depend on the desktop i18n runtime, so each message now carries a stable code plus the values it interpolates, and keeps its English text as the message. The renderer maps code → i18n key at display time and falls back to the raw message when the code is missing or unknown, so an importer that has not been migrated still renders exactly as before. ImportPreviewMessage is `string | ImportMessage`, so preview payloads written by an older build keep parsing.
…ales Covers the strings the localisation pass just pulled out of hardcoded English: the native pairing dialogs and their OS buttons, Electron menu items that were rendering Electron's own English defaults, IPC error copy, the keyboard shortcuts dialog, empty states, reminder and snooze presets, priority and due-date filter labels, relative dates, and import warnings. Menu items and dialog buttons follow each platform's own wording in that language rather than a literal translation of the English. Plural blocks carry every CLDR category the locale requires — Arabic gets all six with proper dual forms, Czech and the Slavic locales get few/many. Machine-validated per locale: placeholders preserved, ICU single-brace only, every plural block compiles and covers its locale's categories.
The application menu was machine-translated and 179 entries were wrong — not
stylistically different, wrong. A native reviewer per locale checked every
entry against the wording that language's macOS and Windows menus actually
use.
The worst were not subtle:
- Arabic Quit read استقال, "he resigned"; German "Hör auf" ("stop it!");
Japanese やめる ("give up"); Polish "Przestań" ("cut it out");
Hebrew עזוב ("forget it"); Romanian "Renunță" ("give up")
- the Indonesian File menu read "Mengajukan" — "to submit a claim", the
verb sense of "to file"
- a Vietnamese value contained a literal newline and would have rendered as
a broken menu item
- Danish Paste was "Indsæt" (Insert), colliding with the Insert menu in the
same menu bar
- several locales had Quit and Exit swapped or identical, losing the
macOS/Windows distinction
Also fixes a recurring grammatical error: menu-bar titles are nouns in
Czech, Polish, Romanian, Hebrew and Indonesian (Edycja, Úpravy, Editare),
but had been translated as second-person imperatives — the app ordering the
user to edit.
Every replacement was checked for placeholder parity and ICU validity before
being written; 0 refusals.
…n English An earlier sweep only looked at strings of two words or more, so every single-word leftover slipped through — 2078 entries across the 31 locales, concentrated in the Agent Chat composer and the Agent MCP / Agent Providers settings panels. Those panels rendered their own buttons and status badges in English inside an otherwise translated interface: Save, Off, Access, Tools, Permissions, Connection, Preset, Custom, Model, Disconnected. 716 are now translated. The remaining 1362 stay English deliberately and were justified per locale: brand and vendor names (Claude, Codex, OpenAI, Ollama, Todoist), font names (Geist, Gelasio), acronyms the language uses unchanged (CRDT, URL, API, PDF, HTML, CSV, MCP), and the YYYY-MM-DD format pattern. Applied with a re-check that refused to overwrite any value no longer identical to English; 0 refusals.
…ments
Adds the English for the confirmation dialogs, filter surfaces, import
status catalog and agent errors that the localisation pass rebuilt, and
removes 30 keys that only ever held the tail of a split HTML entity
('ldquo;', 'rdquo;', 'quot;', 'middot;') plus the sentence fragments that
were concatenated around them — 1056 dead entries across the 32 locales.
Deletion was gated on a repo-wide reference check rather than the agents'
own reports: of the 43 keys reported unused, 13 were still referenced and
were kept. Only the 30 with no remaining call site were removed.
Every importer's ctx.status() line was hardcoded English and rendered straight into the import dialog, so a Turkish or Japanese user watched an English progress log. packages/importers cannot depend on the desktop i18n runtime, so each message now carries a stable code alongside its English text, and the renderer maps code to i18n key at display time. 22 call sites across 13 importers. IMPORT_STATUS_KEYS is typed Record<ImportStatusCode, string>, which was mutation-tested: removing one entry fails typecheck with TS2741, so a status code added later cannot silently ship untranslated. ImportProgressEvent.status and ImportPreviewMessage are 'string | ImportMessage', so a payload produced by an older build still renders, and an unknown code falls back to the raw English message rather than a blank line.
…tences
The i18n codemod cut HTML entities in half. The ampersand stayed in the JSX
and the tail became a translation key, so a delete dialog read:
“Task name” will be permanently deleted.
JSX does not reassemble an entity across an expression boundary — I rendered
it in jsdom to confirm. Users saw the literal “ / ” / " /
· in every locale, English included, across 12 surfaces: the delete
task and subtask dialogs, bulk due-date and priority, duplicate-with-subtasks,
unsaved changes, quick capture, move-to-folder, the formula editor and its
delete confirmation, the command palette, capture input, and sync history.
The same sentences were also assembled from separate translation fragments
with the plural picked by a JS ternary appending "s":
{t('thisWillDelete')}{n}{t('subtask')}{n !== 1 ? 's' : ''}{t('from')}{title}
Word order was frozen to English and no language with real plural rules
could be expressed. Each is now one ICU message with placeholders and a
proper plural block, with real typographic quotes in the English source:
"This will delete {count, plural, one {# subtask} other {# subtasks}} from “{title}”."
Rendered wording is unchanged apart from the entities becoming the
characters they were always meant to be.
- sync/attachments.ts leaked four English sentences to the renderer,
including the server-unreachable message that already had a key
- the folder-view drag handle announced the raw column id to screen readers
('wordCount') while the visible label said 'Word count'
- task priority labels were fabricated with charAt(0).toUpperCase(), and
interactive-priority-badge and inline-priority-popover read them at module
load — before i18n exists — freezing English for the session
- active-filters-bar kept its own duplicate English label tables
- the due-date picker froze its quick options at mount, so Today/Tomorrow
kept the old language after a mid-session switch
- 'Agent runtime is starting' was matched as an English substring to drive
the agent bootstrap retry loop, so localizing it would have broken agent
startup in every non-English locale; the retry now matches a stable code
Adds a placeholder-parity guard: for every locale and key, the ICU
placeholder set must equal English's, and a plural block in English must
stay a plural block. That is the failure the ICU guard cannot see — a
translation that parses cleanly but silently dropped {total}.
Also covers the first-run OS-locale detection, which had no integration
test: the store mock lacked getVaults/setStoredLocale, so detection threw
into its own catch and returned null while all 47 tests still passed. The
existing-install case is now pinned — a user with a vault keeps English.
…31 locales The final 55 keys, most of them the dialog sentences that were previously assembled from fragments in JSX. Because they are now whole ICU messages, these languages can finally order the words naturally and inflect the count instead of inheriting English word order with an appended "s". pnpm i18n:check now reports 0 missing keys across all 32 locales, and the locale scan is clean: 0 double-brace interpolations, 0 ICU parse failures, 0 placeholder drift.
Settings › Language & Region now covers what actually changed for users: a fresh install picks its language from the OS (with regional variants mapped onto the closest supported locale), an existing install is never re-detected, a language change from another device applies without a restart, and dates and weekday names follow the selected language rather than the operating system. Also notes that import progress and warnings are localized.
Conflict in sync/http-client.ts: main added a distinct message for a timed-out sync request (#931) while this branch localized the generic unreachable case. Kept both branches and localized both — main's new copy becomes errors:sync.requestTimedOut rather than being folded into the existing sync.networkTimeout key, whose wording gives different guidance.
|
React Doctor found 1 new issue in 1 file · 1 warning · score 83 / 100 (Needs work) · 1 fixed · vs 1 warning
Reviewed by React Doctor for commit |
These keys arrived with main in English only: the dialog asking whether the AI agent may read the user's Google Calendar events, its Settings toggle, and the promote-dialog notice. Adds the merged sync timeout message too. The consent copy draws a subtle distinction — the user keeps seeing their Google events either way, the choice only controls whether the agent can read them — so translators were briefed to preserve that rather than translate word by word. Allow / Don't allow use each platform's own permission-prompt wording. All 32 locales are complete again: 0 missing keys.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…persist-order bug The coverage ratchet caught that several things this branch introduced had no test. Adds 147 tests across seven modules, all behaviour-first and several mutation-verified rather than written to colour lines green: - active-locale: the default must be the fallback locale, and the value must reach Intl — asserted through real formatted output (en January / de Januar / fr janvier), plus every one of the 32 shipped locale ids round-tripping through Intl without throwing - reminder-presets, tasks-data: the lazy label getters re-resolve per access, pinned by flipping the language between two reads. Eager evaluation was the original bug; this stops it coming back - oauth-errors: the full Google Calendar failure mapping, including that six distinct causes map to six distinct messages — a collapse line coverage cannot see - the synced-locale apply path: drives the real locale-handler rather than stubbing applyLocale, so it asserts the runtime effects. Verified by deleting the applySyncedLocale call, which turns five tests red - use-import-run: both status shapes flow through unchanged, which is the backward-compatibility guarantee for payloads from an older build - tags-handlers: each failure envelope returns its own message key Fix found while writing them: applyLocale persisted before awaiting changeLanguage, so a locale bundle that failed to load left config.json, the store and the DB holding a language the app never switched to, with activeLocale disagreeing — the same drift this path exists to prevent, on the error branch. Persistence now happens only after the switch succeeds. The fix was mutation-tested: reverting the order was initially caught by nothing, so a test now pins it.
use-bulk-actions fires the toasts for every bulk task operation, and this
branch replaced its hand-rolled English pluralization with ICU plural
messages. A regression there is invisible in English — "1 task" / "2 tasks"
is what the old code produced too — and only breaks for other languages.
49 tests covering both plural arms of every operation, the failure path of
each, the empty-selection guards, and the undo affordances — invoking the
sonner action and asserting the operation is actually reversed, not just that
a toast appeared. Assertions pin namespace, key and interpolated values as
well as rendered text, so a lost {count} fails.
Mutation-verified. The one worth noting: swapping a key for an
English-identical sibling (phaseI.errors.failedToArchiveTasks ->
phaseI.toasts.failedToArchiveTasks, both render "Failed to archive tasks")
is caught by exactly one of the new tests and was invisible to the
pre-existing suite, which compares rendered strings.
Lines 85.9% -> 98.63%, uncovered statements 41 -> 9.
| <ul className="mt-1 ps-4 list-disc"> | ||
| {g.warnings.map((w, i) => ( | ||
| <li key={i}>{w}</li> | ||
| <li key={i}>{formatImportMessage(w)}</li> |
There was a problem hiding this comment.
React Doctor · react-doctor/no-array-index-as-key (warning)
Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like key={item.id}, not the array index "i".
Fix → Use a stable id from the item, like key={item.id} or key={item.slug}. Index keys break when the list reorders or filters.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes localisation across all 32 locales, and fixes several i18n defects found on the way that were shipping to users in every language — English included.
What users see today that they shouldn't
Raw HTML entities in dialogs. The i18n codemod cut entities in half: the
&stayed in the JSX and the tail (ldquo;) became a translation key. JSX does not reassemble an entity across an expression boundary, so the delete dialog literally read:Confirmed by rendering it in jsdom. 12 surfaces were affected — delete task/subtasks, bulk due-date and priority, duplicate-with-subtasks, unsaved changes, quick capture, move-to-folder, formula editor and its delete confirmation, command palette, capture input, sync history.
A raw template on the vault picker.
settings.setup.linking.vaultRowused i18next double-brace under the ICU formatter.IntlMessageFormatthrows on{{count}},IcuFormattercatches it and returns the template unchanged, so every user in every locale sawVault — {{count}} items · {{date}}while choosing which vault to link. The repo's own guard test allowlisted the key, so CI stayed green.A menu that read like broken software. Arabic Quit was
استقال("he resigned"), GermanHör auf("stop it!"), Japaneseやめる("give up"), the Indonesian File menuMengajukan("to submit a claim"). One Vietnamese value contained a literal newline. Danish Paste wasIndsæt(Insert), colliding with the Insert menu beside it.Coverage
i18n:checkSentences that were assembled from fragments in JSX — with the plural picked by
n !== 1 ? 's' : ''— are now single ICU messages, so word order and number agreement are finally expressible:Plural blocks carry every CLDR category the locale requires: Arabic gets all six with proper dual forms, the Slavic locales get
few/manyinstead of a copy of English's two branches.Behaviour changes
First-run language detection. A new install started in English no matter the system language —
app.getLocale()was read and sent to telemetry but never reached the UI. It now picks the OS language and maps regional variants (de-AT→de,pt-BR→pt,zh-Hans→zh-CN). Gated on a real first-run signal — no current vault and an empty vault registry — so an existing install can never flip language on update. That guarantee is pinned by a test.Locale-aware dates. Dates and times were formatted with a hardcoded
'en-US', or with no locale at all (which follows the OS, not the app). They now follow the selected language, via agetActiveLocale()kept in sync from i18next'slanguageChanged.Synced language now applies. A language change from another device was written to disk but never applied at runtime, leaving the UI, the native menu and
activeLocalestale until restart.Main-process fallback.
getMainI18n()threw before boot installed the real instance, so an IPC error the user was meant to read could surface asmain-process i18n not initialized. It now falls back to English and reports the boot-order problem to the log.Guards added
דק', where the ASCII apostrophe is ICU's escape character and quoted away a plural block's closing brace. Verified by reintroducing the bug.{total}.IMPORT_STATUS_KEYSisRecord<ImportStatusCode, string>; removing an entry fails typecheck with TS2741, so a status code added later cannot silently ship untranslated. Mutation-tested.Verification
Deleting the dead entity-fragment keys was gated on a repo-wide reference check rather than the agents' reports: of 43 keys reported unused, 13 were still referenced and were kept.
Deliberately not done
general.languageis still never enqueued for sync, so the receiving half added here is dormant. Wiring the sending half is a live sync behaviour change and a product call.role: 'close'still owns Ctrl+W over File → Close Tab. Pre-existing, unrelated to localisation.