fix: synchronize runtime locales and harden online updates

Translate UI, scheduler, validation, preload and maintenance messages dynamically, and propagate locale changes between workers. Harden update checks, downloads and terminal states, verify Debian pnpm, and ignore generated test artifacts.
This commit is contained in:
whyour
2026-10-17 07:07:09 +08:00
parent 8305825ffd
commit ef348b445b
72 changed files with 1733 additions and 553 deletions
+88
View File
@@ -0,0 +1,88 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
test('language changes reach live HTTP/scheduler workers and replacement workers restore the saved language', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-language-cluster-'));
const root = path.resolve(__dirname, '../..');
try {
const state = path.join(dir, 'language.json');
await fs.writeFile(state, '"en"');
const fixture = path.join(dir, 'cluster.cjs');
await fs.writeFile(
fixture,
`
require(${JSON.stringify(require.resolve('ts-node/register/transpile-only'))});
const cluster = require('node:cluster');
const fs = require('node:fs/promises');
const storePath = require.resolve(${JSON.stringify(
path.join(root, 'back/shared/store.ts'),
)});
require.cache[storePath] = { id: storePath, filename: storePath, loaded: true, exports: { shareStore: {
getLang: async () => JSON.parse(await fs.readFile(process.env.LANGUAGE_STATE, 'utf8')),
setLang: (lang) => fs.writeFile(process.env.LANGUAGE_STATE, JSON.stringify(lang)),
} } };
const { initializeLanguageSync } = require(${JSON.stringify(
path.join(root, 'back/shared/languageSync.ts'),
)});
const { setLang, t } = require(${JSON.stringify(
path.join(root, 'back/shared/i18n.ts'),
)});
(async () => {
await initializeLanguageSync();
if (cluster.isPrimary) {
const ready = new Map(), observed = new Set(); let phase = 'initial';
cluster.on('message', (worker, message) => {
if (message.kind === 'ready') {
ready.set(worker.id, message.value);
if (phase === 'initial' && ready.size === 2) {
if ([...ready.values()].some(value => value !== 'Update successful')) throw Error('Initial language was not restored');
phase = 'zh'; cluster.workers[1].send({ command: 'switch', lang: 'zh' });
} else if (phase === 'replacement' && ready.size === 3) {
if (message.value !== '更新成功') throw Error('Replacement lost saved language');
phase = 'en'; observed.clear(); cluster.workers[1].send({ command: 'switch', lang: 'en' });
}
}
if (message.kind === 'observed') {
if (message.value !== (phase === 'zh' ? '更新成功' : 'Update successful')) throw Error('Worker language did not change');
observed.add(worker.id);
if (phase === 'zh' && observed.size === 2) { phase = 'replacement'; cluster.fork(); }
else if (phase === 'en' && observed.size === 3) {
console.log(JSON.stringify({ initialWorkers: 2, updatedWorkers: 3, replacementRestored: true }));
for (const w of Object.values(cluster.workers)) w.kill();
}
}
});
cluster.fork(); cluster.fork();
} else {
process.on('message', async message => {
if (message.command === 'switch') await setLang(message.lang);
if (message.type === 'language-changed') process.send({ kind: 'observed', value: t('更新成功') });
});
process.send({ kind: 'ready', value: t('更新成功') });
}
})().catch(error => { console.error(error); process.exit(1); });
`,
);
const result = await promisify(execFile)(process.execPath, [fixture], {
cwd: root,
timeout: 15000,
env: {
...process.env,
TS_NODE_PROJECT: path.join(root, 'back/tsconfig.json'),
LANGUAGE_STATE: state,
},
});
assert.deepEqual(JSON.parse(result.stdout), {
initialWorkers: 2,
updatedWorkers: 3,
replacementRestored: true,
});
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});