fix(i18n): reconcile preferred_language to the interface locale on every adopt
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
A user who never changed the language in Settings kept their account at the creation-time preferred_language seed (e.g. en from the Telegram launch language_code) even after switching the device to another language: the UI followed the device (ru) but the ad banner and out-of-app push — both resolved server-side from preferred_language — stayed en. The on-adopt reconcile was gated on an explicit local choice (localeLocked), so a system-guess locale was never pushed through. Reconcile preferred_language to the active interface locale (app.locale) on every session adopt and link, regardless of how the locale was chosen; persistLanguageToServer already self-gates (a no-op for guests and when already equal), so there is no steady-state write. The banner and push are the only server-rendered language surfaces and both read preferred_language, so this keeps the whole interface consistent — not just the banner. Drop the now-dead localeLocked flag (the reconcile guards were its only readers; the saved prefs.locale still restores the UI choice per device). Trade-off: preferred_language now follows the most-recently-opened device, so an explicit choice on one device can be overwritten by a system guess on another (the "explicit" mark is local, per-device); making it globally sticky would need a DB flag. Docs: ARCHITECTURE §4 + the profile field.
This commit is contained in:
@@ -158,7 +158,12 @@ arrive from a platform rather than completing a mandatory registration).
|
|||||||
rendered in the recipient's **interface language** (`preferred_language`, en/ru), not in
|
rendered in the recipient's **interface language** (`preferred_language`, en/ru), not in
|
||||||
any bot-scoped language, and the friend-invite **share link** (and its caption) point at
|
any bot-scoped language, and the friend-invite **share link** (and its caption) point at
|
||||||
that one bot. First Telegram contact seeds the new account's `preferred_language` from the
|
that one bot. First Telegram contact seeds the new account's `preferred_language` from the
|
||||||
launch `language_code` (§4); the interface language is otherwise edited in Settings.
|
launch `language_code` (§4), but the **interface language follows the device** — the system
|
||||||
|
guess, or an explicit Settings choice saved locally — and the bot never dictates the UI.
|
||||||
|
`preferred_language` is then **reconciled to the active interface locale on every session
|
||||||
|
adopt** (not only on a Settings change; a no-op for guests and when already equal), so the
|
||||||
|
server-rendered language surfaces — this push and the ad banner — always match the UI rather
|
||||||
|
than stranding a user who never opened Settings on the creation-time seed.
|
||||||
- **Variant preferences (New Game gating).** Which variants a player may be matched into is a
|
- **Variant preferences (New Game gating).** Which variants a player may be matched into is a
|
||||||
per-user **profile** setting — `variant_preferences`, a set of `engine.Variant` labels
|
per-user **profile** setting — `variant_preferences`, a set of `engine.Variant` labels
|
||||||
(`scrabble_en`, `scrabble_ru`, `erudit_ru`) edited on the Settings/Profile screen. New
|
(`scrabble_en`, `scrabble_ru`, `erudit_ru`) edited on the Settings/Profile screen. New
|
||||||
@@ -640,7 +645,7 @@ in either direction (the enqueue excludes the caller's `BlockedWith` set);
|
|||||||
**floats games with any unread entry to the top** of the your-turn and opponent-turn
|
**floats games with any unread entry to the top** of the your-turn and opponent-turn
|
||||||
sections (the finished section keeps its activity order). On each clear the publish-to-read
|
sections (the finished section keeps its activity order). On each clear the publish-to-read
|
||||||
latency is recorded; the read time itself is not retained.
|
latency is recorded; the read time itself is not retained.
|
||||||
- **Profile**: `preferred_language` (en/ru, edited in Settings), display name, email
|
- **Profile**: `preferred_language` (en/ru; tracks the interface language — §4), display name, email
|
||||||
(confirm-code binding, see §4), **timezone**, the daily **away window**, the
|
(confirm-code binding, see §4), **timezone**, the daily **away window**, the
|
||||||
**variant preferences** (`variant_preferences`, the matchable-variant set that gates New
|
**variant preferences** (`variant_preferences`, the matchable-variant set that gates New
|
||||||
Game — §3, defaulting to Erudit only, at least one enforced) and the
|
Game — §3, defaulting to Erudit only, at least one enforced) and the
|
||||||
|
|||||||
+17
-18
@@ -68,7 +68,6 @@ export const app = $state<{
|
|||||||
locale: Locale;
|
locale: Locale;
|
||||||
reduceMotion: boolean;
|
reduceMotion: boolean;
|
||||||
boardLabels: BoardLabelMode;
|
boardLabels: BoardLabelMode;
|
||||||
localeLocked: boolean;
|
|
||||||
/** Pending incoming friend requests, for the lobby ⚙️ badge and the Settings Friends tab. */
|
/** Pending incoming friend requests, for the lobby ⚙️ badge and the Settings Friends tab. */
|
||||||
notifications: number;
|
notifications: number;
|
||||||
/** Per-game flag: the player has at least one unread chat entry (message or nudge) in that
|
/** Per-game flag: the player has at least one unread chat entry (message or nudge) in that
|
||||||
@@ -109,7 +108,6 @@ export const app = $state<{
|
|||||||
locale: 'en',
|
locale: 'en',
|
||||||
reduceMotion: false,
|
reduceMotion: false,
|
||||||
boardLabels: 'beginner',
|
boardLabels: 'beginner',
|
||||||
localeLocked: false,
|
|
||||||
notifications: 0,
|
notifications: 0,
|
||||||
chatUnread: {},
|
chatUnread: {},
|
||||||
messageUnread: {},
|
messageUnread: {},
|
||||||
@@ -451,18 +449,20 @@ async function adoptSession(s: Session): Promise<void> {
|
|||||||
await saveSession(s);
|
await saveSession(s);
|
||||||
try {
|
try {
|
||||||
app.profile = await gateway.profileGet();
|
app.profile = await gateway.profileGet();
|
||||||
// The live interface language follows the device — the explicit local choice (locked, saved
|
// The live interface language follows the device — the explicit local choice (saved in
|
||||||
// in prefs) or the system guess made at bootstrap — and is no longer overridden from the
|
// prefs) or the system guess made at bootstrap — and is no longer overridden from the
|
||||||
// account here. preferred_language stays the user's saved choice (written from Settings,
|
// account here: the Telegram bot a user signs in through must not dictate the UI, so a
|
||||||
// and used for out-of-app push routing), but the Telegram bot a user signs in through must
|
// ru-bot launch on an English system stays English.
|
||||||
// not dictate the UI: a ru-bot launch on an English system stays English.
|
|
||||||
//
|
//
|
||||||
// But the banner and out-of-app push routing ARE resolved from preferred_language, so an
|
// The banner and out-of-app push are resolved server-side from preferred_language, so it
|
||||||
// explicit device choice the account has not recorded yet (picked while a guest, or
|
// must track whatever language the UI actually shows — the explicit choice AND the system
|
||||||
// differing from the Telegram system-language seed) would otherwise leave them in the wrong
|
// guess. Reconcile it to the active locale on every adopt, not only after an explicit
|
||||||
// language until the next Settings change. Reconcile the account to the saved local choice
|
// Settings choice: a user who never opened Settings would otherwise be stuck on the
|
||||||
// here; persistLanguageToServer no-ops for guests and when already equal.
|
// creation-time seed — e.g. an English banner under a Russian UI. This keeps every
|
||||||
if (app.localeLocked) void persistLanguageToServer(app.locale);
|
// server-rendered, language-dependent surface (banner, out-of-app push) aligned with the
|
||||||
|
// interface, not just one. persistLanguageToServer self-gates (a no-op for guests and when
|
||||||
|
// already equal), so there is no write in the steady state.
|
||||||
|
void persistLanguageToServer(app.locale);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err);
|
handleError(err);
|
||||||
}
|
}
|
||||||
@@ -483,9 +483,10 @@ export async function applyLinkResult(r: LinkResult): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
app.profile = await gateway.profileGet();
|
app.profile = await gateway.profileGet();
|
||||||
// A guest who chose a language and then linked in place now has a durable account: push the
|
// A guest who linked in place now has a durable account: push the active interface language
|
||||||
// saved choice so the banner + push routing follow it (see adoptSession).
|
// so the banner + push routing follow it (see adoptSession — reconciled regardless of an
|
||||||
if (app.localeLocked) void persistLanguageToServer(app.locale);
|
// explicit Settings choice).
|
||||||
|
void persistLanguageToServer(app.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -538,7 +539,6 @@ export async function bootstrap(): Promise<void> {
|
|||||||
applyReduceMotion(app.reduceMotion);
|
applyReduceMotion(app.reduceMotion);
|
||||||
if (prefs.locale) {
|
if (prefs.locale) {
|
||||||
app.locale = prefs.locale;
|
app.locale = prefs.locale;
|
||||||
app.localeLocked = true;
|
|
||||||
setLocale(prefs.locale);
|
setLocale(prefs.locale);
|
||||||
} else {
|
} else {
|
||||||
const guess = localeFrom(typeof navigator !== 'undefined' ? navigator.language : 'en');
|
const guess = localeFrom(typeof navigator !== 'undefined' ? navigator.language : 'en');
|
||||||
@@ -754,7 +754,6 @@ export function setTheme(theme: ThemePref): void {
|
|||||||
|
|
||||||
export function setLocalePref(locale: Locale): void {
|
export function setLocalePref(locale: Locale): void {
|
||||||
app.locale = locale;
|
app.locale = locale;
|
||||||
app.localeLocked = true;
|
|
||||||
setLocale(locale);
|
setLocale(locale);
|
||||||
persistPrefs();
|
persistPrefs();
|
||||||
void persistLanguageToServer(locale);
|
void persistLanguageToServer(locale);
|
||||||
|
|||||||
Reference in New Issue
Block a user