Skip to content

zhconvert: correct five mechanical s2twp over-substitutions, and normalize 腳本 - #204

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-194-twp-artifacts
Sep 3, 2026
Merged

hotlong merged 1 commit into
mainfrom
claude/issue-194-twp-artifacts

Conversation

@hotlong

@hotlong hotlong commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #194

s2twp's TWPhrases group carries a handful of entries that are not contested vocabulary at all. They append or swap a word fragment in a way that is wrong in Taiwanese usage in any register, for any audience — the same shape as the 許可權 defect PR #192 repaired, only rarer. This corrects the five of them that fire in this corpus, and normalizes 脚本 in the same pass.

zh-Hans twp shipped now what was wrong n
全局 全域性 全域 a spurious 性 turns a noun into an adjective 4
扩展 擴充套件 擴充 a spurious 套件 ("package"); every use here is the verb 4
适配器 介面卡 配接器 介面卡 is a network interface card — different hardware 3
控件 控制元件 控制項 the standard UI-control term is 控制項 2
镜像 映象 映像 wrong second character 1
脚本 指令碼 腳本 see below — the corpus shipped both 26

Six lines in TWP_PHRASE_OVERRIDES and 18 fixture cases in CONVERSION_CASES, both in src/lib/zhconvert.ts. Everything under content/ is regenerated output; nothing there is hand-edited.

脚本 is a defect, not only a term choice

指令碼 is the Microsoft-glossary rendering and 腳本 the ordinary Taiwanese word, but the reason this could not wait for a vocabulary round is that the corpus already shipped both — 指令碼 26, 腳本 5. That split is the straddling-phrase accident #192 diagnosed for the locative 里: the two-character STPhrases entry 本里 matches across the word boundary in 脚本里, which blocks the phrase layer as well as the character layer, so five sites escaped the substitution. The identity entry settles every site the same way, straddled or not.

The mechanism was checked before it was used, not after

#192's shadow works because TWPhrases is the only dictionary in the chain that can emit the wrong string, and its entries are keyed on the post-s2t form. Both halves were verified for all six against the shipped opencc-js dictionaries rather than assumed:

=== which dictionaries can EMIT each wrong string ===
  全域性    TWPhrases      1     (全局 -> 全域性)
  擴充套件  TWPhrases      1     (擴展 -> 擴充套件)
  介面卡    STPhrases      1     (介面卡 -> 介面卡)   <- identity
  介面卡    TWPhrases      1     (適配器 -> 介面卡)
  控制元件  TWPhrases      1     (控件 -> 控制元件)
  映象      TWPhrases      2     (鏡像 -> 映象, 顯像管 -> 映象管)
  指令碼    TWPhrases      1     (腳本 -> 指令碼)

Two things fell out of that check and are recorded in the code:

  • 介面卡 also exists in STPhrases, as an identity entry. That does not defeat the shadow — an identity entry can preserve a genuine NIC but can never introduce one — and it is the reason this is repaired in the phrase layer instead of by a post-pass. A post-pass rewriting 介面卡 → 配接器 after conversion could not tell a converted 适配器 from a source 介面卡; the shadow never sees the genuine one. There is a fixture case pinning exactly that (['介面卡', '介面卡']), the counterpart of gen-zh-hant: ship 權限/實例 and the straddled locative 裡 #192's ['许可权', '許可權'].
  • TWPhrases carries the same 象/像 slip twice, in 顯像管 → 映象管. Left alone deliberately: no source in this corpus writes 显像管, so it emits nothing (grep -roh '映象管' dist | wc -l → 0), and a CRT is not this card's business. Noted in the comment beside the 鏡像 override so it need not be rediscovered.

Four of the six are corrected to the ordinary term rather than merely disabled — an identity entry on 全局 would have left the mainland spelling standing, which is its own defect.

Every changed byte is accounted for

Not "the diff looks right": each of the 18 changed files was taken as it stands on origin/main, had only the six ruled substitutions applied, and was required to be byte-identical to the regenerated file. One unexplained character would fail its file.

OK   content/blog/forward-deployed-engineer-tools/index.zh-Hant.mdx  [介面卡->配接器 x3]
OK   content/blog/objectos-automation-engine/index.zh-Hant.mdx       [指令碼->腳本 x7]
OK   content/blog/low-code-vs-ai-native-app-platform/index.zh-Hant.mdx [控制元件->控制項 x1, 指令碼->腳本 x1]
…
18 changed content file(s), 40 ruled substitution(s), 0 unexplained.

40 = 4 + 4 + 3 + 2 + 1 + 26, the six counts in the table.

Counts, in dist and not only in the MDX

#192's patch round is the reminder that those are two different populations, so both were measured. In the built output:

before after before after
全域性 6 0 全域 0 6
擴充套件 5 0 擴充 0 5
介面卡 18 0 配接器 0 18
控制元件 3 0 控制項 2 5
映象 6 0 映像 0 6
指令碼 35 0 腳本 5 40

Two things to read carefully in that table. The right-hand "before" is the standalone count: a bare grep -c 全域 on main's dist returns 6 and grep -c 擴充 returns 5, but every one of those is the artifact itself — 全域 is a substring of 全域性, 擴充 of 擴充套件 — so the standalone term appeared zero times before and appears at the artifact's old count after. And the 介面卡 18 from 3 source occurrences, like 指令碼 35 from 26, is chrome amplification: one hit sits in a post description and another in a title, so they reach the cards, the topic hubs, rss.xml and <title>.

控制項 going 2 → 5 rather than 0 → 3 is the point of that row: the corpus was already emitting 控制項 from a source 控制项 while emitting 控制元件 from 控件. The override closes that split, the way the 脚本 entry closes its own.

In the generated MDX corpus, the acceptance numbers exactly: 全域性 4→0, 擴充套件 4→0, 介面卡 3→0, 控制元件 2→0, 映象 1→0, 指令碼 26→0, and 腳本 5 → 31, matching 脚本 in the zh-Hans sources.

Verification

All five gates via scripts/pm/os-verify-lock.sh, exit captured before any pipe. Re-run in full at f650569, the branch head, after merging origin/main — #202 landed a new content-lint rule mid-round and this branch regenerates 18 files that rule inspects, so the whole set was taken again rather than carried over from the pre-merge run:

gate verdict at f650569
pnpm gen:zh-hant VERDICT command-exit 0 · held the lock 2s
pnpm check VERDICT command-exit 0 · held the lock 11s — Result (135 files): 0 errors, 0 warnings, 0 hints
pnpm content:lint --published VERDICT command-exit 0 — ✓ content lint passed (335 files, 44 glossary terms checked)
pnpm build VERDICT command-exit 0 · held the lock 66s — ✓ content lint --dist passed (474 built blog pages checked) (the --dist pass from #201, green; and #202's new rule, green)
pnpm seo:smoke VERDICT command-exit 0 — SEO smoke test passed (866 HTML pages checked)

git status --porcelain is empty after pnpm build — the Traditional locale stays fully generated. #187's link rewrite survives: grep -rn '/zh-Hans/' content/blog --include=index.zh-Hant.mdx is 0. The dist counts above were re-measured at f650569 and are unchanged.

Fixture ablation. Dropped ['適配器', '配接器'] and re-ran the generator. The mutation was proven on disk before anything was read — the anchor count went 1 → 0 and the blob hash moved 767ba60… → e15fe3a…, both checked, with a trap holding the restore:

✗ gen-zh-hant: fixture failed
  [conversion]  in: "适配器"      expected: "配接器"  actual: "介面卡"
  [conversion]  in: "接入适配器"  expected: "接入配接器"  actual: "接入介面卡"
gen exit: 1

Restored and proven restored: on-disk blob back to 767ba60… = the HEAD blob, git diff HEAD empty, generator green again, tree clean. No rebuild step sits between the edit and the reading — scripts/gen-zh-hant.mjs imports src/lib/zhconvert.ts directly via Node type stripping — so there is no stale-dist hazard to defeat this one.

Browser pass, chrome and article body, 1440×900 and 390×844. The fix that stopped at content/ is exactly what #192's patch round existed to repair, so this was checked on both halves. src/lib/zhconvert.ts derives the UI strings too:

page what it carries 1440×900 390×844
/zh-Hant/security/ 映像, derived by s2t from src/lib/security.ts — a UI string, in no MDX wrong=[] right=[映像] wrong=[] right=[映像]
/zh-Hant/blog/ 配接器 in the FDE card, from that post's description wrong=[] right=[配接器] wrong=[] right=[配接器]
/zh-Hant/blog/forward-deployed-engineer-tools/ 配接器 in the article body wrong=[] right=[配接器] wrong=[] right=[配接器]

wrong is all six artifact spellings, right the intended forms, both read off rendered innerText. No horizontal overflow and no console errors on any of the six loads; the nav renders ObjectOS 產品 開發者 資源 定價 繁體中文 開始使用. Every replacement is the same length or shorter than what it replaces, so there was no layout risk to begin with — measured anyway.

Scope

Only the six ruled terms. The 15 contested renderings in #195 — 元数据 → 後設資料 and the rest — are untouched, as are the two semantic collapses it records.

A sweep of all 143 TWPhrases entries that fire in this corpus turned up no further artifact of this class: every remaining candidate (局部 → 區域性, 回调 → 回撥, 抛出 → 丟擲, 命名空间 → 名稱空間, 优先级 → 優先順序, 表达式 → 表示式, 外键 → 外部索引鍵) is already a row in #195's table. One term the survey missed, 连接器 → 聯結器 at 17 source occurrences, is reported in #203 — contested rather than mechanical, so out of scope here and not touched.

🤖 Generated with Claude Code

Generated by Claude Code

…e 腳本

`s2twp`'s TWPhrases group carries a handful of entries that are not contested
vocabulary. They append or swap a word fragment in a way that is wrong in
Taiwanese usage in any register: 全局 → 全域**性** turns a noun into an
adjective, 扩展 → 擴充**套件** glues on "package" where every use here is the
verb, 控件 → 控制**元件** where the standard UI-control term is 控制項, 镜像 →
映**象** swaps the second character, and 适配器 → **介面卡** substitutes
different hardware entirely — 介面卡 is a network interface card.

Same shape as the 許可權 defect fixed for #144, and the same mechanism: an entry
at the head of the TWPhrases group, keyed on the post-s2t form. Verified before
relying on it — TWPhrases is the only dictionary in the chain that can emit any
of these strings; the one hit elsewhere is STPhrases' identity entry
介面卡 → 介面卡, which preserves a genuine NIC but can never introduce one.
That asymmetry is also why these are repaired in the phrase layer and not by a
post-pass, which could not tell a converted 适配器 from a source 介面卡.

Also normalizes 脚本 to 腳本 rather than 指令碼. Not only a term choice: the
corpus already shipped both (指令碼 26, 腳本 5), because the STPhrases entry
本里 matches across the word boundary in 脚本里 and blocks the phrase layer, so
a handful of sites escaped the substitution. Whichever term wins, that split had
to close in the same pass.

Six fixture cases per term — bare, in the corpus's own phrase shapes, and with
negative cases pinning that a genuine 介面卡 and a source 控制项 are untouched —
so a preset upgrade cannot silently reinstate any of this.

content/ is regenerated output only, nothing hand-edited: 18 files, 40
substitutions, every changed byte reproduced by applying the six ruled terms
alone to the origin/main version and requiring byte equality.

Fixes #194

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — re-verified against current main, with the seat's own diff-scope check.

main moved to 8a36292 while this was in flight (#202 landed), so the union was rebuilt on it. All five gates through the shared lock: content:lint 0, content:lint --published 0, check 0, build 0 (867 page(s) built · ✓ content lint --dist passed (474 built blog pages checked)), seo:smoke 0, tree clean.

Diff scope re-derived from scratch, not read off the PR: each of the 18 files taken as it stands on main, only the six ruled substitutions applied, byte equality required.

zh-Hant files changed: 18 ; unexplained: 0

And the artifacts are gone from the built output, chrome included:

in dist
全域性 · 擴充套件 · 介面卡 · 控制元件 · 映象 · 指令碼 0 each
映象管 (the second 象/像 slip, deliberately left) 0 — emits nothing, as you said
全域 6 · 擴充 5 · 配接器 11 · 控制項 5 · 映像 4 · 腳本 17 pages carrying the correct form

配接器 reaches 2 zh-Hant topic hubs — chrome amplification confirmed from the other side, and the form that arrives there is the right one. #187's link rewrite survives (/zh-Hans/ still 0) and #192's own terms are untouched (許可權 0, 例項 0, 權限 across 113 pages).

Three things in this PR are why it needed no round trip.

You checked the mechanism before relying on it, and the check found something. #192's shadow works only because TWPhrases is the sole dictionary that can emit the wrong string. Dumping that for all six rather than assuming it turned up 介面卡 living in STPhrases too — as an identity entry. Your reading is right: an identity entry can preserve a genuine network interface card but can never introduce one, so the shadow holds — and that is precisely why this belongs in the phrase layer rather than a post-pass, because a post-pass could not tell a converted 适配器 from a source 介面卡. Pinning ['介面卡','介面卡'] as the counterpart to #192's ['许可权','許可權'] is the right instinct: the negative property is the one worth defending.

Four of the six are corrected, not merely disabled. An identity entry on 全局 would have left the mainland spelling standing, because disabling that rule means no conversion at all rather than the right one. Easy to get wrong, and getting it wrong would have traded one defect for a quieter one.

The 顯像管 → 映象管 slip is left alone with evidence. Same 象/像 error, second occurrence, proven to emit nothing in this corpus, noted beside the 鏡像 override so nobody rediscovers it. That is the correct disposition for a real defect that is not this card's — and the note is what stops it costing someone an hour later.

One correction on my side, from your table. I read 全域 = 6 in my own earlier measurement as "six correct uses now appearing". Your framing is the accurate one: 全域 is a substring of 全域性, so a bare grep on main matched the artifact itself. The standalone term was genuinely at zero and now sits at the artifact's old count. Same for 擴充 inside 擴充套件. My number was right and my reading of it was loose; the PR's is the one that belongs in the record.

Landing on main. #195 — the 14 contested terms needing a native reviewer — is untouched and stays with the maintainer.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 3, 2026 04:54
@hotlong
hotlong merged commit c6ab426 into main Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Five more s2twp over-substitutions are wrong in any register — 介面卡 is a network interface card, not an adapter

2 participants