Files
houseplan-card/test/i18n-lazy-namespaces.test.mjs
Claude 82a48ee0ce perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
2026-09-24 14:51:18 +03:00

108 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// #627 AC3: словари пространств `settings`, `support`, `topology` — английский
// статически (синхронный слой отката), ru/de/fr — отдельные ленивые чанки, по
// одному на пару «пространство × язык», у каждого загрузчик с двумя попытками.
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { NAMESPACE_LOCALE_CHUNKS } from '../scripts/bundle-manifest.mjs';
const NAMESPACES = ['settings', 'support', 'topology'];
const LAZY_LANGUAGES = ['ru', 'de', 'fr'];
const source = (path) => readFileSync(new URL(`../src/i18n/${path}`, import.meta.url), 'utf8');
const json = (path) => JSON.parse(source(path));
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
test('#627 AC3 ни один ru/de/fr словарь пространства не импортируется статически', () => {
for (const namespace of NAMESPACES) {
const text = source(`${namespace}.ts`);
const staticImports = [...text.matchAll(/^import\s[^;]*?from\s+['"]([^'"]+)['"]/gm)]
.map((match) => match[1]);
assert.ok(staticImports.includes(`./${namespace}/en.json`),
`${namespace}: английский слой отката обязан быть статическим`);
for (const language of LAZY_LANGUAGES) {
const lazy = new RegExp(`^\\./${namespace}/(?:${language}\\.json|${namespace}-${language})$`);
assert.deepEqual(staticImports.filter((path) => lazy.test(path)), [],
`${namespace}/${language}: статический импорт вернул бы словарь в граф редактора ×4`);
}
// Ни одного JSON, кроме английского, в модуле пространства вообще.
assert.deepEqual(staticImports.filter((path) => path.endsWith('.json') && path !== `./${namespace}/en.json`), []);
}
});
test('#627 AC3 у каждого из девяти чанков — загрузчик с двумя попытками и свой retry-токен', () => {
assert.equal(NAMESPACE_LOCALE_CHUNKS.length, NAMESPACES.length * LAZY_LANGUAGES.length);
for (const namespace of NAMESPACES) {
const text = source(`${namespace}.ts`);
for (const language of LAZY_LANGUAGES) {
const entry = NAMESPACE_LOCALE_CHUNKS.find((candidate) => candidate.namespace === namespace
&& candidate.language === language);
assert.ok(entry, `${namespace}-${language}: запись манифеста`);
assert.equal(entry.module, `/src/i18n/${namespace}/${namespace}-${language}.ts`);
assert.match(text, new RegExp(
`${language}: \\(attempt\\) => \\(attempt === 0 \\? import\\('${escape(`./${namespace}/${namespace}-${language}`)}'\\)\\n`
+ `\\s+: import\\(/\\* @vite-ignore \\*/ retryUrl\\('${entry.token}'\\)\\)\\)`,
), `${namespace}-${language}: первая попытка — ребро rollup, вторая — точный hashed-адрес`);
assert.equal(text.split(entry.token).length - 1, 1, `${entry.token}: ровно одно вхождение`);
// Чанк несёт свой язык и отпечаток сборки.
const loader = new URL(`../src/i18n/${namespace}/${namespace}-${language}.ts`, import.meta.url);
assert.ok(existsSync(loader), `${namespace}-${language}.ts существует`);
const loaderText = readFileSync(loader, 'utf8');
assert.match(loaderText, new RegExp(`^import dictionary from '\\./${language}\\.json' with \\{ type: 'json' \\};`, 'm'));
assert.match(loaderText, /export const fingerprint = '__HOUSEPLAN_SOURCE_FINGERPRINT__';/);
}
}
});
test('#627 рантайм пространства: en готов сразу, ru/de/fr — после ensure и ровно своим словарём', async () => {
const modules = {
settings: await import('../test-build/i18n/settings.js'),
support: await import('../test-build/i18n/support.js'),
topology: await import('../test-build/i18n/topology.js'),
};
const runtimes = {
settings: modules.settings.SETTINGS_LANGUAGE_RUNTIME,
support: modules.support.SUPPORT_LANGUAGE_RUNTIME,
topology: modules.topology.TOPOLOGY_LANGUAGE_RUNTIME,
};
const translate = {
settings: modules.settings.settingsT,
support: modules.support.supportT,
topology: modules.topology.topologyT,
};
for (const namespace of NAMESPACES) {
const runtime = runtimes[namespace];
const english = json(`${namespace}/en.json`);
assert.equal(runtime.state('en'), 'ready', `${namespace}: английский статический`);
for (const language of LAZY_LANGUAGES) {
const expected = json(`${namespace}/${language}.json`);
const key = Object.keys(expected).find((candidate) => expected[candidate] !== english[candidate]
&& !/\{/.test(expected[candidate]));
assert.ok(key, `${namespace}/${language}: есть переведённый ключ без плейсхолдеров`);
assert.equal(runtime.state(language), 'pending', `${namespace}/${language}: не загружен до ensure`);
assert.equal(translate[namespace](language, key), english[key],
`${namespace}/${language}: до загрузки — синхронный английский откат, не сырой ключ`);
await runtime.ensure(language);
assert.equal(runtime.state(language), 'ready');
assert.deepEqual(runtime.dictionary(language), expected,
`${namespace}/${language}: загружен словарь своего языка`);
assert.equal(translate[namespace](language, key), expected[key]);
}
}
});
test('#627 рантайм пространства — тот же проверенный класс и тот же слушатель отказа', async () => {
const { LanguageRuntime } = await import('../test-build/i18n/language-runtime.js');
const namespaceSource = source('namespace-language.ts');
assert.match(namespaceSource,
/\], BUILD_FINGERPRINT, console\.warn, notifyLanguageLoadFailures\);/,
'отказ словаря пространства идёт тем же тостом, что и основного каталога');
for (const [path, name] of [
['settings.js', 'SETTINGS_LANGUAGE_RUNTIME'], ['support.js', 'SUPPORT_LANGUAGE_RUNTIME'],
['topology.js', 'TOPOLOGY_LANGUAGE_RUNTIME'],
]) {
const module = await import(`../test-build/i18n/${path}`);
assert.ok(module[name] instanceof LanguageRuntime, `${name}: LanguageRuntime, а не двойник`);
}
});