Regression / regression (push) Canceled after 0s
本提交含两条并行工作线,因互相咬合(package.json scripts、regression.yml、 npm run build 链)无法按文件干净拆分,故合并为一次自洽提交。 ## 详情页减重(本轮主任务:修复「AI 干太重」) - 导航收敛:宽屏只用右侧目录、≤1200px 只用页内 sticky 导航,纯 CSS 媒体查询 实现(不引入 JS 宽度监听)。此前两套导航同时可见,active 状态互相打架。 - 入口去重:标题区由「查看示例/在线测试/契约 JSON」三个减为「在线测试」一个; 契约 JSON 归入实现资源;删除示例底部重复的「在线测试」。 - 示例工具栏:删除与目录锚点重复的示例下拉选择器;「全部展开代码」只在 示例数 >1 时出现(103 个组件里 49 个仅 1 个示例,此前恒显示)。 - 重复文案:示例区两句同义导语合并为一句。 - 首页 CTA 由 5 个减为 2 个(浏览组件/快速开始),测试总览入口挂到已有的 通过率统计卡上,不再另占 Hero 按钮。 - 统一详情取数:抽出 fetchDetail/loadDetail 作为 details/*.json 的唯一路径, FAQ 不再自行 fetch 一遍,与组件页共用缓存与失败兜底。 ## i18n - 删除 12 组重复键(含整段 FAQ 说明),字典 604 → 585 唯一键。 - 删除本次改动产生的 6 个死键。 - verify-i18n.mjs 新增 `unique dictionary keys` 断言:重复键在对象字面量里 是静默的后值覆盖,此前无从发现;现由门禁拦住。 ## 文档事实修正(实测为准) - TESTING/CONTRIBUTING:79 → 103 组件;旧断言数改为回指 tests/report.json。 - PLATFORMS:移动端 108 文件/18 端 → 282 文件/47 端;契约 5 → 47; 令牌 17 → 15;uni-app SFC 21 → 50。 - AGENTS:断言 1405/18 页 → 1464/103 页(PC)、807/47 页(移动端)。 - package.json:YOUR-ACCOUNT 占位 → gitea 实址与 kole-ui.mymoyu.top。 ## 品牌标识(并行会话成果,一并入库) - brand-mark.json 收归真源,build:brand 生成 favicon 与单色 SVG; PC 与移动端共用资产,verify:brand 24 条断言。 - regression.yml 增加 verify:brand 步骤。 ## 门禁与验收 新增工具:verify-component-page.mjs(86 条真实浏览器断言,随详情页改造同步 更新为「只允许一套导航可见」)、verify-brand-mark.mjs、lib/i18n-dead-keys.mjs (只读诊断)。 回归:PC 100%(1464/1464,103 页,N/A 50)· 移动端 100%(807/807,47 页)。 门禁:component-page 86 · i18n 17 · brand 24 · routes all · smoke all · theme OK · isolation 31 · nav all · examples 9 · api-docs 10 · mobile-docs 12。 已知未做:i18n 另有约 44 条历史死键(非本次产生),已记为 ROADMAP S8-P6; 顶栏与悬浮区的两个主题入口为刻意设计(verify-theme 断言其互斥),未删。
77 lines
4.3 KiB
JavaScript
77 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* verify-i18n.mjs — static checks for the documentation site's zh-CN/en switch.
|
|
* No runtime dependencies; exits non-zero when a source key is missing or a
|
|
* module-level translated value can go stale after a language change.
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const appPath = join(ROOT, 'site', 'app.js');
|
|
const i18nPath = join(ROOT, 'site', 'i18n.js');
|
|
const indexPath = join(ROOT, 'site', 'index.html');
|
|
const app = readFileSync(appPath, 'utf8');
|
|
const i18n = readFileSync(i18nPath, 'utf8');
|
|
const index = readFileSync(indexPath, 'utf8');
|
|
|
|
const failures = [];
|
|
const checks = [];
|
|
function check(name, ok, detail = '') {
|
|
checks.push({ name, ok, detail });
|
|
if (!ok) failures.push(`${name}${detail ? `: ${detail}` : ''}`);
|
|
}
|
|
|
|
check('dictionary exists', /var\s+EN\s*=\s*\{/.test(i18n));
|
|
check('language storage key', /LANG_KEY\s*=\s*['"]kole-lang['"]/.test(i18n));
|
|
check('language values', /saved\s*===\s*['"]en['"]/.test(i18n) && /saved\s*===\s*['"]zh['"]/.test(i18n));
|
|
check('HTML lang sync', /setAttribute\(['"]lang['"],\s*lang\s*===\s*['"]en['"]\s*\?\s*['"]en['"]\s*:\s*['"]zh-CN['"]/.test(i18n));
|
|
check('language select markup', /id="lang-trigger"/.test(index) && /id="lang-menu"/.test(index)
|
|
&& /role="listbox"/.test(index) && /class="lang-opt"[^>]*data-lang="zh"/.test(index) && /class="lang-opt"[^>]*data-lang="en"/.test(index));
|
|
check('language select binding', /function\s+bindLangSelect\s*\(/.test(app)
|
|
&& /function\s+applyLang\(next\)[\s\S]*?I18N\.setLang\(next\)/.test(app)
|
|
&& /applyLang\(opt\.getAttribute\(['"]data-lang['"]\)\)/.test(app));
|
|
check('no legacy lang toggle left', !/lang-toggle/.test(index) && !/bindLangToggle/.test(app) && !/\.lang-zh\b/.test(index));
|
|
check('English document title', /Kole UI · Component Library/.test(app));
|
|
check('source-key navigation map', /var\s+NAV_KEY\s*=\s*\{[^}]*home:\s*['"]首页['"]/.test(app));
|
|
check('search stores category key', /return\s*\{\s*c:\s*c,[\s\S]*?category:\s*c\.category\s*\}/.test(app));
|
|
check('related translations evaluated at render', /why:\s*T\(rule\.why\)/.test(app));
|
|
check('theme labels evaluated at render', /function\s+themeMeta\s*\(\)[\s\S]*?T\(m\[1\]\)/.test(app));
|
|
check('theme presets evaluated at render', /function\s+themePresets\s*\(\)[\s\S]*?T\(p\.label\)/.test(app));
|
|
const relatedDecl = app.match(/var\s+RELATED_MAP\s*=\s*\[[\s\S]*?\n\s*\];/);
|
|
const themeDecl = app.match(/var\s+THEME_META_KEYS\s*=\s*\[[\s\S]*?\n\s*\];/);
|
|
check('no stale module-level translated caches', !(relatedDecl && /\bT\s*\(/.test(relatedDecl[0])) && !(themeDecl && /\bT\s*\(/.test(themeDecl[0])));
|
|
|
|
const dictBlock = i18n.match(/var\s+EN\s*=\s*\{([\s\S]*?)\n\s*\};/);
|
|
const dictionaryKeys = new Set();
|
|
const duplicatedKeys = [];
|
|
if (dictBlock) {
|
|
for (const match of dictBlock[1].matchAll(/^\s*(['"])((?:\\.|(?!\1)[\s\S])*?)\1\s*:/gm)) {
|
|
const key = match[2].replace(/\\(['"])/g, '$1');
|
|
/* 重复键在对象字面量里是「后值覆盖前值」:静默生效、无报错,历史上真的积累出
|
|
12 组(含整段 FAQ 说明),排查时完全看不见。这里显式拦住 —— 判据是「字典键唯一」,
|
|
发现重复即失败,而不是等某个语言下文案变成另一句。 */
|
|
if (dictionaryKeys.has(key)) duplicatedKeys.push(key);
|
|
dictionaryKeys.add(key);
|
|
}
|
|
}
|
|
check('unique dictionary keys', duplicatedKeys.length === 0, duplicatedKeys.length ? duplicatedKeys.join(' | ') : `${dictionaryKeys.size} unique keys`);
|
|
const sourceKeys = new Set();
|
|
for (const match of app.matchAll(/\bT\(\s*(['"])((?:\\.|(?!\1)[\s\S])*?)\1\s*\)/g)) {
|
|
sourceKeys.add(match[2].replace(/\\(['"])/g, '$1'));
|
|
}
|
|
const missing = [...sourceKeys].filter((key) => !dictionaryKeys.has(key));
|
|
check('literal T() coverage', missing.length === 0, missing.length ? missing.join(' | ') : `${sourceKeys.size} keys covered`);
|
|
|
|
const detailDir = join(ROOT, 'site', 'details');
|
|
check('detail payload directory', existsSync(detailDir));
|
|
|
|
for (const result of checks) {
|
|
console.log(`[i18n] ${result.ok ? 'OK' : 'FAIL'} ${result.name}${result.detail ? ` — ${result.detail}` : ''}`);
|
|
}
|
|
if (failures.length) {
|
|
console.error(`\n[i18n] ${failures.length} check(s) failed`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\n[i18n] OK — ${checks.length} checks passed`);
|