fix(i18n): localize all user-facing Life OS text
This commit is contained in:
1 parent
4297f1ee42
commit
a95d3652ca
12 files changed
+1057
-110
No files matched your search
+4
-10
@@ -7,16 +7,10 @@ const meta = new URL("../dist/.openai/", import.meta.url);
|
||||
|
||||
await rm(dist, { recursive: true, force: true });
|
||||
await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]);
|
||||
const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, i18nRenderersJs, manifest, serviceWorker, icon] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("i18n-renderers.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg")]);
|
||||
const release = "1.5.1";
|
||||
// The original single-file prototype carried an obsolete English-to-Chinese
|
||||
// DOM replacement block. Runtime localisation now lives in i18n.js and
|
||||
// i18n-renderers.js, so exclude that dead block from the deployed document.
|
||||
const withoutLegacyI18n = rawIndexHtml
|
||||
.replace(/const ZH=.*?function browserLanguage/s, 'function browserLanguage')
|
||||
.replace(/function localizeDynamicText.*?function i18n\(key,fallback=key\)\{return lang\(\)===\'zh\'\?\(ZH\[key\]\|\|key\):fallback\}/s, 'function i18n(key,fallback=key){return fallback}');
|
||||
const indexHtml = withoutLegacyI18n.replace('src="addons.js"', `src="addons.js?v=${release}"`).replace('src="tasks.js"', `src="tasks.js?v=${release}"`).replace('src="calendar.js"', `src="calendar.js?v=${release}"`);
|
||||
await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const bootGuardJs=${JSON.stringify(bootGuardJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const tasksJs=${JSON.stringify(tasksJs)};\nexport const calendarJs=${JSON.stringify(calendarJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\nexport const manifest=${JSON.stringify(manifest)};\nexport const serviceWorker=${JSON.stringify(serviceWorker)};\nexport const icon=${JSON.stringify(icon)};\n`);
|
||||
const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, i18nRenderersJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("i18n-renderers.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]);
|
||||
const release = "1.5.2";
|
||||
const indexHtml = rawIndexHtml.replace('src="addons.js"', `src="addons.js?v=${release}`).replace('src="tasks.js"', `src="tasks.js?v=${release}`).replace('src="calendar.js"', `src="calendar.js?v=${release}`);
|
||||
await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const bootGuardJs=${JSON.stringify(bootGuardJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const localeEn=JSON.parse(${JSON.stringify(localeEn)});\nexport const localeZh=JSON.parse(${JSON.stringify(localeZh)});\nexport const localeEnJs=${JSON.stringify(`window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`)};\nexport const localeZhJs=${JSON.stringify(`window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`)};\nexport const tasksJs=${JSON.stringify(tasksJs)};\nexport const calendarJs=${JSON.stringify(calendarJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\nexport const manifest=${JSON.stringify(manifest)};\nexport const serviceWorker=${JSON.stringify(serviceWorker)};\nexport const icon=${JSON.stringify(icon)};\n`);
|
||||
await copyFile(new URL("../worker/index.js", import.meta.url), new URL("index.js", server));
|
||||
await copyFile(new URL("../.openai/hosting.json", import.meta.url), new URL("hosting.json", meta));
|
||||
console.log("Built Life OS Worker artifact");
|
||||
+34
-1
@@ -1,11 +1,44 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
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/);
|
||||
console.log("Life OS Worker artifact is valid");
|
||||
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");
|
||||
assert.doesNotMatch(source, /localizeDynamicText|createTreeWalker|NodeFilter\.SHOW_TEXT|const ZH=/);
|
||||
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`);
|
||||
Reference in new issue
Block a user