ADR 0035: Surface the OS locale through the preload bridge
- Status: Accepted
- Date: 2026-07-30
- Related: 04-ux/06-settings-ia · 04-ux/02-i18n-english-first · 04-e2e-test-plan · E2E-091
- Updates: the
window.piDesktoppreload contract in api.ts
Context
The Settings → Basics → Language control offers an Auto option that should follow the user's OS display language. The original implementation resolved "auto" from the renderer's navigator.language.
In Electron, the renderer's navigator.language is the embedded browser locale and defaults to en-US regardless of the actual OS language. On a Chinese system the renderer still reports en-US, so "Auto" resolved to English even though the OS was Simplified Chinese. The same default also seeded the initial i18n language on first paint.
The reliable source is the main process: app.getLocale() reads the OS display language (macOS AppleLanguages, Windows user UI language, Linux LANG). The main process already owns OS-level facts; the renderer only needed a way to read them synchronously before first paint.
Decision
- Expose the authoritative OS locale from the preload bridge as a synchronous field
window.piDesktop.locale. The main process resolvesapp.getLocale()while creating the window and passes it throughwebPreferences.additionalArguments; sandboxed preload code reads that argument rather than importing Electron's main-onlyappmodule (alongside the existingplatformfield). - Add optional
locale?: stringto thewindow.piDesktoptype inapi.ts; non-Electron contexts do not receive a window creation argument. - Resolve "auto" language through a new
resolveOsLocale()helper inlib/app-language.tsthat preferswindow.piDesktop.localeand falls back tonavigator.language/userLanguagefor non-Electron contexts (e.g. tests). - Seed the initial i18n
lnginmain.tsxfromresolveOsLocale()instead ofnavigator.language.
This keeps OS-fact resolution in the main process and requires no new IPC channel (the value is available before first paint, like platform).
Consequences
- "Auto" language now matches the real OS display language (e.g. Simplified Chinese on a Chinese system), and the Auto card shows the detected language inline ("当前:简体中文").
- Initial i18n language on first paint is correct for the OS locale.
- The
window.piDesktoppreload contract grows one read-only string field. - Non-Electron contexts (unit tests, potential web builds) fall back to
navigator.languageand remain functional.
Alternatives
Keep using navigator.language
Rejected: it is the root cause of the misdetection and cannot see the OS language from the renderer in Electron.
Add an async IPC call for the OS locale
Rejected: an async round-trip would delay first paint of the correct language and complicate the synchronous pre-paint setup that already uses platform. Surfacing the value synchronously from the bridge is simpler and sufficient.
References
apps/desktop/electron/main/index.ts(passesapp.getLocale()throughadditionalArguments)apps/desktop/electron/preload/index.ts(reads the locale argument without importingapp)apps/desktop/src/lib/api.ts(window.piDesktop.localetype)apps/desktop/src/lib/app-language.ts(resolveOsLocale,resolveAppLanguage)apps/desktop/src/main.tsx(initiallngfromresolveOsLocale)apps/desktop/src/pages/SettingsPage.tsx(Auto card shows detected language)docs/spec/06-delivery/04-e2e-test-plan.md(E2E-091 added)