119 lines
8.1 KiB
JavaScript
119 lines
8.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
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));
|
|
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.addonsJs, /window\.exportData = exportData/);
|
|
assert.match(assets.addonsJs, /schemaVersion: 2/);
|
|
assert.match(assets.addonsJs, /settings: settings\(\), system: sys\(\)/);
|
|
assert.match(assets.addonsJs, /if \(mergedLocalBoard\) await uploadCloudState\(\)/);
|
|
assert.match(assets.i18nRenderersJs, /dialog\.goals/);
|
|
assert.match(assets.i18nRenderersJs, /shell\.wallpaperTitle/);
|
|
assert.match(assets.i18nRenderersJs, /window\.museumApp/);
|
|
assert.equal(assets.manifest.includes('"description"'), false);
|
|
const flatten = (value, prefix = "") => Object.entries(value).flatMap(([key, child]) => child && typeof child === "object" && !Array.isArray(child) ? flatten(child, prefix ? `${prefix}.${key}` : key) : [prefix ? `${prefix}.${key}` : key]).sort();
|
|
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)) {
|
|
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]}`);
|
|
}
|
|
}
|
|
for (const locale of [assets.localeEn, assets.localeZh]) {
|
|
assert.ok(locale.login.title && locale.login.description && locale.login.continue && locale.login.footnote);
|
|
assert.equal(locale.prompt.films.length, 30);
|
|
assert.equal(locale.prompt.lines.length, 30);
|
|
}
|
|
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\)/);
|
|
assert.match(source, /id==='today'\)window\.today\(c\)/);
|
|
assert.match(assets.i18nJs, /window\.setLanguage = setLanguage/);
|
|
assert.match(assets.i18nRenderersJs, /window\.renderAll\(\)/);
|
|
assert.match(assets.indexHtml, /setTimeout\(\(\)=>window\.applyLanguage\?\.\(\),0\)/, "missing locale UI code cannot abort desktop initialization");
|
|
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.ok(assets.indexHtml.indexOf('src="i18n-renderers.js?v=1.5.6"') < assets.indexHtml.lastIndexOf("window.init()"), "locale renderers load before desktop initialization");
|
|
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 => {
|
|
const values = new Map(Object.entries(initial || {}));
|
|
return { getItem: key => values.get(key) ?? null, setItem: (key,value) => values.set(key,String(value)), values };
|
|
};
|
|
let legacySettings = { language: "auto", languageMode: "auto" };
|
|
const calls = { icons: 0, render: 0, apply: 0, toast: 0 };
|
|
const localStorage = makeStorage();
|
|
const localeContext = vm.createContext({
|
|
navigator: { languages: ["zh-CN"], language: "zh-CN" },
|
|
localStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
settings: () => legacySettings,
|
|
renderIcons() { calls.icons++; }, renderAll() { calls.render++; }, applyLanguage() { calls.apply++; }, toast() { calls.toast++; },
|
|
});
|
|
localeContext.window = localeContext;
|
|
vm.runInContext(assets.i18nJs, localeContext);
|
|
assert.equal(localeContext.LifeI18n.preference(), "auto");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "auto mode follows Chinese browser language");
|
|
localeContext.navigator.languages = ["en-US"];
|
|
assert.equal(localeContext.LifeI18n.language(), "en", "auto mode follows English browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "TODAY");
|
|
localeContext.setLanguage("zh");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "manual Chinese choice overrides browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "今日");
|
|
assert.equal(localStorage.getItem("lifeos_language_preference_v1"), "zh");
|
|
assert.deepEqual(calls, { icons: 1, render: 1, apply: 1, toast: 1 }, "language selection redraws the desktop and shows feedback");
|
|
legacySettings = { language: "en", languageMode: "manual" };
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "cloud settings cannot overwrite the local manual choice");
|
|
localeContext.setLanguage("en");
|
|
assert.equal(localeContext.LifeI18n.language(), "en", "manual English choice overrides browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "TODAY");
|
|
const reloadedManualContext = vm.createContext({
|
|
navigator: { languages: ["en-US"] }, localStorage: makeStorage(),
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
});
|
|
reloadedManualContext.window = reloadedManualContext;
|
|
vm.runInContext(assets.i18nJs, reloadedManualContext);
|
|
assert.equal(reloadedManualContext.LifeI18n.language(), "en", "a reloaded page starts in automatic mode when no manual choice was saved");
|
|
localeContext.navigator.languages = ["zh-CN"];
|
|
localeContext.setLanguage("auto");
|
|
assert.equal(localeContext.LifeI18n.preference(), "auto");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "auto mode ignores stale cloud preference and follows this device");
|
|
localeContext.setLanguage("en");
|
|
const persistedManualContext = vm.createContext({
|
|
navigator: { languages: ["zh-CN"] }, localStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
});
|
|
persistedManualContext.window = persistedManualContext;
|
|
vm.runInContext(assets.i18nJs, persistedManualContext);
|
|
assert.equal(persistedManualContext.LifeI18n.language(), "en", "a manually selected language persists across reloads");
|
|
const migratedStorage = makeStorage({ lifeos_settings: JSON.stringify({ language: "zh" }) });
|
|
const migratedContext = vm.createContext({
|
|
navigator: { languages: ["en-US"] }, localStorage: migratedStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
settings: () => ({ language: "zh", languageMode: "manual" }),
|
|
});
|
|
migratedContext.window = migratedContext;
|
|
vm.runInContext(assets.i18nJs, migratedContext);
|
|
assert.equal(migratedContext.LifeI18n.language(), "zh", "legacy manual preference migrates to the local preference key");
|
|
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 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" } }), {});
|
|
assert.match(await enLogin.text(), /Sign in with ChatGPT/);
|
|
const localeResponse = await worker.default.fetch(new Request("https://life-os.test/locales/zh-CN.js"), {});
|
|
assert.equal(localeResponse.status, 200);
|
|
assert.match(await localeResponse.text(), /window\.LifeOSLocales\['zh-CN'\]/);
|
|
console.log(`Life OS artifact valid · ${enKeys.length} bilingual locale keys`);
|