i18n: language picker in the bottom-right corner

Replaces the EN/ES/RU buttons in the Visual Presets dock with a picker
that scales to many languages: a globe button with the current code
next to the POWER UP chip, opening a list of languages named in their
own language with their codes, the current one first and checked. A
search field appears past 8 languages. Keyboard: arrows, Home/End,
Enter/Space, Esc, type-ahead; keys stay in the list while it is open.
Choosing a language still asks before dropping unsaved work, then
reloads. Phones get the globe alone and a full-width sheet; the layout
uses logical properties for right-to-left languages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 02:03:17 +04:00
1 parent 7fd1dd867f
commit 6031c95a81
21 files changed
+1381 -139

No files matched your search

+28 -27
View File
@@ -45,7 +45,7 @@ import {
registerUnsavedWork,
t,
} from '../i18n/index.js';
import { LOCALES, LOCALE_NAMES } from '../i18n/locale.js';
import { applyLocaleChoice, mountLocalePicker } from './localePicker.js';
/**
* Central UI orchestrator for the God's Eye View application.
@@ -715,17 +715,13 @@ export class StyleManager extends ShellFacade {
}
/**
* Renders the command-dock locale switch: one button per shipped locale,
* labelled with its code and named in its own language. A click stores the
* choice and reloads, so nothing re-renders live; it asks first when the
* reload would lose work (see _confirmLocaleReload).
* Mounts the interface language picker in the bottom-right corner
* (src/ui/localePicker.js). Choosing another language stores it and
* reloads, so nothing re-renders live; it asks first when the reload would
* lose work (see _chooseLocale).
* @returns {void}
*/
_initLocaleSelector() {
const container = document.querySelector(
'#control-panel .dock-locale-switch',
);
if (!container) return;
// Work this shell can see that a reload drops. Other modules register
// their own (registerUnsavedWork in src/i18n/index.js).
this._unsavedWorkChecks = [
@@ -740,24 +736,27 @@ export class StyleManager extends ShellFacade {
this.cockpitView?.active ? msg('the open cockpit') : null,
),
];
const active = getLocale();
for (const locale of LOCALES) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'dock-locale-btn';
button.dataset.locale = locale;
button.textContent = locale.toUpperCase();
button.setAttribute('aria-label', LOCALE_NAMES[locale]);
button.lang = locale;
const isActive = locale === active;
button.classList.toggle('active', isActive);
button.setAttribute('aria-pressed', String(isActive));
button.addEventListener('click', () => {
if (!this._confirmLocaleReload()) return;
persistLocaleAndReload(button.dataset.locale);
});
container.appendChild(button);
}
const root = globalThis.document?.getElementById?.('locale-picker');
if (!root) return;
this._localePicker = mountLocalePicker({
root,
current: getLocale(),
onSelect: (locale) => this._chooseLocale(locale),
});
}
/**
* Switch to `locale`: nothing for the active language, otherwise store it
* and reload once _confirmLocaleReload agrees.
* @param {string} locale
* @returns {'unchanged'|'declined'|'switched'}
*/
_chooseLocale(locale) {
return applyLocaleChoice(locale, {
current: getLocale(),
confirmSwitch: () => this._confirmLocaleReload(),
switchLocale: (next) => persistLocaleAndReload(next),
});
}
/**
@@ -1759,6 +1758,8 @@ export class StyleManager extends ShellFacade {
this._lifetime.destroy();
for (const unregister of this._unsavedWorkChecks?.splice(0) ?? [])
unregister();
this._localePicker?.destroy();
this._localePicker = null;
this._recording.destroy();
this._panelChrome.destroy();
this._feedback.destroy();