SPEC_HOMEPAGE_I18N.md
Spec: siao.ai 首頁 — 十語言化、自製語言選單、colophon 移除
適用範圍:siao-ai/(siao.ai 首頁 repo)。
與 siao-ai/SPEC.md 的關係:那份是首頁的原始規格(兩幕結構、極簡原則、
無建置步驟、單一 Playwright 接縫)。本規格擴充它,並且是本專案第一次
明確推翻它的既有決定——推翻項目在下方逐條列出,未列出的每一條(無導覽列、
無 CTA、無深色模式、自動捲動可被打斷、動畫品質不自動測、三年不維護仍能跑)
繼續成立。
本文件取代同名的前一版(雙語化 + colophon)。前一版的決定有一半仍然 有效、一半被推翻,兩者混在一起讀會誤導,因此整份改寫而非增補。前一版內容 見 git 歷史。
決策來源:一場 /grill-me 訪談(見文末 Further Notes),擁有者逐題確認。
Problem Statement
首頁現在有兩個語言:/(英文)與 /zh/(繁體中文)。擁有者對它的不滿有
兩個,性質不同:
- 中文讀起來是機翻。 不是用詞錯誤,是結構性的——中文頁是英文頁的
逐項對譯,連
elsewhere/別處、colophon/版本記都一一對應。 擁有者的原話:「純粹翻譯看起來就很機翻,如果是我我不會想看。」其中山頂是謊言。巨石是真的。是最刺眼的一句:那是這一頁的座右銘,把它 翻成中文既失去原句的重量,也讀不出中文的語感。 - 只有兩個語言,而切換的方式無法擴充。 現在的切換鍵是「顯示另一個
語言」的單一連結(英文頁寫
中文)。這個設計在兩個語言時是最優解,在 十個語言時無法沿用——不可能在角落列九個連結。擁有者要一個自己寫的 選單,明確排除瀏覽器原生的<select>。
同時,第三幕的 colophon(版本記)擁有者判定「看起來很怪」,要求移除,改放 著作權聲明。
Solution
一、把「翻譯」的範圍縮到最小
頁面上大部分的字所有語言都維持英文,不翻譯:名字 Siao、座右銘
The summit is a lie. The boulder is real.、eyebrow elsewhere、出口名稱
(git.siao.ai 等)、email 位址、微標籤 contact / soon、著作權行。
理由是這些字不是在傳遞資訊,是這一頁的骨架與署名。座右銘是刻在東西上
的字,colophon、elsewhere 這類排版元件名在多數語言裡根本沒有等價的
日常詞(版本記 就是硬造出來的)。維持英文既躲掉十種語言的機翻,也讓整頁
在任何語言下保有同一個形狀。
翻譯只留給真的在傳遞資訊的句子:兩句出口描述,以及頁面之外的
<title> / meta description / og。
二、colophon 移除,頁面回到兩幕
第三幕整個刪除。出口幕底部長出一行小字 © 2026 Siao。這是還原而不是新設計
——SPEC.md 的原始結構就是兩幕,第三幕是為了 colophon 才長出來的。
這一刀的連帶效果是本規格能夠成立的關鍵:colophon 一走,全站需要在地化的 散文就只剩兩句話。 十個語言的機翻風險,從「兩段自述 × 10」縮成「兩句短句 × 10」——那是可以在沒有母語校對的情況下守得住的量。
三、十個語言,一份對照表,一支產生器
語言從 2 個變 10 個。維護的對象從「兩份手寫 HTML」變成一份 i18n 對照
表;十份靜態 HTML 由一支離線腳本產生,產物 commit 進 repo,部署路徑一行
不變。
四、右上角變成自製選單
<details> / <summary> 撐起的展開清單,關閉時顯示當前語言的自稱,
展開時列出十個語言的自稱,每一項都是指向真實 URL 的 <a>。無 JS 也能開合。
User Stories
語言的取得
- 作為母語不是英文的訪客,我想在頁面右上角看到一個看起來就是「語言名字」 的字串,這樣就算整頁我都讀不懂,我也知道那裡可以換語言。
- 作為開錯語言的訪客,我想在頁面一載入就看到那個切換入口、不需要捲動, 這樣我不必先讀完一頁我讀不懂的東西才找得到出路。
- 作為訪客,我想點一下就展開十個語言的清單,這樣我不需要猜測還有哪些語言。
- 作為訪客,我想清單裡的每個語言都以它自己的文字寫出(
日本語而非Japanese),這樣我一定認得出哪一個是我的語言。 - 作為訪客,我想清單裡當前語言那一項看得出來是當前語言,這樣我知道自己 現在在哪。
- 作為訪客,我想選單裡每一項都是真的連結,這樣我可以中鍵開新分頁、可以 複製網址。
- 作為關閉 JavaScript 的訪客,我想選單仍然能展開、十個語言仍然都到得了, 這樣我不會因為關掉腳本而失去這一頁的一半內容。
- 作為鍵盤使用者,我想用 Tab 就能聚焦到切換器,且它是頁面上第一個可聚焦 的元素,這樣我不必走過整頁才碰得到它。
- 作為鍵盤使用者,我想用 Enter/Space 展開、方向鍵在項目間移動、Esc 收起, 這樣它的行為和我對選單的預期一致。
- 作為滑鼠使用者,我想點選單以外的任何地方就收起它,這樣我不必精準地 再點一次觸發器。
- 作為螢幕閱讀器使用者,我想這個切換器被讀成「可展開的語言選擇,內含 十個連結」,這樣我理解它是什麼而不是撞到一團沒有語意的文字。
- 作為螢幕閱讀器使用者,我想每個語言項目帶有正確的
lang與hreflang標記,這樣朗讀引擎會用該語言的發音唸它,而不是用英文腔唸日文。 - 作為第一次進站的訪客,我想瀏覽器語言若在支援清單內就直接看到那個語言 的頁面,這樣我不必自己動手。
- 作為瀏覽器語言不在支援清單內的訪客,我想直接得到英文頁,這樣我不會被 丟到一個隨機的語言。
- 作為手動選過語言的訪客,我想那個選擇永遠優先於我的瀏覽器語言,這樣 我不必每次進站都重選一遍。
- 作為拿到別人分享的
/ja/網址的訪客,我想它就是打開日文頁,這樣一個 被明確指定的位址不會被系統二次猜測。 - 作為使用私密瀏覽(
localStorage不可用)的訪客,我想語言切換仍然 有效,這樣我最多是下次要再選一次,而不是完全不能用。
內容與語感
- 作為中文讀者,我想出口描述讀起來像中文寫的,而不是英文句子折過來的, 這樣我不會覺得自己在讀機器輸出。
- 作為任何語言的讀者,我想座右銘維持英文原文,這樣我讀到的是作者真正 寫下的那句話,而不是它的影子。
- 作為任何語言的讀者,我想出口描述短而具體,這樣它傳遞的是事實而不是 文案。
- 作為簡體中文讀者,我想看到簡體字頁面,這樣我不必辨讀繁體。
- 作為阿拉伯文讀者,我想整頁由右至左,切換器在左上角,這樣版面符合我的 閱讀方向。
- 作為阿拉伯文讀者,我想頁面上的英文(名字、座右銘、email)仍然以由左至 右呈現,這樣它們不會變成亂碼般的順序。
- 作為任何語言的讀者,我想頁面上的文字都用網站自己的字體渲染、不出現 豆腐方框,這樣我讀得到每一個字。
- 作為選單使用者,我想清單裡的日文、韓文、俄文、阿拉伯文名稱都顯示得 出來,就算我的作業系統沒裝那些字體,這樣我一定找得到我的語言。
版面
- 作為訪客,我想頁面回到兩幕、底部只有一行著作權,這樣我不會滑到一段 突兀的自述。
- 作為訪客,我想那行著作權小到不與任何東西爭奪注意力,這樣它只是收尾 而不是內容。
- 作為訪客,我想選單展開時能清楚讀到那十行字、不與底下的內容疊在一起, 這樣我不需要瞇著眼睛辨認。
- 作為訪客,我想選單看起來仍然是這一頁的一部分——沒有邊框、沒有陰影、 沒有卡片——這樣它不像從別的網站貼過來的元件。
- 作為手機訪客,我想選單在窄螢幕上一樣好按、十個項目都摸得到,這樣我 不需要放大。
機器可讀性
- 作為搜尋引擎,我想每個語言都有自己的 URL 與 canonical,這樣我不會把 十個語言判成重複內容。
- 作為搜尋引擎,我想每一頁都宣告全部十個
hreflang互指與x-default, 這樣我能把它們理解成同一頁的十個語言版本。 - 作為搜尋引擎,我想 sitemap 列出十個 URL 且各自帶語言標註,這樣我不必 靠爬行去發現它們。
- 作為社群平台的連結預覽,我想
og:locale與各語言的標題、描述正確, 這樣貼在 LINE / Slack / Discord 的連結會用讀者的語言顯示。 - 作為 AI 檢索系統,我想每一頁都有一致的 JSON-LD
Person,這樣「Siao」 這個名字有一個可援引的定義(延續NOTES_HOMEPAGE.mdch.21 的消歧 問題)。 - 作為訪客,我想舊的
/zh/網址仍然可用、會轉到繁體頁,這樣既有的連結 與書籤不會壞掉。
網站擁有者
- 作為網站擁有者,我想改一句文案只需要編輯一份對照表,這樣我不必在十份 HTML 之間同步。
- 作為網站擁有者,我想新增第十一個語言只需要在對照表加一段、跑一次腳本, 這樣擴充的成本是線性而不是十倍。
- 作為網站擁有者,我想 CI 在我忘記重跑產生器時紅燈,這樣對照表與頁面 不會悄悄不同步。
- 作為網站擁有者,我想部署仍然不依賴任何建置鏈,這樣即使腳本壞掉, 已上線的站也不受影響。
- 作為網站擁有者,我想字檔仍然全部自架,這樣訪客的 IP 不會外流給任何 第三方。
- 作為網站擁有者,我想每一頁常態只載入它自己需要的字檔,這樣多九個語言 不會讓每一頁都變胖。
- 作為網站擁有者,我想缺字守衛涵蓋全部字檔,這樣我改文案忘記重產 subset 時 CI 會擋下來。
- 作為網站擁有者,我想視覺快照只有版面真的會分岔的那幾張,這樣測試不會
變成我習慣性用
--update-snapshots繞過的東西。 - 作為網站擁有者,我想「無 JS 仍可換語言」這句承諾有測試守著,這樣它不會 在某次重構中無聲失效。
Implementation Decisions
被本規格推翻的既有決定
明列於此,以免日後被當成漂移:
| 既有決定 | 出處 | 新決定 |
|---|---|---|
| 兩個語言,兩份手寫 HTML,不引入產生器 | 前版 I18N 規格 | 十個語言,一份對照表 + 離線產生器 |
| 切換鍵只顯示另一個語言、單一連結、不做選單 | 前版 I18N 規格 | <details> 自製選單,列出十個自稱 |
| 第三幕 colophon | 前版 I18N 規格 | 刪除,改為出口幕底部一行著作權 |
/zh/ 即中文頁 |
前版 I18N 規格 | /zh-hant/ 與 /zh-hans/;/zh/ 301 |
中文頁的 聯絡 / 即將 標籤 |
前版 I18N 規格 | 所有語言統一顯示 contact / soon |
SPEC.md 的「無建置步驟」沒有被推翻:見下方〈產生器〉。
語言清單
十個,全部平等(英文不是「原版」,只是預設):
| 語言 | 路徑 | <html lang> |
選單顯示 | 方向 |
|---|---|---|---|---|
| 英文 | / |
en |
English |
ltr |
| 繁體中文 | /zh-hant/ |
zh-Hant |
繁體中文 |
ltr |
| 簡體中文 | /zh-hans/ |
zh-Hans |
简体中文 |
ltr |
| 日文 | /ja/ |
ja |
日本語 |
ltr |
| 韓文 | /ko/ |
ko |
한국어 |
ltr |
| 西班牙文 | /es/ |
es |
Español |
ltr |
| 法文 | /fr/ |
fr |
Français |
ltr |
| 德文 | /de/ |
de |
Deutsch |
ltr |
| 俄文 | /ru/ |
ru |
Русский |
ltr |
| 阿拉伯文 | /ar/ |
ar |
العربية |
rtl |
- 英文維持在
/,不另開/en/;/en/以 301 導向/。canonical與x-default已在該處,且根路徑本來就必須吐東西。 - 中文以文字系統而非地區命名。
/cn/、/tw/這類地區碼被明確拒絕:那是 國家不是語言,會把新加坡與馬來西亞的簡體讀者排除在語意之外,而hreflang要的本來就是語言碼。 - 路徑一律小寫;
hreflang屬性用正規大小寫(zh-Hant)。 /zh/以 Cloudflare Pages 的_redirects做 301 →/zh-hant/。保留舊 網址而不是讓它 404,因為它已進索引且可能被分享過。
翻譯範圍:哪些字不翻
所有語言都維持英文:Siao(h1)、座右銘、eyebrow elsewhere、出口
名稱、email 位址、CSS 微標籤 contact / soon、© 2026 Siao。
依語言在地化:兩句出口描述、<title>、<meta name="description">、
og:title / og:description、JSON-LD 的 description。knowsAbout 不
在地化,理由見下方〈中繼資料〉。
styles.css 中 html[lang^="zh"] 對 聯絡 / 即將 的覆寫刪除。CJK
的排版微調(line-height: 1.9、eyebrow 字距)保留,並從 zh 擴及 ja、
ko、zh-Hans。
出口描述的寫作約束
擁有者只校對英文與繁體中文,其餘八種語言沒有母語者會讀過。訪談中一度把 這列為「已知並接受的風險」,擁有者明確否決:「不能接受讀起來奇怪。」
因此文案的形式本身要承擔這個保證,而不是靠事後校對:
- 無法校對的八個語言,描述寫成名詞短語,不寫成句子。 句子有語序、
時態、語氣、連接方式可以出錯,翻譯腔全部藏在那裡;名詞短語沒有這些
表面。
セルフホストの Git サーバーと自作のフロントエンド這種形式, 要嘛對、要嘛用詞錯(而用詞錯是查得出來的),不會「文法對但讀起來很怪」。 - 不用比喻、不用破折號、不用文學語氣、不做修飾。
- 每則以該語言的自然計數方式計不超過約十二個詞/字。
- 英文與繁體中文不受此限——擁有者讀得懂,可以寫成句子並親自驗收。
- 回譯檢查:每個語言的文案回譯成中文比對原意,並檢查該語言常見的 翻譯腔標記(日文的冗長助詞鏈、俄文的英式語序、阿拉伯文的直譯冠詞等)。
- 單一語言退場機制:若某個語言連名詞短語都沒有把握,該語言的描述 退回英文,其餘語言照常。退回的是一個語言,不是整組將就。頁面上其他 部分本來就是英文,退回一則描述不會顯得突兀。
繁體中文的兩句重寫,不沿用現有譯文(自架的 git 伺服器,以及為它寫的 前台 是英文語序)。擁有者原本抱怨的 一台會寄信的郵件伺服器 正是長句折
過來的產物,而它隨 colophon 一起消失了。
colophon 移除與著作權行
- 第三幕整個刪除:HTML 區塊、
styles.css的.colophon*規則、tests/colophon.spec.js、兩張colophon-*快照。 - 出口幕底部新增一行
© 2026 Siao,字級與 eyebrow 同等或更小,同一個 低對比灰。 - 年份寫死,不自動更新。以 JS 產生會讓它變成「JS 沒跑就消失的字」; 以產生器產生會讓它變成一項年度維護工作。著作權年份標示創作年,寫死在 法律上正常。
- 不加
All rights reserved(法律上無意義的贅字,也是典型的機翻感來源)。 - 不在這一行掛原始碼 repo 連結——那會讓著作權行變成第三個出口,與第二幕 搶功能。
語言選單
骨架:<details> / <summary>。
- 關閉時
<summary>顯示當前語言的自稱(/顯示English,/ja/顯示日本語)。這是全世界網站的既定慣例,語言跑錯的人看得出那是一個 語言名字;而且零額外字元成本——每頁只需要它本來就載入的那套字。- 被否決的替代方案:地球圖示(
SPEC.md禁圖示);文A符號(文是 CJK 字元,會逼俄文頁與阿拉伯文頁為了一個字多載一個 CJK 字檔); 翻譯過的「語言」一詞(對唯一需要它的人——讀不懂當前語言的人——無效)。
- 被否決的替代方案:地球圖示(
- 展開時為十行右對齊小字,與頁面同底色作為遮蔽,無邊框、無陰影、 無圓角、無卡片。視覺上像那十行字直接長在頁面上。
- 當前語言那一行不是連結,以
--ink(深)呈現;其餘為--ink-dim(灰), hover / focus 轉深——沿用.lang-switch a現有規則,不發明新的視覺語言。 - 每一項是真的
<a href="/ja/">,帶lang與hreflang屬性。中鍵開新 分頁、複製網址、螢幕閱讀器的正確發音、爬蟲看到十語互連,全部因此免費 取得。 <summary>的預設三角形標記以 CSS 移除。- JS 只做兩件加分的事:Esc 收起、點選單外收起。兩者都不影響可用性——
JS 全滅時
<details>照樣開合,十個語言全部可達。這是SPEC.md「三年 後不維護仍然能跑」在本次的具體兌現。 - 位置維持
position: absolute的頁首角落,不改成fixed:它只存在 於第一幕,不跟著捲動變成常駐家具(「這一頁沒有導覽列,而這不是導覽列的 開端」)。 text-transform: uppercase移除(Español會變ESPAÑOL,而自稱是 專有名詞),letter-spacing一併調小。- RTL 由
inset-inline-end等邏輯屬性處理,不寫第二套 CSS。
語言判斷與記憶
- 只有
/帶那支 first-paint 之前的內嵌腳本;其餘九個路徑不帶——一個 被明確輸入或分享的位址永不被二次猜測。 - 判斷順序:
localStorage的手動選擇 → 永遠優先,直接使用- 逐一比對
navigator.languages,先試完整標籤 (zh-TW/zh-HK/zh-MO→zh-Hant;zh-CN/zh-SG/zh-Hans→zh-Hans),再試主要語言碼(ja/ko/es/fr/de/ru/ar) - 都對不上 → 留在
/
- 導向用
location.replace(),不進入訪客的歷史紀錄。 - 手動選擇的寫入時機:只有點選單裡的連結才寫入
localStorage。直接 造訪/ja/不覆寫既有選擇。 localStorage讀寫全部包try/catch:私密模式下最壞情況是下次再問一次, 不是壞掉的頁面。- 被否決的替代方案:Cloudflare Pages Functions 讀
Accept-Language做 伺服器端 302。它能消除一次location.replace的閃動,代價是引入一個 執行期元件與 CDN 快取分歧,與「純靜態、三年不維護」相衝突。 - 被否決的替代方案:完全不猜。那兩句出口描述是這一頁上唯一在傳遞資訊的
東西,讓讀者靠自己找到角落才讀得到它,等於把唯一的內容藏在一次點擊後面;
且
<title>、meta、分享預覽的語言是在頁面之外發生的,不猜就永遠是英文。
字體
九個自架字檔(規格初稿寫七個,實作時切換器拆成兩個、西里爾直接用 Google 的建置),全部沿用既有的自架理由(第三方 CDN 等於把訪客 IP 送人)。
| 字檔 | 涵蓋 | 載入時機 |
|---|---|---|
| EB Garamond latin(既有,44KB / 236 字) | en / es / fr / de,以及每一頁的英文骨架 | 全部頁面,preload |
| EB Garamond cyrillic(新) | ru | 僅 /ru/ |
| Noto Serif TC subset(既有) | zh-Hant | 僅 /zh-hant/ |
| Noto Serif SC subset(新) | zh-Hans | 僅 /zh-hans/ |
| Noto Serif JP subset(新) | ja | 僅 /ja/ |
| Noto Serif KR subset(新) | ko | 僅 /ko/ |
| Noto Naskh Arabic subset(新) | ar | 僅 /ar/ |
| 切換器 Han subset(新) | 十個自稱裡的漢字 | 僅選單展開時 |
| 切換器 Hangul subset(新) | 十個自稱裡的諺文 | 僅選單展開時 |
- 現有 Garamond latin subset 經實測(用專案自己的
tests/font-coverage.js讀 cmap)已完整涵蓋西歐重音字,es/fr/de不需要新字檔。它沒有 西里爾與阿拉伯,故ru/ar各需一個。 - 切換器 subset 是本次唯一非顯而易見的字體決定:選單在每一頁都列出十個
自稱,等於俄文頁需要 CJK 字、日文頁需要阿拉伯字。解法是利用
<details>關閉時內容為display: none、瀏覽器不會為未渲染文字下載 字體——那個字檔只在使用者第一次展開選單時才抓。每頁常態載入仍然只有 「拉丁 + 自己那套」。 - 被否決的替代方案:選單標籤交給系統字體。失敗模式是豆腐方框——一台沒裝
日韓字體的 Windows 會在選單裡顯示
□□□,而那一行正是唯一能救出「開錯 語言的日本訪客」的東西。那不是美觀問題,是功能失敗。 subset-font.py從單一字檔擴充為處理一組字檔,字元集仍然從原始碼推導 而非手工維護(現行腳本的核心理由不變:手寫清單是網站文字的第二份副本, 兩者必然漂移)。
產生器
- 唯一手動維護的內容檔是一份
i18n對照表:語言碼 → 路徑、<html lang>、 方向、自稱、<title>、description、兩句出口描述、JSON-LD 欄位。 - 一支離線腳本讀它,套上共用的 HTML 模板,輸出十份
index.html加sitemap-pages.xml加_redirects。 - 產物 commit 進 repo。 部署仍然是「Cloudflare Pages 直接吃靜態檔」,
一行都不改;腳本壞掉不影響已上線的站。這與
scripts/subset-font.py是同一個模式,先例已存在——所以SPEC.md的「無建置步驟」在它真正保護 的意義上(部署不依賴建置鏈、三年後仍能跑)沒有被推翻。手動維護的對象 反而從兩份 HTML 減成一份表。 - 被否決的替代方案:純前端 i18n(一份 JSON +
main.js在瀏覽器塞字, 單一 URL)。它會讓hreflang、canonical、og:locale、sitemap 的十個 語言條目全部失效,因為爬蟲只看得到一個 URL——而NOTES_HOMEPAGE.mdch.20 / ch.21 整輪工作處理的正是「Google 把 Siao 混成三個帳號、Gemini 編造履歷」,其解法就是讓機器有各語言各自定址、可援引的頁面。 - 被否決的替代方案:十份手寫 HTML。十份
<head>必然漂移。
中繼資料
- 每頁
canonical指向自己。 - 每頁宣告全部十條
hreflangalternate 加x-default→/。 og:locale為該頁語言,og:locale:alternate列出其餘九個。- JSON-LD
Person每頁一份,description為該語言;knowsAbout十個 語言一律英文——它是給機器讀的實體名稱,穩定的英文術語對取用這份標記的 系統比十份翻譯有用(中文頁原本有翻譯,本次收回)。url一律指向https://siao.ai/。延續 ch.20:不含年齡、不含本名,sameAs維持省略而非空陣列。 sitemap-pages.xml由產生器輸出十個<url>,各自帶十條xhtml:link rel="alternate" hreflang。sitemap.xml(索引檔)不動。
404
404.html維持單一英文、noindex,不生成十份。- 不掛語言選單:一個不進索引的死路頁面配十語切換器很荒謬。維持只有
一條回
siao.ai的連結。
阿拉伯文與 RTL
/ar/的<html>帶dir="rtl"。- 版面完全鏡像:切換器移到左上角、出口清單靠右對齊。實作上以 CSS
邏輯屬性(
inset-inline-end、padding-inline等)取得,不新增[dir="rtl"]選擇器分支——這一頁幾乎全置中,真正有左右可言的只有那個 角落。 - 頁面上永遠英文的東西(
Siao、座右銘、email、著作權行)在 RTL 頁裡仍然 是 LTR 拉丁文,交給瀏覽器的雙向演算法,不強制方向。 - 被否決:
/ar/用dir="ltr"。阿拉伯文以 LTR 排會讓標點落在錯誤的一端, 讀起來是壞掉的。
Testing Decisions
什麼是好的測試
沿用 SPEC.md 與前版的定義,不重新協商:只斷言外部可觀察的行為。不斷言
class 名稱、不斷言 CSS 屬性值、不斷言 JS 內部狀態。
接縫
三個,其中兩個已存在:
- 已渲染的頁面(Playwright) — 主要接縫,沿用。語言導向、選單行為、
<head>內容、404 狀態碼、RTL 版面全部落在這裡。 - 字檔產物(
tests/font-coverage.js直接讀 woff2 的 cmap) — 沿用, 擴及全部七個字檔。理由不變:擁有者本機有 CJK 字體可 fallback,document.fonts只回報宣告過什麼而非檔案裡有什麼,唯一誠實的答案是讀 檔案。 - 產生器的輸出(新增) — 「重跑產生器,輸出與 commit 內容零差異」。
這個接縫不可避免:
i18n對照表與十份 HTML 之間的漂移,在頁面接縫與 字檔接縫上都看不見。它斷言的是產物,不是實作細節。
不新增其他接縫。特別是:不為了可測性把語言判斷邏輯抽成可匯出模組——那是 為了測試而發明實作細節,然後測試那個發明物。
測試範圍
代表性抽樣,不是十個語言全測。 十個語言各跑一遍所有斷言、十張快照,
會讓任何一次字距微調造成全面紅燈,然後養成 --update-snapshots 的習慣,
那比沒有測試更糟(SPEC.md 已為同一理由拒絕過動畫中間影格的快照)。
語言判斷與記憶(Playwright context
locale)zh-TW→/zh-hant/;zh-CN→/zh-hans/;ja→/ja/pt-BR(不支援)→ 留在/- 直接進
/ja/→ 不被導走,不論瀏覽器語言 - 手動選英文後,再以
zh-TW進/→ 留在/ /zh/→ 301 到/zh-hant/;/en/→ 301 到/
選單行為
- 關閉時顯示當前語言自稱;展開後十個語言全在,且都是
<a> - 當前語言那一項不是連結
- 切換器在載入時即位於視窗內、無需捲動(
toBeInViewport)——這是它 位於頁首的整個理由,必須守住 - 它是頁面上第一個可聚焦的元素
- Esc 收起、點外面收起
- 停用 JavaScript 時選單仍可展開、十個連結全在(Q9 承諾的守門)
- 關閉時顯示當前語言自稱;展開後十個語言全在,且都是
中繼資料互指
- 每頁 canonical 指向自己(抽樣三頁)
- 十條 hreflang 互指完整、
x-default→/ <html lang>與dir正確og:*/<title>/ description 為該語言- JSON-LD 為合法 JSON,且不含身分細節(沿用 ch.20 的負面斷言)
sitemap-pages.xml為合法 XML 且恰好十個 URL
缺字守衛(第二接縫,擴充)
- 每個語言頁實際渲染出的文字,逐字元比對該頁載入的
.woff2cmap - 選單的十個自稱,比對切換器字檔的 cmap
- 缺任何字元即紅燈,訊息列出缺哪些字
- 每個語言頁實際渲染出的文字,逐字元比對該頁載入的
產生器無漂移(第三接縫,新增)
- CI 跑一次產生器,與工作區比對,任何差異即紅燈
視覺回歸——四張,不是十張
/(拉丁基準)/zh-hant/(CJK 排版規則)/ar/(RTL 鏡像)- 選單展開的狀態
- 這三個語言是版面真的會分岔的三種情況;其餘七個在版面上與
/沒有差別。既有的英文快照因頁首與底部變動需一併重產。
資產與狀態碼
- 每個語言頁的字檔載入成功
- 不存在的路徑回 404 而非 200
robots.txt的Sitemap:指向取得得到的檔案
無障礙:axe 只跑
/與/ar/(LTR 與 RTL 各一)。
明確不測
- 八種未校對語言的文案品質。 沒有自動化方式判斷,也沒有人能判斷—— 這是本規格已接受的風險,不是測試能補的洞。
- 選單的美感、CJK 與阿拉伯排版的美感。 快照只保護破版,不保護品味。
- 字體 subset 的產出流程本身。 測產物夠不夠,不測產它的指令。
- Lighthouse CI、單元測試——
SPEC.md的排除原封不動繼續成立。
Out of Scope
- 十個語言以外的任何語言(葡萄牙文、印地文、泰文、越南文、希伯來文等)。 結構支援線性擴充,但本次不做。
- 深色模式、動畫回歸、scroll-jacking——
SPEC.md的排除項全部維持。 - colophon 內容以任何形式回到頁面上(包括縮短版、包括藏在選單裡)。
- 出口清單本身增減項目。
accounts.siao.ai仍然不列為出口(它不是一個 地方,直接進去只有一個表單),且它原本被寫在 colophon 裡——colophon 移除後,它在頁面上不再被提及,這是本次接受的內容損失。 - 伺服器端語言協商(Cloudflare Pages Functions)。
- 母語者校對流程。
- og:image 與社群預覽圖。
- 前端框架、部署期建置鏈。
- 語言選單出現在 404 頁。
- 著作權年份自動更新。
Further Notes
本規格推翻的是「上一場 /grill-me 的產物」。 前版 I18N 規格對「單一
連結、不做選單」的論證在兩個語言的前提下是正確的;被推翻不是因為它錯,
是因為前提從 2 變成 10。這一點值得記住:那份文件裡對「切換鍵放頁首而非
頁尾」的論證(「用戶不可能滑到尾部再切換語言」)在新前提下仍然成立,
因此原封不動保留。
最重要的一次成本反轉。 訪談初期估計「十個語言 = 十份文案 × 母語校對 不可能」,看起來不可行。真正讓它可行的不是任何技術決定,是 Q2 與 Q3 兩次 縮減翻譯範圍:把座右銘與排版元件名釘死在英文、把 colophon 整個砍掉之 後,每個語言真正要顯示的該語言文字只剩兩句短句。技術部分(產生器、選單、 字檔)從頭到尾都是機械工作;風險全部集中在文案,而解法是讓需要文案的地方 變少。
選單帶回了一個剛被否決過的成本。 Q7 以「文 是 CJK 字元,會逼八個頁面
多載一個字檔」否決了 文A 符號;Q8 選定的清單式選單,在每一頁列出十個
自稱,等於把那個成本乘以三十倍請了回來。這是訪談中唯一一次一個決定悄悄
撤銷了另一個決定的理由。它被 <details> 關閉時不下載字體的性質救回來——
但那是運氣,不是設計。若日後把選單改成常駐展開,這個成本會立刻現形。
擁有者可校對的語言只有兩種,而他拒絕接受由此而來的風險。這一點值得 單獨記住,因為它改變的是解法的性質:既然無法用校對保證品質,就用形式 保證——把無法校對的語言的文案降到名詞短語,讓「讀起來很怪」在文法上沒有 發生的餘地,並保留單一語言退回英文的出口。這不是把風險寫得好看一點,是 換一種不依賴人力的作法。代價是那八個語言的描述會比中英兩版更乾、更像標籤 ——這是刻意的取捨。若日後有母語者回報,修正成本是編輯對照表的一行。
/zh/ 的 301 是一次性的債。 它會永遠留在 _redirects 裡。可接受的
理由是這個站上線僅數月、索引足跡極小,且那一行的維護成本是零。
未設定 issue tracker:本規格以檔案形式產出,工單同樣落在
siao-ai/.scratch/siao-ai-homepage/issues/,沿用既有編號序列。
Implementation Notes(實作後補記)
規格與實作的落差,記在這裡而不是散在 commit 訊息裡:
- 出口清單改為產生器渲染的靜態 HTML。 原本由
main.js在客戶端塞入。 全站唯一的對外連結不該取決於一支腳本有沒有跑;產生器既然存在,就沒有理由 再讓它跑在瀏覽器裡。main.js因此只剩自動捲動、記住語言、選單的兩個關閉 手勢。 - 中繼資料測試跑全部十個語言,不抽樣。 抽樣的理由是快照的維護成本,讀
<head>沒有那個成本。產生器最可能壞掉的地方是它的某一個分支。 scripts/serve.js實作了_redirects。 只實作這個站用到的語法。它 存在的理由本來就是「在 Cloudflare Pages 錯的地方一起錯」,否則 301 這條 規則在本機與 CI 都無從驗證。- 選單清單覆蓋在觸發器上。 往下展開會讓當前語言相鄰出現兩次。這是截圖 驗收才看得出來的問題,測試抓不到。
- 非拉丁文字關閉斜體。 描述設定為 italic,而 CJK 與阿拉伯文沒有斜體, 瀏覽器會用剪切變形偽造一個(synthetic oblique)。歪掉的假名與納斯赫體 不是強調,是錯誤。
- RTL 的著作權行加
dir="ltr"。 規格寫「交給瀏覽器的雙向演算法處理」, 實測不夠:©是中性字元,開頭中性字元在 RTL 段落裡會被丟到右端,那一行 變成Siao 2026 ©。 ↗是具名例外。 連結的外連箭頭從加進來那天起就不在任何自架字檔裡, 一直由系統字渲染。為一個裝飾符號讓每頁多抓一個檔不划算,因此在測試裡 明確列為例外,而不是讓它繼續是一個沒人知道的洞。- 視覺快照的容差未動。
maxDiffPixelRatio: 0.01在 1280×800 上是 10,240 px,實測足以讓一整行新文字無聲通過。選單的結構改由language.spec.js逐項斷言補上,容差本身留給擁有者決定。