fix(i18n): ship valid bundled scripts for locale controls
This commit is contained in:
1 parent
e98cd2b7ce
commit
0d673a3b5a
7 files changed
+61
-17
No files matched your search
+39
-5
@@ -4,9 +4,10 @@ import vm from "node:vm";
|
||||
const worker = await import(new URL("../dist/server/index.js", import.meta.url));
|
||||
assert.equal(typeof worker.default?.fetch, "function");
|
||||
const assets = await import(new URL("../dist/server/assets.js", import.meta.url));
|
||||
const source = await readFile(new URL("../public/index.html", import.meta.url), "utf8");
|
||||
assert.match(assets.indexHtml, /!\[1,2\]\.includes\(d\.schemaVersion\)/);
|
||||
assert.doesNotMatch(assets.indexHtml, /localizeDynamicText|createTreeWalker|NodeFilter\.SHOW_TEXT|const ZH=/);
|
||||
assert.match(assets.indexHtml, /locales\/en\.js/);
|
||||
assert.match(assets.indexHtml, /window\.LifeOSLocales\.en=/);
|
||||
assert.match(assets.addonsJs, /window\.exportData = exportData/);
|
||||
assert.match(assets.addonsJs, /schemaVersion: 2/);
|
||||
assert.match(assets.addonsJs, /settings: settings\(\), system: sys\(\)/);
|
||||
@@ -19,8 +20,8 @@ const flatten = (value, prefix = "") => Object.entries(value).flatMap(([key, chi
|
||||
const enKeys = flatten(assets.localeEn), zhKeys = flatten(assets.localeZh);
|
||||
assert.deepEqual(enKeys, zhKeys, "English and Chinese locale keys must match");
|
||||
const resolveKey = (locale, key) => key.split(".").reduce((value, part) => value?.[part], locale);
|
||||
for (const source of [assets.i18nRenderersJs, assets.addonsJs, assets.indexHtml]) {
|
||||
for (const match of source.matchAll(/(?:\bt|LifeI18n\.t)\(['"`]([A-Za-z][\w.]+)['"`]/g)) {
|
||||
for (const script of [assets.i18nRenderersJs, assets.addonsJs, source]) {
|
||||
for (const match of script.matchAll(/(?:\bt|LifeI18n\.t)\(['"`]([A-Za-z][\w.]+)['"`]/g)) {
|
||||
assert.notEqual(resolveKey(assets.localeEn, match[1]), undefined, `Missing locale key: ${match[1]}`);
|
||||
assert.notEqual(resolveKey(assets.localeZh, match[1]), undefined, `Missing Chinese locale key: ${match[1]}`);
|
||||
}
|
||||
@@ -33,7 +34,6 @@ for (const locale of [assets.localeEn, assets.localeZh]) {
|
||||
for (const script of [assets.tasksJs, assets.calendarJs]) {
|
||||
assert.doesNotMatch(script, /createTreeWalker|NodeFilter\.SHOW_TEXT/);
|
||||
}
|
||||
const source = await readFile(new URL("../public/index.html", import.meta.url), "utf8");
|
||||
for (const [, inlineScript] of source.matchAll(/<script>([\s\S]*?)<\/script>/g)) new vm.Script(inlineScript);
|
||||
assert.doesNotMatch(source, /localizeDynamicText|createTreeWalker|NodeFilter\.SHOW_TEXT|const ZH=/);
|
||||
assert.match(source, /id==='system'\)window\.systemApp\(c\)/);
|
||||
@@ -44,7 +44,11 @@ assert.match(assets.indexHtml, /setTimeout\(\(\)=>window\.applyLanguage\?\.\(\),
|
||||
assert.doesNotMatch(assets.indexHtml, /setTimeout\(applyLanguage\s*,/);
|
||||
assert.doesNotMatch(assets.addonsJs, /(?<![\w.])applyLanguage\s*\(/, "add-on actions must not call a missing global locale function");
|
||||
assert.match(assets.indexHtml, /window\.renderIcons=renderIcons;window\.renderAll=renderAll/, "shared renderers are explicitly exposed to locale scripts");
|
||||
assert.ok(assets.indexHtml.indexOf('src="i18n-renderers.js?v=1.5.7"') < assets.indexHtml.lastIndexOf("window.init()"), "locale renderers load before desktop initialization");
|
||||
const shippedScripts = [...assets.indexHtml.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(match => match[1]);
|
||||
assert.equal(shippedScripts.length, 10, "the complete desktop script chain is present in the served HTML");
|
||||
assert.doesNotMatch(assets.indexHtml, /<script\s+src=/, "the desktop cannot silently fall back when an app script fails to load");
|
||||
assert.ok(assets.indexHtml.indexOf('window.systemApp =') < assets.indexHtml.lastIndexOf("window.init()"), "locale renderers run before desktop initialization");
|
||||
for (const script of shippedScripts) new vm.Script(script);
|
||||
for (const script of [assets.i18nJs, assets.tasksJs, assets.calendarJs, assets.i18nRenderersJs, assets.addonsJs]) new vm.Script(script);
|
||||
assert.doesNotMatch(source, /Version 0\.1|Schema v1/);
|
||||
const makeStorage = initial => {
|
||||
@@ -109,6 +113,36 @@ assert.equal(migratedContext.LifeI18n.language(), "zh", "legacy manual preferenc
|
||||
assert.match(assets.i18nRenderersJs, /preference === 'zh'/);
|
||||
assert.match(assets.i18nRenderersJs, /onclick=\"window\.setLanguage\('zh'\)\"/);
|
||||
assert.doesNotMatch(assets.addonsJs, /lifeos_language_preference_v1/, "the device locale preference stays outside cloud-synced keys");
|
||||
const uiNodes = new Map();
|
||||
const uiNode = () => ({ innerHTML: "", textContent: "", style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, remove() {}, append() {}, querySelector: () => null, querySelectorAll: () => [] });
|
||||
for (const id of ["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal", "windows", "clock", "date", "desktop-brand", "life-launcher", "content-system"]) uiNodes.set(`#${id}`, uiNode());
|
||||
const uiDocument = {
|
||||
documentElement: { dataset: {}, lang: "en" }, title: "", head: { insertAdjacentHTML() {}, append() {} }, body: { append() {} },
|
||||
querySelector: selector => uiNodes.get(selector) || null, getElementById: id => uiNodes.get(`#${id}`) || null,
|
||||
createElement: uiNode, addEventListener() {}, querySelectorAll: () => []
|
||||
};
|
||||
const uiContext = vm.createContext({
|
||||
document: uiDocument, navigator: { languages: ["en-US"], language: "en-US" }, localStorage: makeStorage(),
|
||||
Blob, crypto: globalThis.crypto, innerWidth: 1000, innerHeight: 800,
|
||||
matchMedia: () => ({ matches: false }), setTimeout: () => 0, setInterval: () => 0,
|
||||
fetch: async () => ({ ok: false, json: async () => ({}) }), console,
|
||||
...Object.fromEntries(["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal"].map(id => [id, uiNodes.get(`#${id}`)]))
|
||||
});
|
||||
uiContext.window = uiContext;
|
||||
for (const index of [0, 2, 3, 4, 8]) vm.runInContext(shippedScripts[index], uiContext);
|
||||
vm.runInContext("windows.system={classList:{contains(){return false}}}", uiContext);
|
||||
uiContext.setLanguage("zh");
|
||||
assert.equal(uiContext.LifeI18n.language(), "zh");
|
||||
assert.match(uiNodes.get("#content-system").innerHTML, /系统设置/);
|
||||
assert.match(uiNodes.get("#icons").innerHTML, /今日/);
|
||||
assert.equal(uiDocument.documentElement.lang, "zh-CN");
|
||||
uiContext.setLanguage("en");
|
||||
assert.match(uiNodes.get("#content-system").innerHTML, /SYSTEM SETTINGS/);
|
||||
assert.match(uiNodes.get("#icons").innerHTML, /TODAY/);
|
||||
uiContext.navigator.languages = ["zh-CN"];
|
||||
uiContext.setLanguage("auto");
|
||||
assert.match(uiNodes.get("#content-system").innerHTML, /系统设置/);
|
||||
assert.equal(uiContext.LifeI18n.preference(), "auto");
|
||||
const zhLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "zh-CN,zh;q=0.9" } }), {});
|
||||
assert.match(await zhLogin.text(), /登录 ChatGPT/);
|
||||
const enLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "en-US,en;q=0.9" } }), {});
|
||||
|
||||
Reference in new issue
Block a user