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

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, so
choosing a language deliberately throws it away and builds it again. The
remount re-read the settings file — which still held the old language,
because the write is debounced by three seconds — and applied it, putting
the old language back. The click that appeared to work was the one made
after the previous write had landed, which is exactly the "sometimes"
in the report.

Worse than it looked: the queued push survived the remount, so the file
was eventually written with the new language while the screen showed the
old one. A reload then changed the language on its own.

Fixed twice over, because either alone leaves a race. The file is read
once per account per page load rather than once per mount, and hydrate
now holds back any key with a change still queued — a change that has
not been written up is newer than the file by definition. That rule is
`mergeRemote`, pulled out as a pure function so it could be tested
without a JMAP client.

**Both browsers kept offering to translate an English page.** They were
right to: `<html lang>` said English while the visible text was 6,289
message rows of marketing copy and brand names in whatever language the
sender wrote in. The list is most of the text on the screen, so that is
what the detector was reading.

Sender, subject and preview in the list, and the thread subject and
sender name in the reader, are now marked as what they are — content,
not interface. Message bodies were already marked, so this is the same
line drawn in the places the earlier pass missed rather than a new one.
Whether it silences the prompt is Chrome's call and cannot be checked
from inside the page; the marking is right either way.
This commit is contained in:
2026-08-31 13:45:18 -07:00
parent a4f7d386a6
commit 51dabd9cab
7 changed files with 117 additions and 13 deletions
+7 -2
View File
@@ -18,7 +18,7 @@ import { MailView } from "@/views/mail/MailView";
import { ComposerDock } from "@/views/compose/ComposerDock";
import { setUnreadBadge } from "@/lib/notify";
import { useSettings, syncedPart } from "@/store/settings";
import { armSettingsSync, loadRemoteSettings, queueSettingsPush, settingsSyncAvailable } from "@/lib/settingsSync";
import { armSettingsSync, loadRemoteSettings, queueSettingsPush, settingsAlreadyLoadedFor, settingsSyncAvailable } from "@/lib/settingsSync";
import { listenForVerification, renewWebPush } from "@/lib/webpushEnable";
import { useLanguageVersion } from "@/lib/i18n";
@@ -68,8 +68,13 @@ function AuthedApp() {
// Settings that live with the account rather than the browser. The cached
// ones have already painted, so this only has to correct them (issue #54).
//
// Once per account, not once per mount: this subtree is keyed on the
// language version, so picking a language throws it away and builds it
// again. Re-reading the settings file there would apply a copy written
// before the change and undo it.
useEffect(() => {
if (!accountId) return;
if (settingsAlreadyLoadedFor(accountId)) return;
let cancelled = false;
void (async () => {
const remote = await loadRemoteSettings();