Stop a language change undoing itself, and stop the translate prompt #160

Closed
opened 2026-08-31 20:45:11 +00:00 by jcoffey-dev · 0 comments
Owner

Two reports, both about the language setting.

Picking a language sometimes took several clicks

The subtree that reads the account's settings file is keyed on the language version — choosing a language deliberately throws the tree away and builds it again, which is how a thousand t() call sites get new strings without each becoming a subscriber.

That remount re-read the settings file. The file still held the old language, because the write is debounced by three seconds. So it applied it, and the click came undone.

click  → local state = ja, push queued (3s)
       → languageVersion++ → AuthedApp remounts
       → effect runs again → reads settings.json (still says "en")
       → hydrate() → uiLanguage = "en"        ← the click, undone
3s     → queued push writes "ja" to the server ← now they disagree

That last line is the part that does not show up as "it didn't work": the queued push survives the remount, so the file ends up saying ja while the screen says English. A reload later changes the language on its own. The click that appeared to work was simply one made after the previous write had landed — hence "sometimes".

Fixed in both places, because either on its own leaves a race:

  • the file is read once per account per page load, not once per mount (settingsAlreadyLoadedFor, cleared by stopSettingsSync so signing out and back in still reads)
  • hydrate holds back any key with a change still queued — a change that has not been written up is newer than the file by definition

The second rule is mergeRemote(current, remote, held), pulled out as a pure function so it is testable without standing up a JMAP client. Four tests cover it and the load guard.

Both browsers kept offering to translate an English page

They were right to. <html lang> said en; the visible text was 6,289 message rows of marketing copy and brand names in whatever language the sender wrote in. The list is the overwhelming majority of text on screen, so that is what the language detector was reading — and it disagreed with the declaration, which is exactly when the prompt appears.

Marked as content rather than interface: sender, subject and preview in the list; subject and sender name in the reader. Message bodies were already marked, so this is the same line drawn where the earlier pass missed it, not a new policy.

Deliberately not done: <meta name="google" content="notranslate"> would silence the prompt outright, but it would also mean nobody can ever browser-translate the UI — and ihasmail has catalogues for nine languages, not for the other ~190. Someone reading in Thai should still be able to machine-translate the chrome.

Whether the marking alone silences the prompt is Chrome's and Firefox's call and cannot be observed from inside the page — the offer lives in browser chrome. The marking is correct either way; if the prompt persists, the meta tag is one line away.

Also left alone: the folder tree, where role folders are translated and custom ones are the user's own words. Mixing the two in one list needs a per-item decision, and the sidebar is a small share of the text next to the message list.

npx tsc --noEmit, 109 node tests, 467 web tests (4 new), npm run build.

Merged 2026-08-31 as coffey-labs/ihasmail@9b7f66a2c0

Rebuilt from: git history, session transcript.

Two reports, both about the language setting. ## Picking a language sometimes took several clicks The subtree that reads the account's settings file is keyed on the language version — choosing a language deliberately throws the tree away and builds it again, which is how a thousand `t()` call sites get new strings without each becoming a subscriber. That remount re-read the settings file. The file still held the *old* language, because the write is debounced by three seconds. So it applied it, and the click came undone. ``` click → local state = ja, push queued (3s) → languageVersion++ → AuthedApp remounts → effect runs again → reads settings.json (still says "en") → hydrate() → uiLanguage = "en" ← the click, undone 3s → queued push writes "ja" to the server ← now they disagree ``` That last line is the part that does not show up as "it didn't work": the queued push survives the remount, so the file ends up saying `ja` while the screen says English. A reload later changes the language on its own. The click that appeared to work was simply one made after the previous write had landed — hence "sometimes". Fixed in both places, because either on its own leaves a race: - the file is read **once per account per page load**, not once per mount (`settingsAlreadyLoadedFor`, cleared by `stopSettingsSync` so signing out and back in still reads) - `hydrate` **holds back any key with a change still queued** — a change that has not been written up is newer than the file by definition The second rule is `mergeRemote(current, remote, held)`, pulled out as a pure function so it is testable without standing up a JMAP client. Four tests cover it and the load guard. ## Both browsers kept offering to translate an English page They were right to. `<html lang>` said `en`; the visible text was 6,289 message rows of marketing copy and brand names in whatever language the sender wrote in. The list is the overwhelming majority of text on screen, so that is what the language detector was reading — and it disagreed with the declaration, which is exactly when the prompt appears. Marked as content rather than interface: sender, subject and preview in the list; subject and sender name in the reader. Message *bodies* were already marked, so this is the same line drawn where the earlier pass missed it, not a new policy. Deliberately **not** done: `<meta name="google" content="notranslate">` would silence the prompt outright, but it would also mean nobody can ever browser-translate the UI — and ihasmail has catalogues for nine languages, not for the other ~190. Someone reading in Thai should still be able to machine-translate the chrome. Whether the marking alone silences the prompt is Chrome's and Firefox's call and cannot be observed from inside the page — the offer lives in browser chrome. The marking is correct either way; if the prompt persists, the meta tag is one line away. Also left alone: the folder tree, where role folders are translated and custom ones are the user's own words. Mixing the two in one list needs a per-item decision, and the sidebar is a small share of the text next to the message list. `npx tsc --noEmit`, 109 node tests, 467 web tests (4 new), `npm run build`. **Merged** 2026-08-31 as coffey-labs/ihasmail@9b7f66a2c031 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.