Persistent supports the same ordered identities as Little Launcher's Shared/SupportedLanguages.cs: en-US, en-CA, es-ES, fr-FR, fr-CA, de-DE, zh-CN, pt-BR, and ja-JP. Canadian English currently shares US wording; Canadian French shares French wording. Their identities and regional formatting remain distinct. Each device preference is considered in order, trying an exact identity and then a compatible language/script family. Unsupported languages fall back to English. Traditional Chinese never selects Simplified Chinese.
packages/shared/src/locales.ts is the canonical typed contract. LocalePreferences stores languageOverride: Locale | null and formattingLocaleOverride: string | null in the existing device settings. Null means Match device. A language change does not change the account time zone, schedule interpretation, saved reminder content, or an explicitly chosen 12/24-hour format. Regional formatting follows the first valid device locale unless separately overridden.
NativeLocale is a version 1, additive display-only payload with the preferences plus resolvedLocale, formattingLocale, timeFormat, and timeFormatChosen. Native hosts persist it for background, offline, and restart presentation. NativeLanguageState is the version 1 read/event snapshot containing languageOverride: Locale | null and ordered deviceLanguages: string[]. New Android hosts expose getLocale({ fallbackLanguageOverride }), emit localeChanged, and accept the existing additive setLocale({ locale: NativeLocale }) mirror. The web subscribes before reading and fences older reads after native events or manual choices. A slow or temporarily failed supported read leaves the web usable without publishing stale local storage; failed reads retry. Only the explicit unsupported-method response enables the older-host fallback. Presentation synchronization does not gate authentication or alarm readiness.
On Android 13 and later, the OS per-app language picker is authoritative. The in-app selector writes the same OS application locale list, including an empty list for Match device; a later OS choice overrides the private mirror. On upgrade, a prior private/web override migrates only once and only if no OS choice exists. Earlier Android versions import a prior web choice once only when no native mirror exists, then retain the private per-device preference, including an explicit reset to Match device. The generated Android manifest declares all nine locales through localeConfig; Windows MSIX declares the same supported identities. Native sources live under apps/mobile/android-plugin, android-tests, and scripts/setup-android.mjs, rather than the generated android directory.
Unless explicitly chosen, the clock follows the regional formatting locale's 12/24-hour convention on web and native surfaces. This includes an independent regional override, rather than Android's unrelated system clock switch. An explicit saved 12/24-hour choice always wins. Calendar and numeral extensions are presentation only; Gregorian date arithmetic, schedule parsing, and stored ISO instants retain their existing rules. Changing language or region updates mounted dates and widget snapshots without resetting drafts, selected units, sounds, or confirmation state.
Device requests carry X-Persistent-Locale; web apiFetch sends it only to the app origin with normal credentials, never to external promo requests or requests using credentials: omit. Older clients can negotiate with weighted Accept-Language, excluding q=0. Native background requests resolve the stored override against current device preferences. Push registration optionally supplies locale. FCM escalation data adds locale and bodyRange, a decimal start:length range measured in UTF-16 code units within the localized legacy body. The server computes this range from the trusted template placeholder, never by searching user content. An empty base body uses 0:0. New Android clients validate bounds and surrogate boundaries, extract the literal substring, and render the escalation marker in their current language. Missing or invalid ranges retain the legacy body verbatim. FCM never duplicates baseBody: its 4096-byte limit requires one copy of user content plus bounded presentation metadata. Existing independently oversized content is neither truncated nor replaced with a sync-only message. Alarm projections, which have no FCM size cap, retain bodyTemplate: 'escalated' and baseBody. These markers support a closed bridge, offline display and restart. Unmarked reminder bodies remain literal. Existing identifiers, instants, authorization and notification channel identifiers are unchanged; older clients keep their existing fields.
Account email language is a separate explicit emailLanguage: Locale preference, defaulting to en-US for existing accounts. PATCH /api/auth/preferences validates AccountLanguageInput, scopes the update to the authenticated account, and returns its current public profile. Requesting a verification code accepts an optional device locale for that message and does not overwrite an existing account preference. Share, assignment, and escalation messages use the recipient account's email language when known. Unknown recipients receive English. The sender's language is never assumed to be the recipient's language. Recipient lookup is internal to delivery, uses the normalized exact destination address, and does not disclose whether an address belongs to an account. The migration adds only User.emailLanguage and PushSubscription.locale, both with English defaults.
API errors preserve the existing English error field. Optional code, localized message, and parameters provide an additive representation for newer clients. Stable codes identify application errors; raw database/provider messages must never be exposed as translations.
Application wording is translated. Reminder titles, details, checklist items, names, addresses, medicine names, doses, quantities, units, and tone titles remain unchanged. Translation completeness and placeholders must be checked against the English catalog. Automated checks do not constitute native-speaker language review. Store listing publication and production rollout are coordinated separately; the web/Android release preparation and nine-locale Play metadata are documented in apps/mobile/store/release-0.29.0.md. Legal pages remain canonical English, with no claim of translated legal or native-speaker review.
The shared package contains pure locale helpers and trusted bundled catalogs, with zod as its only runtime dependency. The web uses i18next and react-i18next outside that package; all catalogs are bundled for offline use. Native hosts load the same JSON catalogs, retain English fallback on missing or corrupt resources, and log safe packaging diagnostics. Canadian aliases do not duplicate the catalog. Catalog tests enforce identical keys, interpolation names and rich-text slots, reject encoded JSX entities, and audit literal translation calls. Interpolated values are replaced once and rendered as text. Translation functions receive catalogued application keys, never arbitrary provider/database errors or user content.
Installed Windows shell descriptions, widget-provider labels, widget-picker descriptions and accessible preview text use package-relative ms-resource:Resources/... bindings. A focused .NET generator reads the canonical catalogs and native alias contract to create all nine RESW identities during packaging. These are generated output, not another maintained translation set. After the Store or dev identity is finalized, package-resources.ps1 indexes exactly one completed host PRI plus these nine RESWs into package-root resources.pri. Its primary resource map uses the final manifest identity. The supported PRI indexer drops the previous top-level map while preserving every host resource name, candidate, qualifier and value, including embedded XAML. No loose XBF files or additional framework PRIs enter this indexing pass. Language splitting, AutoMerge and reverse mapping are disabled.
The standalone Persistent.Desktop.pri remains present and unchanged by packaging. Manifest-only strings are no longer injected into that host index. Packaged WinUI uses the default resource manager's primary map, so a metadata-only root index would be insufficient: the merged package index must also retain every original host resource. This applies when the dev identity and host map share a name as well as when the Store identity differs. Installed shell metadata follows Windows resource language qualification; live host and widget content follow the per-device application choice.
The normal desktop launcher still needs only Bash and Python 3. Before invoking the unchanged shared bridge, it prepares byte-verified catalog inputs under ignored apps/desktop/build-inputs/localization. Directory.Build.props selects the canonical source directory in a full checkout and that generated directory in the ordinary narrow source_directory: apps/desktop snapshot. Desktop, widget and native resource generation consume this one resolver. The helper does not need a broader repository snapshot, npm, an alternate checkout, or an independent injection of packages into its stage.
Bundled promotional copy uses a local exact-source adapter on web and Windows. Known headings, buttons and blurbs translate through the existing catalog; product/brand names, links, icons, platform identifiers and order are unchanged. Unfamiliar remote copy remains verbatim English. The external promo submodule is unchanged, and its original refresh/cache/offline path remains responsible for loading the catalog.
Changing the interface language immediately reposts already-visible Windows notifications in the new language. The same occurrence identifiers, alarm scenario, action authorization, and two-tap Done confirmation remain in use. Windows can restart alarm audio and reset an unsubmitted snooze dropdown selection during this replacement; this is the explicitly chosen behavior. Independent alarms remain separate.
Run devkit run npm run validate, npm run build:linux, devkit run npm run build --workspace @persistent/web, and npm run android:verify in the documented Android build environment. Windows requires both the real npm run verify:desktop bridge build and an agreed npm run install:desktop runtime window. The portable C# source check and dotnet run --project apps/desktop/tools/localization-tests supplement that build; the latter requires an isolated PERSISTENT_LOCALIZATION_TEST_PROFILE directory equal to XDG_CONFIG_HOME on Linux so it cannot write real preferences.
Focused tests cover script-aware ordered matching, weighted HTTP negotiation, catalog/placeholder completeness, literal medicine content, localized errors and emails, same-origin headers, region-only rerendering, grammar, actual translated duration/snooze controls, and slow/event-first native bridge hydration. Windows source-level regressions exercise the actual gated presentation owner: immediate relabel, armed confirmation, independent alarms, failed-post retry, dismissal racing relabel and session departure. Generator tests compare every native resource against its canonical catalog, including both Canadian aliases, and reject missing translations without partial output. Package fixtures cover Store and dev identities, missing root indexes, wrong authorities/maps, keys present only in secondary maps, missing languages/defaults, and changed or absent host XAML bytes, paths and qualifiers. They also reject loose XBFs or additional framework PRIs in the isolated merge inputs. A standard-launcher boundary regression checks fresh and changed canonical inputs, hashes, bounded stale-input pruning and narrow snapshot isolation. Android JVM tests cover locale matching and OS/private override precedence. These tests supplement, rather than establish, OS delivery guarantees.
Browser acceptance used an isolated local PostgreSQL database and synthetic accounts only. Japanese sign-in (including the provider button), German and Simplified Chinese settings, independent French Canadian email preference, and the German editor were observed in shared Linux Chromium at a 390-pixel viewport. The editor wraps expanded footer buttons and scrolls its tab row without horizontal form overflow. Runtime API checks confirmed account/device separation, login preservation, ownership-scoped preferences and reminder access, additive localized errors, and unchanged alarm identifiers, times, actions and medicine text across language projections. No real email or push was sent.
The initial home-server npm run verify:desktop invocation could not delegate because this exact workspace was absent from the remote source mount. Ryan then explicitly approved disposable staging of this uncommitted patch on windows-dev for build-only acceptance. A task-local transport reused the unchanged common snapshot_source and build-staged.ps1, using only the normal prepared desktop tree under unique current-user C:\TEMP\WindowsAppBuilds stages. Snapshot and returned artifact hashes were verified, including generated catalogs against canonical sources. Real ARM64 Debug WinUI/XAML compilation and unsigned ARM64 Release MSIX packaging succeeded with tracked version 0.5.8 unchanged and package version 0.5.8.0. Those runs established compilation and narrow-source preparation. Their check that another PRI contained four strings in nine languages did not establish package-level manifest resolution and is superseded by the packaging regression described below. Historical logs, snapshot manifests, package hashes, compiled resource dumps and stage cleanup proof remain under .artifacts/windows-build; .artifacts/localization/README.md indexes those runs.
Microsoft Store rejected the subsequent Windows 0.6.0 upload because its absolute manifest reference targeted the assembly map and the package contained no root resources.pri. Dumps of both rejected architectures confirmed the keys existed only in Persistent.Desktop.pri. An isolated native MRT lookup succeeded for that assembly authority and failed for the Store identity. Packaging now checks the actual finished MSIX after unpacking it: the root index must have the final identity as its primary map, all 36 canonical metadata values with an English default, no AutoMerge/reverse mapping, and every original host candidate preserved in that primary map. The standalone host PRI hash must remain unchanged. This gate runs before signing or bundling and fails packaging if manifest resources cannot resolve through the correct package map.
The corrected normal narrow-source ARM64 Store package 0.6.1.0 passed real Windows SDK indexing, packaging and unpacked-package validation. Both checks found all 36 canonical metadata values and all 17 original host resources, including eight embedded app XBF resources, in the primary Store-identity map. The standalone host PRI remained unchanged. A headless native Microsoft MRT probe against the returned package resolved all 36 language-specific strings, four unsupported it-IT lookups to English, and all eight embedded XBF resources through empty-authority default-map URIs with byte hashes matching the standalone host index. A second bounded check finalized Identity.Name=Persistent.Desktop in a disposable unpacked layout and used the same packaging helper and exact prepared catalogs. Actual SDK indexing and all 48 native lookups also passed for that dev identity; this was not a signed dev package or installation. No app installation, launch or OS language change occurred. Build and probe stages were cleaned after their processes finished. The coordinator retains snapshot/hash, SDK log, package and native probe evidence under .artifacts/windows-build/20261021T230911Z-cddfa6bf-package, .artifacts/windows-release-0.6.1/native-probes/Persistent.Desktop_0.6.1_ARM64/ and .artifacts/windows-release-0.6.1/dev-identity/. These initial results establish the corrected ARM64 package resource contract and both identity modes, not installed startup or Store certification.
The subsequent same-source x64 0.6.1.0 package passed the same real WinUI/widget build, SDK merge, finished-MSIX validation and 48 native MRT checks. Both architecture snapshots match each other and the current prepared desktop source byte for byte. The x64 evidence is retained under .artifacts/windows-build/20261021T231556Z-79d9bbda-package and .artifacts/windows-release-0.6.1/native-probes/Persistent.Desktop_0.6.1_x64/. This adds x64 package/resource qualification; installed startup and Store certification remain separate.
The root-index requirements follow Microsoft's Desktop Bridge manifest-resource tests. The preservation behavior follows the documented PRI indexer, rather than renaming the assembly index or indexing the entire published directory again.
Portable C# checks supplement the real build, but neither compilation nor unsigned packaging proves startup or visible Windows behavior. Physical Android per-app picker, offline/reboot alarms, channel labels and native audio, Windows install/widgets/toasts, and Linux OS tray/notification behavior still need runtime acceptance on their respective hosts. No physical installs or shared desktop interaction occurred. The Windows SDK's first-use command created one untrusted ASP.NET development HTTPS certificate; its exact creation time, thumbprint and purpose matched the task's SDK output, and only that new certificate and its key were removed. Subsequent build processes disabled first-use certificate generation. No signing key was copied or trusted. Native-speaker translation review remains outstanding. All-language offline bundling increases the web bundle and retains Vite's existing large-chunk warning. The Progress dashboard was unavailable in this workspace; its plugin was not installed or Paseo was unreachable, and no replacement tracker was created.