Point generated zh-Hant body links at the Traditional locale - #187
Conversation
OpenCC only rewrites Han characters, so an internal link authored in a Simplified post as `[标题](/zh-Hans/blog/some-slug/)` survived verbatim into index.zh-Hant.mdx and sent a Traditional reader to the Simplified companion post -- invisible to every gate, since the generated file was byte-for-byte what the generator produced. The generator now rewrites `/zh-Hans/...` to `/zh-Hant/...` on link targets in the body, but only where the Traditional target is provably built: blog posts are emitted per-locale with no fallback, so the rewrite is conditional on the Traditional file existing after the run, and topic hubs (whose locale set needs the term extractor) are never rewritten. A wrong-locale link is a defect; one the generator turned into a 404 would be worse, so unprovable targets are left alone and reported. Scoped, not global: fenced blocks and inline code spans are masked out using the same two patterns scripts/content-lint.mjs strips before it resolves links, frontmatter is excluded by construction, and slugs are never link targets. A fixture table pins both halves -- a body link rewritten, the identical string inside code left alone -- and runs on every dev/build, this repo having no test runner. 8 links across 5 generated Traditional posts move to the right locale; the other 41 regenerated files are byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
|
ACCEPT — reviewed against the branch, with the seat's own scoping probe. Union I at Independent target check. All eight rewritten links resolve to a Traditional file that exists — checked by walking the rewritten URLs back to disk rather than by trusting the gate:
The scoping and conditional demos, re-run by the seat on real content. Six Six for six. And the generator does not do this silently: That is ruling (1) working exactly as intended, and the reporting is what makes the withheld case reviewable instead of invisible. Note also that OpenCC converted the prose around the code span ( On the parts you declined to guess at. Not rewriting topic hubs — because their locale set lives in The 16-case fixture running inside the generator on every build is the right answer for a repo with no test runner, and it catching a bug in its own stub during development is the kind of evidence a fixture is supposed to produce. Your finding that the hand-written locales carry no cross-locale defect — all 20 non-self-locale links are Landing on Generated by Claude Code |
The generator converts zh-Hans to zh-Hant with OpenCC's `s2twp`. Two of that preset's 603 phrase entries are wrong for this corpus, and one segmentation accident pins a locative: 权限 -> 許可權 a Microsoft-glossary rendering; 權限 is the ordinary term 实例 -> 例項 not standard Taiwanese usage in any register X里 straddling STPhrases matches pin the locative 里 as 里 Keep the preset — its other 142 firing entries (資料, 程式, 物件, 介面, 伺服器, 軟體, 資訊, 快取, 使用者, 預設 …) are all correct, and plain `tw` would lose every one of them. Instead put identity entries at the head of the TWPhrases group, which disables exactly those two rules and nothing beneath them; a genuine 许可权 in a source still converts to 許可權 on its own terms. The locative is a different defect: STCharacters already reads a bare 里 as 裏/裡, and 541 of 553 come out right unaided. The twelve that do not are all a two-character STPhrases entry (本里, 里根, 里拉, 道里, 里加, 里长, 数里) matching across a word boundary. Those entries are correct in their own right, so the repair is a whitelist applied after conversion, behind a listed word only, guarded so 里程碑 is never touched. Both are pinned in the generator's fixture, which runs on every dev and build, so a preset upgrade cannot silently put 許可權 back in 435 places. A new unclaimed 里 is reported rather than guessed at. Regenerated: 許可權 435 -> 0, 權限 0 -> 435 (the exact count of 权限 in the Simplified sources), 例項 2 -> 0, 實例 0 -> 2, 裡 541 -> 553. The 8 links #187 pointed at /zh-Hant/ are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Fixes #142
OpenCC only rewrites Han characters, so an internal link authored in a Simplified post as
[标题](/zh-Hans/blog/some-slug/)survived verbatim intoindex.zh-Hant.mdxand sent a Traditional reader to the Simplified companion post. The generator now rewrites the locale segment on its way out.(Angle-bracket placeholders are spelled in caps throughout — GitHub's body sanitizer eats short bracketed fragments, including inside code spans, and ate a whole section of the first draft of this description.)
The rewrite rule
/zh-Hans/PATHbecomes/zh-Hant/PATHon link targets in the body, and only where the Traditional target is provably built. A wrong-locale link is a defect; one the generator turned into a 404 would be worse, so an unprovable target is left alone and reported on stdout rather than rewritten.Where the conditional is load-bearing. Two routes are emitted per-locale with no fallback, so only they can 404 after a rewrite:
/[lang]/blog/SLUG/src/pages/[lang]/blog/[...slug].astro)/[lang]/blog/topics/SLUG/src/lib/terms.ts, which this script does not importWhere no check is needed, so none is invented. Everything else under a routed locale builds for
zh-Hantexactly when it builds forzh-Hans:src/pages/[lang]/*, emitted for every entry ofLOCALESinsrc/lib/i18n.ts—zh-Hantis one of them;zh-Hantfromzh-Hansand fall back to English (src/glossary/registry.ts,src/content-pages/registry.ts), so the two locales resolve together or not at all. The one thing that can still be false — the term existing anywhere — is checked;/zh-Hans/SLUG/is ambiguous between a cluster and a marketing page without importingsrc/lib/clusters.ts, so an unmatched single segment is reported rather than rewritten.Where the rewrite does not apply
scripts/content-lint.mjsstrips before it resolves links, so the generator and the validator agree on what counts as a link.relatedTermslist is never touched./zh-Hansprefix is ever a candidate. The/en/blog/...links in this repo's translated posts point at English-only posts and are correct as written; they are untouched. A look-alike prefix such as/zh-Hansard/is not matched either.@generatedmarker) — still skipped entirely, unchanged behaviour.Scoped-rewrite evidence
Two demonstrations on real content. Each mutates a Simplified source, proves the mutation actually reached disk before reading any result, runs the generator, then restores and proves the restore by blob hash — restoring with
git checkout HEAD -- ABSOLUTE_PATH, never a bare checkout, and never trusting an exit code for it.1 — a body link is rewritten, the identical string in code is not. Injected into
content/blog/ai-wrote-your-app-dare-to-merge/index.zh-Hans.mdx:The generated Traditional output, all three cases side by side:
2 — the conditional actually withholds a rewrite. A link to a slug with no post directory, and a topic hub:
Both runs restored to byte-identical state —
zh-Hans now 25dc4c30… vs HEAD 25dc4c30…,zh-Hant now f493f4b6… vs HEAD f493f4b6…,git status --porcelain: ''.Pinned, not just demonstrated. This repo has no test runner, and the property worth keeping is a negative one — a link is rewritten, an identical string inside code is not — which a later edit to the patterns would break silently, in generated output nobody reads. So a 16-case fixture table runs on every
pnpm devandpnpm buildagainst a stub registry, printing nothing unless a case fails. It earned its place immediately: it caught a bug in its own stub during development.Before / after link counts
The issue measured 6 links in 3 files;
mainhas moved since (#130's reverse-linking work), so the live figure at458bd1bwas 8 links across 5 files. All of them had Traditional targets — every one was a wrong-locale link, not a dead one.ai-wrote-your-app-dare-to-mergegive-your-agent-rules-for-governable-appsautomation-cross-system-flowsenterprise-ontology-race-open-vs-closedobjectos-agent-permission-boundariesThe content diff is exactly the locale segment and nothing else — mechanically verified by mapping every removed line through
s|/zh-Hans/|/zh-Hant/|and diffing against the added lines: identical, zero remaining difference. The other 41 regenerated Traditional files are byte-identical, which is the second half of the proof that the generator changed rather than the generated text.Gates
All at head
04e3d56, run as one union through the shared verify lock, exit code captured before any pipe. Each row quotes the gate's own verdict line.pnpm content:lint✓ content lint passed (334 files, 44 glossary terms checked)pnpm checkResult (134 files):·0 errors·0 warnings·0 hintspnpm build✓ zh-Hant: generated 46, kept 0 hand-maintained, 5 with body links pointed at /zh-Hant/·[build] 866 page(s) built in 71.04spnpm seo:smokeSEO smoke test passed (865 HTML pages checked)os-verify-lock: VERDICT command-exit 0 · held the lock 90s (1m30s) · waited 0sgit status --porcelainis empty afterpnpm build— the committed Traditional files are exactly what the generator produces.#137's link validator is on
main, so a mistake here would have surfaced as acontent:lintfailure rather than silently; it stayed green.Browser verification
Served the built
dist/and drove Chromium over the three affected Traditional pages at 1440×900 and 390×844. All six page loads returned 200, and every in-article internal link — the anchors insidearticle .prosewith a root-relative href — was checked for both locale and reachability:14 click-target checks (7 links × 2 viewports), all
/zh-Hant/, all 200. Screenshots scrolled to the rewritten links confirm they render as ordinary links in the Traditional paragraph, with the inline code alongside them (GET /api/refunds) still rendering as code, and no layout change at either width.The language switcher's
/zh-Hans/...alternate link is untouched — it lives outsidearticle .prose, and pointing at the Simplified page is its entire job.Out of scope
Per the seat ruling, the hand-written locales were checked but not edited. The check came back clean: all 20 non-self-locale links in
ja/ko/de/es/frposts are/en/blog/...links to three posts that exist only in English (enterprise-ontology-platform-comparison,ontology-vs-semantic-layer-vs-knowledge-graph,forward-deployed-engineer-trap). Linking to the only locale that is built is correct authoring, not the defect this card anticipated — useful input for #179, which is where mechanical detection belongs.Filed while verifying: #184 — every zh-Hant page renders the Simplified-text cover and diagrams (46 covers, 21 body assets). Same defect class, different surface: nothing converts
.svgtext nodes.Generated by Claude Code