mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
feat(i18n): an ./i18n entry for an application with its own language choice
startPageLocale, followPageLocale and switchLocale start, follow and switch the page's language as the standalone application and the globe picker do, for an application that offers the language elsewhere and passes localePicker: false. The standalone application uses them.
This commit is contained in:
1 parent
feab48397f
commit
5f96009b67
12 files changed
+255
-23
No files matched your search
+3
-1
@@ -224,7 +224,9 @@ leaving out `command-dock` the voice control and its pointer reticle;
|
||||
leaving out `context` the Bhote Koshi reconstruction panel; and leaving out
|
||||
`intel-hud` the Intel HUD overlay (`#intel-hud`), whose readouts the HUD
|
||||
implementation publishes to the `gev-hud` store. An unknown
|
||||
view name is an error. A host that does not initialize Core's locale passes
|
||||
view name is an error. An application that offers its own language choice
|
||||
starts and switches the language with the `./i18n` entry (`startPageLocale`,
|
||||
`followPageLocale`, `switchLocale`; see [I18N.md](./I18N.md)) and passes
|
||||
`localePicker: false` to `createApplicationControls` instead of editing the
|
||||
markup. Node tests that import application modules register the component
|
||||
loader with `node --import=<core package>/build/register-vue`.
|
||||
@@ -5080,6 +5080,10 @@ Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, *
|
||||
views re-render, code that published translated text words it again through
|
||||
`onLocaleChange`, and `<html lang>`/`dir` and the page's `data-i18n` markup
|
||||
follow. Nothing is lost; a toast already on screen keeps its words.
|
||||
An application that offers the language elsewhere (a menu) uses the
|
||||
`./i18n` entry (`src/i18n/page.js`): `startPageLocale` at boot,
|
||||
`followPageLocale` for the markup and `switchLocale`, which does what a
|
||||
picker choice does, with `localePicker: false`.
|
||||
- **Right to left.** Arabic (`ar`) and Hebrew (`he`) are in `RTL_LOCALES`, so
|
||||
`<html dir="rtl">`: the title, style indicator, both panel stacks and their
|
||||
icon rails swap sides through logical properties (`inset-inline-*`,
|
||||
|
||||
@@ -59,6 +59,30 @@ The language picker switches in place, without a reload. A view that calls `t()`
|
||||
|
||||
A toast already on screen keeps its words.
|
||||
|
||||
## An application with its own language choice
|
||||
|
||||
An application that composes Core and offers the language somewhere else (a menu, a settings page) imports the `./i18n` entry (`src/i18n/page.js`) and passes `localePicker: false` to `createApplicationControls`:
|
||||
|
||||
```js
|
||||
import {
|
||||
LOCALES,
|
||||
LOCALE_NAMES,
|
||||
followPageLocale,
|
||||
getLocale,
|
||||
onLocaleChange,
|
||||
startPageLocale,
|
||||
switchLocale,
|
||||
} from '<core package>/i18n';
|
||||
|
||||
startPageLocale(document); // before any view renders: ?lang=, stored choice, browser, English; translates data-i18n markup
|
||||
const stopFollowing = followPageLocale(document); // <html lang>/dir and data-i18n markup follow each switch
|
||||
|
||||
for (const code of LOCALES) addMenuItem(LOCALE_NAMES[code], code === getLocale());
|
||||
menu.onChoose = (code) => switchLocale(code); // switched in place and stored (in ?lang when storage is blocked)
|
||||
```
|
||||
|
||||
`switchLocale` does what a choice in the globe picker does, and returns `'unchanged'` for the active language. `onLocaleChange` tells the application's own menu to mark the new language.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
|
||||
@@ -109,6 +133,7 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca
|
||||
| `src/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs |
|
||||
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
|
||||
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
|
||||
| `src/i18n/page.js` | the `./i18n` entry: start, follow and switch the page's language |
|
||||
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
|
||||
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
|
||||
| `scripts/i18n.mjs` | `npm run i18n`: extracts messages, checks the catalogs, finds unmarked UI text |
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
"./application": "./src/app/application.js",
|
||||
"./application/viewer": "./src/app/viewer.js",
|
||||
"./time": "./src/time/worldClock.js",
|
||||
"./i18n": "./src/i18n/page.js",
|
||||
"./build/vite": {
|
||||
"node": "./build/vite.js"
|
||||
},
|
||||
|
||||
@@ -1342,6 +1342,8 @@
|
||||
"src/data/layerState.js",
|
||||
"src/data/layerState.test.mjs",
|
||||
"src/data/spriteOrder.js",
|
||||
"src/i18n/page.js",
|
||||
"src/i18n/page.test.mjs",
|
||||
"src/i18n/locales/ar.json",
|
||||
"src/i18n/locales/de.json",
|
||||
"src/i18n/locales/es.json",
|
||||
|
||||
@@ -175,6 +175,28 @@
|
||||
],
|
||||
"external": ["vue", "pinia", "vue-i18n"]
|
||||
},
|
||||
"i18n": {
|
||||
"exports": ["./i18n"],
|
||||
"modules": [
|
||||
"src/i18n/page.js",
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/ar.json",
|
||||
"src/i18n/locales/de.json",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/fr.json",
|
||||
"src/i18n/locales/he.json",
|
||||
"src/i18n/locales/it.json",
|
||||
"src/i18n/locales/ja.json",
|
||||
"src/i18n/locales/pt.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/sv.json",
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"time": {
|
||||
"exports": ["./time"],
|
||||
"modules": ["src/time/worldClock.js"],
|
||||
|
||||
Vendored
+2
-1
@@ -9,7 +9,8 @@ import { t } from '../i18n/index.js';
|
||||
* Construct the existing controls and camera presentation. `initialShareCode`
|
||||
* opens a share code (the `<code>` of `/share/<code>`) at start-up, for a host
|
||||
* whose address does not carry one. `localePicker: false` removes the language
|
||||
* picker, for a host that does not initialize Core's locale.
|
||||
* picker, for a host that offers its own language choice (`switchLocale` from
|
||||
* the `./i18n` entry).
|
||||
*/
|
||||
export function createApplicationControls({
|
||||
scene: { viewer, mapStackController, operations },
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @module i18n/page
|
||||
* @description The page's interface language, for an application that
|
||||
* composes Core and offers its own language choice (a menu item, a settings
|
||||
* page) instead of the globe picker. It starts the language the standalone
|
||||
* application starts, keeps `<html lang>`/`<html dir>` and the page's
|
||||
* `data-i18n` markup in step, and switches in place, stored for the next
|
||||
* visit, exactly as the picker does. Guide: docs/I18N.md.
|
||||
*/
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
LOCALES,
|
||||
LOCALE_NAMES,
|
||||
applyDocumentLanguage,
|
||||
isRtlLocale,
|
||||
normalizeLocale,
|
||||
resolveLocale,
|
||||
} from './locale.js';
|
||||
import {
|
||||
applyDocumentTranslations,
|
||||
getLocale,
|
||||
onLocaleChange,
|
||||
persistLocale,
|
||||
setLocale,
|
||||
} from './index.js';
|
||||
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
LOCALES,
|
||||
LOCALE_NAMES,
|
||||
applyDocumentTranslations,
|
||||
getLocale,
|
||||
isRtlLocale,
|
||||
onLocaleChange,
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the page in its language: `?lang=`, then the stored choice, then the
|
||||
* browser's languages, then English. Call it before any view renders, so
|
||||
* every lookup and `<html lang>`/`<html dir>` agree from the first frame; the
|
||||
* page's static `data-i18n` markup (a loading screen) is translated now.
|
||||
* @param {Document} [doc]
|
||||
* @param {Parameters<typeof resolveLocale>[0]} [inputs] Location, storage
|
||||
* and languages, for tests.
|
||||
* @returns {string} The active locale.
|
||||
*/
|
||||
export function startPageLocale(doc = globalThis.document, inputs) {
|
||||
const locale = setLocale(resolveLocale(inputs));
|
||||
applyDocumentLanguage(doc, locale);
|
||||
applyDocumentTranslations(doc);
|
||||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep `<html lang>`/`<html dir>` and the page's `data-i18n` markup in step
|
||||
* with every language switch.
|
||||
* @param {Document} [doc]
|
||||
* @returns {() => void} Stops following.
|
||||
*/
|
||||
export function followPageLocale(doc = globalThis.document) {
|
||||
return onLocaleChange(() => {
|
||||
applyDocumentLanguage(doc, getLocale());
|
||||
applyDocumentTranslations(doc);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the page to `locale` in place and keep it for the next visit. The
|
||||
* active language, or an unknown code that would fall back to it, changes
|
||||
* nothing.
|
||||
* @param {string} locale One of LOCALES.
|
||||
* @param {Parameters<typeof persistLocale>[1]} [options] Location, history
|
||||
* and storage, for tests.
|
||||
* @returns {'unchanged'|'switched'}
|
||||
*/
|
||||
export function switchLocale(locale, options) {
|
||||
const next = normalizeLocale(locale) || DEFAULT_LOCALE;
|
||||
if (next === getLocale()) return 'unchanged';
|
||||
setLocale(persistLocale(next, options));
|
||||
return 'switched';
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// src/i18n/page.test.mjs — an application with its own language choice starts,
|
||||
// follows and switches the page's language as the globe picker does.
|
||||
import { afterEach, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { LOCALE_STORAGE_KEY } from './locale.js';
|
||||
import {
|
||||
followPageLocale,
|
||||
getLocale,
|
||||
startPageLocale,
|
||||
switchLocale,
|
||||
} from './page.js';
|
||||
import { setLocale } from './index.js';
|
||||
|
||||
afterEach(() => setLocale('en'));
|
||||
|
||||
function memoryStorage() {
|
||||
const data = new Map();
|
||||
return {
|
||||
data,
|
||||
getItem: (key) => data.get(key) ?? null,
|
||||
setItem: (key, value) => data.set(key, String(value)),
|
||||
};
|
||||
}
|
||||
|
||||
function page(elements = []) {
|
||||
return {
|
||||
documentElement: { lang: '', dir: '' },
|
||||
querySelectorAll: (selector) =>
|
||||
selector === '[data-i18n]' ? elements : [],
|
||||
};
|
||||
}
|
||||
|
||||
/** A data-i18n element, as applyDocumentTranslations reads one. */
|
||||
function marked(text) {
|
||||
return {
|
||||
textContent: text,
|
||||
hasAttribute: (name) => name === 'data-i18n',
|
||||
getAttribute: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
test('the page starts in ?lang=, else the stored choice, with lang and dir set', () => {
|
||||
const doc = page();
|
||||
const storage = memoryStorage();
|
||||
storage.setItem(LOCALE_STORAGE_KEY, 'de');
|
||||
assert.equal(
|
||||
startPageLocale(doc, {
|
||||
location: { search: '?lang=ar' },
|
||||
storage,
|
||||
languages: ['ru'],
|
||||
}),
|
||||
'ar',
|
||||
);
|
||||
assert.equal(getLocale(), 'ar');
|
||||
assert.deepEqual(doc.documentElement, { lang: 'ar', dir: 'rtl' });
|
||||
assert.equal(
|
||||
startPageLocale(doc, { location: { search: '' }, storage, languages: [] }),
|
||||
'de',
|
||||
);
|
||||
assert.deepEqual(doc.documentElement, { lang: 'de', dir: 'ltr' });
|
||||
});
|
||||
|
||||
test("the page's data-i18n markup is in its language from the start", () => {
|
||||
const close = marked('Close');
|
||||
startPageLocale(page([close]), {
|
||||
location: { search: '?lang=de' },
|
||||
storage: memoryStorage(),
|
||||
languages: [],
|
||||
});
|
||||
assert.equal(close.textContent, 'Schließen');
|
||||
});
|
||||
|
||||
test('a switch is stored, drops ?lang, and the followed page takes it', () => {
|
||||
const doc = page();
|
||||
const stop = followPageLocale(doc);
|
||||
const storage = memoryStorage();
|
||||
const replaced = [];
|
||||
const options = {
|
||||
location: { href: 'https://example.test/?lang=fr#view' },
|
||||
history: { replaceState: (_s, _t, url) => replaced.push(url) },
|
||||
storage,
|
||||
};
|
||||
assert.equal(switchLocale('he', options), 'switched');
|
||||
assert.equal(getLocale(), 'he');
|
||||
assert.equal(storage.data.get(LOCALE_STORAGE_KEY), 'he');
|
||||
assert.deepEqual(replaced, ['https://example.test/#view']);
|
||||
assert.deepEqual(doc.documentElement, { lang: 'he', dir: 'rtl' });
|
||||
stop();
|
||||
switchLocale('ja', options);
|
||||
assert.deepEqual(doc.documentElement, { lang: 'he', dir: 'rtl' });
|
||||
});
|
||||
|
||||
test('choosing the active language, or one that falls back to it, changes nothing', () => {
|
||||
const storage = memoryStorage();
|
||||
const options = {
|
||||
location: { href: 'https://example.test/' },
|
||||
history: { replaceState() {} },
|
||||
storage,
|
||||
};
|
||||
assert.equal(switchLocale('en', options), 'unchanged');
|
||||
assert.equal(switchLocale('xx', options), 'unchanged');
|
||||
assert.equal(storage.data.size, 0);
|
||||
assert.equal(switchLocale('pt-BR', options), 'switched');
|
||||
assert.equal(getLocale(), 'pt');
|
||||
assert.equal(switchLocale('pt', options), 'unchanged');
|
||||
});
|
||||
@@ -64,12 +64,9 @@ async function standaloneStartup(Cesium, credentials, fetchImpl) {
|
||||
document: {
|
||||
getElementById: () => ({ querySelector: () => ({}) }),
|
||||
},
|
||||
resolveLocale: () => 'en',
|
||||
setLocale() {},
|
||||
getLocale: () => 'en',
|
||||
onLocaleChange: () => () => {},
|
||||
startPageLocale: () => 'en',
|
||||
followPageLocale: () => () => {},
|
||||
mountApplicationViews() {},
|
||||
applyDocumentLanguage() {},
|
||||
applyDocumentTranslations() {},
|
||||
CITY_POIS: [],
|
||||
createApplication: (options) => options,
|
||||
|
||||
@@ -6,13 +6,11 @@ import { createStandaloneScene } from './scene.js';
|
||||
import { createStandaloneControls } from './controls.js';
|
||||
import { createStandaloneData } from './data.js';
|
||||
import { createStandaloneTools } from './tools.js';
|
||||
import { resolveLocale, applyDocumentLanguage } from '../i18n/locale.js';
|
||||
import {
|
||||
applyDocumentTranslations,
|
||||
getLocale,
|
||||
onLocaleChange,
|
||||
setLocale,
|
||||
} from '../i18n/index.js';
|
||||
followPageLocale,
|
||||
startPageLocale,
|
||||
} from '../i18n/page.js';
|
||||
import { mountApplicationViews } from '../ui/views/index.js';
|
||||
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
||||
|
||||
@@ -37,9 +35,7 @@ export function createStandaloneApplication({
|
||||
// scripts execute after parsing, so the DOM is ready here: the views
|
||||
// translate as they render, and the page's static data-i18n* markup (the
|
||||
// loading screen) is translated here and after every language switch.
|
||||
const activeLocale = resolveLocale();
|
||||
setLocale(activeLocale);
|
||||
applyDocumentLanguage(document, activeLocale);
|
||||
startPageLocale(document);
|
||||
// Views render the page's markup; wiring reads it.
|
||||
mountApplicationViews(document);
|
||||
applyDocumentTranslations(document);
|
||||
@@ -50,12 +46,7 @@ export function createStandaloneApplication({
|
||||
return createApplication({
|
||||
createScene: async (context) => {
|
||||
// A language switch applies in place; the page's own markup follows.
|
||||
context.defer(
|
||||
onLocaleChange(() => {
|
||||
applyDocumentLanguage(document, getLocale());
|
||||
applyDocumentTranslations(document);
|
||||
}),
|
||||
);
|
||||
context.defer(followPageLocale(document));
|
||||
placeSearch = createStandalonePlaceSearch({
|
||||
// The bundled city and landmark data the offline name provider reads.
|
||||
// The search package takes it as plain data rather than importing it,
|
||||
|
||||
@@ -96,7 +96,7 @@ export class StyleManager extends ShellFacade {
|
||||
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.localePicker=true] - false removes the language
|
||||
* picker, for a host that does not initialize Core's locale.
|
||||
* picker, for a host that offers its own language choice.
|
||||
*/
|
||||
constructor(
|
||||
viewer,
|
||||
@@ -764,7 +764,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._lifetime.own(
|
||||
onLocaleChange(() => nextTick(() => this._sizeDisplayLabelColumn())),
|
||||
);
|
||||
// A host that does not initialize Core's locale shows no picker.
|
||||
// A host that offers its own language choice shows no picker.
|
||||
if (this._localePickerEnabled === false) return;
|
||||
this._publishLocalePicker();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user