Files
aurora-admin 5ee5d829a4
Regression / regression (push) Canceled after 0s
refactor(详情页+S8): 详情页减重(导航互斥/入口去重/i18n去重) + 品牌标识入库
本提交含两条并行工作线,因互相咬合(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 断言其互斥),未删。
2026-09-22 23:56:05 +08:00

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`);