From 384568d4bb07d5a472bc588523a435a737021646 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sun, 23 Aug 2026 13:20:00 +0800 Subject: [PATCH] chore(design): audit every locale, and measure CJK in CJK bin/design-audit.mjs is the only thing that measures the rendered pages, so its blind spots are the design system's blind spots. It had two. Its page list was six English routes plus /zh-CN/, leaving five languages unmeasured. It now derives the list from LOCALES in build-pages.mjs -- six page shapes across all seven languages, plus the three untranslated pages -- so the eighth language will be audited the day it lands. English stays the default; --all expands to the full matrix. More importantly, the character probe was hard-coded Latin. Every language therefore reported 72 characters per line, which looked like perfect consistency but was really the container width restated in Latin units. The CJK pages run at 41 full-width characters and that number had never appeared in any report. The probe now follows . Measured: Latin 70-72 (band 45-75, fine); CJK 40-41 (band 30-40, one over). Left as is -- 660 / 16px is 41.25, and pulling only the paragraphs in would break their alignment with the headings, rules and tables the whole design hangs on. Recorded in docs/design-system.md so the next person to touch the type scale knows it is already at the ceiling. Two contract tests pin the script's own coverage. Reverse-verified: stashing the design-audit change turns both red. Co-Authored-By: Claude Opus 5 --- bin/design-audit.mjs | 50 +++++++++++++++---- docs/design-system.md | 16 +++++- .../2026-08-23-design-audit-all-locales.md | 50 +++++++++++++++++++ .../2026-08-23-seven-languages-audit.md | 4 +- test/unit/design-contract.test.ts | 21 ++++++++ 5 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 docs/explorations/2026-08-23-design-audit-all-locales.md diff --git a/bin/design-audit.mjs b/bin/design-audit.mjs index 87fae7ed..505758c1 100644 --- a/bin/design-audit.mjs +++ b/bin/design-audit.mjs @@ -8,30 +8,52 @@ * arrives one page at a time, each one defensible on its own, and it is only * visible side by side. Run it after adding a page or touching a stylesheet: * - * pnpm run preview # or any server for the built site + * pnpm run preview # or any server for the built site * node bin/design-audit.mjs [baseUrl] + * node bin/design-audit.mjs [baseUrl] --all # every page in every language * * The reference scale is in docs/design-system.md. Nothing here fails a build: * a number outside the range may be right for that page, and the audit exists * to make the choice deliberate, not to forbid it. */ import { chromium } from '@playwright/test'; +import { LOCALES } from './build-pages.mjs'; const base = process.argv[2] ?? 'http://localhost:4173'; +/** `--all` measures every locale; by default only English, which is the scale. */ +const allLocales = process.argv.includes('--all'); -const PAGES = [ - ['/', 'home EN'], - ['/zh-CN/', 'home ZH'], +/** + * One row per page shape, in English -- the scale the design system is written + * against. With --all, each shape is measured in every language the site ships: + * a measure that is 66 characters in English can be something else entirely in + * German (long compounds) or Japanese (no spaces, wider glyphs), and until this + * existed nobody was looking. + */ +const SHAPES = [ + ['/', 'home'], ['/help', 'help'], ['/changelog', 'changelog'], ['/private-document-editor', 'landing (satellite)'], ['/open/docx', 'open/'], ['/convert/docx-to-pdf', 'convert/'], +]; +const UNTRANSLATED = [ ['/history', 'history'], ['/404.html', '404'], ['/embed-demo.html', 'embed-demo'], ]; +const localePath = (locale, path) => (locale === 'en' ? path : `${LOCALES[locale].prefix}${path === '/' ? '/' : path}`); +const PAGES = allLocales + ? [ + ...Object.keys(LOCALES).flatMap((locale) => + SHAPES.map(([path, label]) => [localePath(locale, path), `${label} [${locale}]`]), + ), + ...UNTRANSLATED, + ] + : [...SHAPES, ...UNTRANSLATED]; + /** Everything measured inside the page, in one pass. */ function probe() { const cs = (el) => (el ? getComputedStyle(el) : null); @@ -49,13 +71,23 @@ function probe() { .filter((p) => (p.textContent || '').trim().length > 80 && p.offsetParent) .sort((a, b) => (b.textContent || '').length - (a.textContent || '').length)[0]; + // Measure with the script the page is actually set in. A Latin probe on a + // Japanese page just restates the container width in Latin units, which is + // how the audit read the same 72 for all seven languages while the CJK pages + // were really running ~33 characters. The bands differ too: 45-75 for Latin, + // 30-40 for CJK (JLReq / CLReq). + const script = /^(zh|ja|ko)/.test(document.documentElement.lang || 'en') ? 'cjk' : 'latin'; + const PROBE = { + latin: 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ', + cjk: '\u6587\u5b57\u6392\u7248\u884c\u9577\u6e2c\u5b9a\u7528\u4f8b\u6a19\u672c\u6c49\u5b57\u5bbd\u5ea6', + }[script]; let chars = null; if (para) { const probeEl = document.createElement('span'); probeEl.style.cssText = `position:absolute;visibility:hidden;white-space:pre;font:${cs(para).font}`; - probeEl.textContent = 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + probeEl.textContent = PROBE; document.body.appendChild(probeEl); - const avg = probeEl.getBoundingClientRect().width / probeEl.textContent.length; + const avg = probeEl.getBoundingClientRect().width / PROBE.length; probeEl.remove(); chars = Math.round(para.getBoundingClientRect().width / avg); } @@ -64,7 +96,7 @@ function probe() { const bar = document.querySelector('header.bar'); return { container: Math.round(container.getBoundingClientRect().width), - chars, + chars: chars === null ? null : `${chars}${script === 'cjk' ? ' cjk' : ''}`, body: px(cs(document.body).fontSize), lh: px(cs(document.body).lineHeight), h1: h1 ? `${px(cs(h1).fontSize)}/${cs(h1).fontWeight}` : '-', @@ -77,7 +109,7 @@ const browser = await chromium.launch(); const page = await (await browser.newContext({ viewport: { width: 1440, height: 900 } })).newPage(); const COLS = [ - ['page', 22], + ['page', 26], ['container', 11], ['chars/line', 12], ['body', 6], @@ -96,7 +128,7 @@ for (const [url, label] of PAGES) { await page.waitForTimeout(250); m = await page.evaluate(probe); } catch { - console.log(label.padEnd(22) + '(unreachable)'); + console.log(label.padEnd(26) + '(unreachable)'); continue; } const cells = [label, `${m.container}px`, m.chars ?? '-', m.body, m.lh, m.h1, m.bar, m.footer]; diff --git a/docs/design-system.md b/docs/design-system.md index b8fa8dfa..45edbc1f 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -4,7 +4,8 @@ 适用:所有用户可见页面(落地页、生成页、`/history`、demo、404)。编辑器本身(`/editor`) 是全屏应用,不在此列。 -审计脚本:`node bin/design-audit.mjs `——渲染每个页面,打印下面这些数字。 +审计脚本:`node bin/design-audit.mjs `——渲染每个页面,打印下面这些数字; +加 `--all` 则把每种页型在**七种语言**下各量一遍(默认只量英文,那是本规范写作时的基准)。 一致性应该是可核对的,而不是靠记忆:漂移都是一页一页来的,每一页单看都合理, 只有并排列出来才看得见。 @@ -31,6 +32,19 @@ Notion 正文 708)。**不存在"一个正确的宽度",只存在按内容 当前实测(1440 视口):首页 71、`/help` 72、落地页 72、`/history` 66、404 59。 全部在区间内。 +### 中日韩另有一档:30–40 全角字符 + +拉丁字母的 45–75 换算成中日韩就是 22–37 全角字(一个汉字约等于两个字母宽), +业界惯例(JLReq / CLReq、以及知乎/少数派一类中文长文站)给的是 **30–40**。 +**审计脚本按 `` 选探针**:中日韩页面用汉字量、拉丁页面用字母量, +输出带 `cjk` 后缀。这一条不是多余的讲究——探针写死成字母时,七种语言全部报 72, +那个数字其实只是"容器宽度换算成字母",中日韩页面真实跑在 41,审计等于没审。 + +当前实测:中日韩内容页 **40–41 全角字**(660 ÷ 16px 字号 = 41.25),首页同值。 +比 40 高出 1,**刻意不改**:要压到 40 以内只能单独收窄段落,而整站的标题、发丝线、 +表格都对齐在 660 上,段落缩进去会让右边缘参差,代价大于收益。这个数字记在这里, +是为了下次有人动字号或列宽时知道它已经贴着上沿。 + ### 内容页在宽屏上是两栏,不是一根柱子 `≥1081px` 时内容页(`/open/*`、`/convert/*`、特性页、`/help`、`/changelog`)用 diff --git a/docs/explorations/2026-08-23-design-audit-all-locales.md b/docs/explorations/2026-08-23-design-audit-all-locales.md new file mode 100644 index 00000000..9bfc9006 --- /dev/null +++ b/docs/explorations/2026-08-23-design-audit-all-locales.md @@ -0,0 +1,50 @@ +# 版式审计补上另外六种语言,并按语言换探针 + +日期:2026-08-23 +起因:[七语言审计](2026-08-23-seven-languages-audit.md)留下的第一条"还没做的"—— +`bin/design-audit.mjs` 只量英文页面,另外六种语言的行宽从来没被量过。 + +## 做了什么 + +1. **页型 × 语言**。页面清单不再手写路由,改成从 `bin/build-pages.mjs` 的 `LOCALES` + 派生:六种页型(首页 / help / changelog / 特性页 / open / convert)在七种语言下各量一遍, + 加上三张不翻译的页(`/history`、`/404`、`/embed-demo`)。默认仍只量英文(那是规范 + 写作时的基准),`--all` 才展开成 45 行。以后加第八种语言,审计自动跟上。 +2. **探针按 `` 换**。这才是关键的一步。 + +## 探针写死成拉丁字母时,审计是假绿的 + +脚本量行宽的办法是:造一个隐藏 ``,抄段落的 `font`,塞一串字符, +用总宽除以字符数得到平均字宽,再拿段落宽度去除。那串字符原本硬编码成 +`abcdefghijklmnopqrstuvwxyz ABC...`。 + +于是七种语言全部报 **72 字符**——看起来完美一致,其实那个数字只是 +"660px 容器换算成拉丁字母",跟页面里排的是什么字毫无关系。中日韩页面 +真实跑在 **41 个全角字**,而它从未出现在任何一份报告里。 + +改成按 `document.documentElement.lang` 选探针(`^(zh|ja|ko)` 用汉字串), +输出带 `cjk` 后缀。 + +## 量出来的结果 + +| 页型 | 拉丁(en/de/es/pt) | 中日韩(zh-CN/ja/ko) | +| --------------- | ------------------- | --------------------- | +| 首页(1152 框) | 71 | 41 全角 | +| 内容页(660) | 70–72 | 40–41 全角 | + +拉丁全部落在 45–75 区间内。中日韩的业界区间是 **30–40**(JLReq / CLReq), +实测 40–41,**高出 1,刻意不改**:660 ÷ 16px 字号 = 41.25,要压到 40 以内 +只能单独收窄段落,而整站的标题、发丝线、表格都对齐在 660 上,段落缩进去 +右边缘就参差了。数字记在 `docs/design-system.md` 里,下次动字号或列宽时 +知道它已经贴着上沿。 + +除此之外没有任何一种语言把版式撑出区间——德语的长复合词、日语的无空格 +换行都没有造成溢出(那部分本来就有 E2E 的溢出检查兜着)。 + +## 用例与反向验证 + +`test/unit/design-contract.test.ts` 新增两条,钉住审计脚本自己的覆盖面: +清单必须从 `LOCALES` 派生、必须有 CJK 探针分支。 + +反向验证:`git stash` 掉 `bin/design-audit.mjs` 的改动(退回英文清单 + 拉丁探针), +两条用例同时变红;`stash pop` 后恢复绿。 diff --git a/docs/explorations/2026-08-23-seven-languages-audit.md b/docs/explorations/2026-08-23-seven-languages-audit.md index d3ac5442..fd2b3b7a 100644 --- a/docs/explorations/2026-08-23-seven-languages-audit.md +++ b/docs/explorations/2026-08-23-seven-languages-audit.md @@ -111,7 +111,7 @@ E2E 的第 4/5 步立刻回落英文。 ## 还没做的 -- **`bin/design-audit.mjs` 只审英文页面**。七种语言的行宽/字号没有被审计脚本覆盖, - 目前靠 E2E 的溢出检查兜底。 +- ~~**`bin/design-audit.mjs` 只审英文页面**~~ 已补,见 + [2026-08-23-design-audit-all-locales.md](2026-08-23-design-audit-all-locales.md)。 - **`/history` 与 `/404` 仍是手写页**。它们的 chrome 与生成页重复了一份,这次的语言列表 漏更新就是这个重复的代价。要么把它们也纳入生成器,要么就靠刚加的那条单测钉着。 diff --git a/test/unit/design-contract.test.ts b/test/unit/design-contract.test.ts index 0ade9061..51acd921 100644 --- a/test/unit/design-contract.test.ts +++ b/test/unit/design-contract.test.ts @@ -163,3 +163,24 @@ describe('page chrome', () => { } }); }); + +/** + * The audit script is the only thing that measures the rendered pages, so a + * blind spot in it is a blind spot in the design system. It had one: the page + * list was six English routes plus `/zh-CN/`, which left five languages + * unmeasured, and the character probe was hard-coded Latin, which made the CJK + * pages report a Latin-equivalent number (72) instead of the 41 full-width + * characters they actually run at. + */ +describe('design audit coverage', () => { + const src = readFileSync(resolve(__dirname, '../../bin/design-audit.mjs'), 'utf8'); + + it('derives its page list from the locale table, so a new language is audited on arrival', () => { + expect(src).toMatch(/import \{ LOCALES \} from '\.\/build-pages\.mjs'/); + expect(src).toMatch(/Object\.keys\(LOCALES\)/); + }); + + it('measures CJK pages in full-width characters, not Latin ones', () => { + expect(src).toMatch(/\/\^\(zh\|ja\|ko\)\//); + }); +});