Cross-app locale preference contract (RP-ML-012)¶
Status: Phase 3 wave 2 — public-core.
Scope: protocol-level propagation contract only. No commercial framing, no
Tower involvement. See strategy docs/i18n/glossary-and-policy.md §5.4 for
the precedence policy this contract implements (sanitized per §7 row 5.4 —
"scope to the propagation contract; do not surface Tower or commercial
framing").
Why this contract exists¶
RigPlane Pro embeds the Core web UI either via a Tauri WebView, an
external browser window, or a managed-station kiosk surface. When Pro
holds a user-explicit locale preference (set in the Pro Settings UI),
the embedded Core UI must honor that preference instead of falling back
to the browser locale. Conversely, when Core is launched standalone or
without any Pro hint, its existing per-installation explicit choice
(localStorage["rigplane.i18n.locale"]) and detection logic must keep
working unchanged.
The contract intentionally avoids any network dependency. Locale selection is local-first and works offline. Tower is never consulted.
Envelope shape¶
A single small JSON envelope is the unit of propagation:
{
"locale": "ja-JP",
"source": "pro-shell",
"updatedAt": "2026-05-19T18:00:00Z",
"supportedLocales": ["en-US", "ja-JP", "qps-ploc"]
}
Fields:
locale— BCP-47 tag. Must be one ofsupportedLocales. Only the locales bundled with Core's runtime are accepted; anything else is rejected and Core falls back to its standalone resolution.source— one of:"pro-shell"— written by Pro from its own Settings UI;"core-explicit"— informational; for symmetry when Core writes to its own contract surface (Core already usesrigplane.i18n.locale, so this value is reserved for future use);"browser"— informational fallback marker;"fallback"— informational marker for the en-US fallback.updatedAt— ISO-8601 UTC instant. The reader does not enforce freshness today; the field exists so a future revision can ignore stale envelopes (for example, a Pro reinstall that left a stalelocalStoragekey behind).supportedLocales— the locales the writer believes are supported. Informational; the Core reader validateslocaleagainst its own bundled list, not the writer's list, because Core is the runtime authority for what it can render.
The envelope is JSON-encoded as a single value at every transport.
Transport surfaces¶
Core accepts the contract from Pro through three surfaces. A writer (Pro) MAY use any subset; a reader (Core) checks them in fixed order.
Surface A — URL query parameter ?locale=<bcp47> (required)¶
The smallest, most testable surface. Pro launches embedded or external
Core URLs with ?locale=<tag>, for example:
Core reads URLSearchParams exactly once at module init. Subsequent
navigations do not re-read this parameter — it is a boot signal, not a
session state. This is intentional so a user who later picks a
different locale via the Core LanguageSelector is not stomped on every
route change.
The query parameter form carries only locale. The source,
updatedAt, and supportedLocales fields are synthesized by the
reader (source "pro-shell", updatedAt = read time).
Surface B — localStorage key rigplane.i18n.proLocale.v1¶
Pro MAY write the full envelope as JSON to:
This surface is useful when Pro and Core share the same WebView storage
partition. The Core reader parses the envelope and validates each
field. Malformed JSON or a missing/invalid locale is silently
ignored. This surface is read once at boot, like surface A.
Surface C — postMessage from a Tauri host (deferred)¶
Reserved for a future revision. The shape MUST be the same envelope
above wrapped in { type: "rigplane.i18n.localePreference", payload: <envelope> }.
Not implemented in RP-ML-012A. Pro's RP-ML-012B is not required to
implement this surface either; it is documented to keep the contract
extensible.
Read precedence (Core side)¶
When Core boots, it computes the active locale using:
- Surface A:
?locale=<bcp47>in the current URL. - Surface B:
rigplane.i18n.proLocale.v1inlocalStorage. - Explicit Core setting: existing
rigplane.i18n.localeinlocalStorage(owned by the Core LanguageSelector). - Browser locale:
navigator.languages/navigator.language, narrowed to a supported locale. en-USfallback.
Rungs 1 and 2 are the "Pro setting" rung from strategy §5.4. Rungs 3–5 are the existing Core behavior; they are not changed by this contract.
An external source (rungs 1 or 2) is accepted only if its locale
parses as a member of Core's bundled SUPPORTED_LOCALES. An invalid
tag (typo, unsupported region, removed locale) is ignored entirely and
Core falls through to rung 3.
The pseudo-locale qps-ploc is accepted from a Pro hint only as a
deliberate developer opt-in. The reader does not special-case it; if a
Pro build is configured to forward qps-ploc, the Core side honors it.
Standalone behavior is preserved¶
When neither surface A nor surface B yields a valid locale, the boot
path is a no-op against the existing store and Core behaves exactly as
before RP-ML-012A. The contract module never writes to the explicit
Core key rigplane.i18n.locale; that key remains owned by the
LanguageSelector and is the user's stable preference across launches.
Dev-mode diagnostics¶
In development builds (import.meta.env.PROD === false) the contract
module logs the resolved source on boot via console.info, prefixed
with [i18n]. Production builds are silent. This is to support manual
QA of precedence without shipping a visible UI affordance.
Cross-side coordination¶
The Pro side (RP-ML-012B / rigplane-pro #882) implements the writer.
Both sides reference this document as the single source of truth for
the envelope shape and transport surfaces. Any change to envelope
field names or transport keys is a coordinated change across both
repos.