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/(繁體中文)。擁有者對它的不滿有 兩個,性質不同:

  1. 中文讀起來是機翻。 不是用詞錯誤,是結構性的——中文頁是英文頁的 逐項對譯,連 elsewhere別處colophon版本記 都一一對應。 擁有者的原話:「純粹翻譯看起來就很機翻,如果是我我不會想看。」其中 山頂是謊言。巨石是真的。 是最刺眼的一句:那是這一頁的座右銘,把它 翻成中文既失去原句的重量,也讀不出中文的語感。
  2. 只有兩個語言,而切換的方式無法擴充。 現在的切換鍵是「顯示另一個 語言」的單一連結(英文頁寫 中文)。這個設計在兩個語言時是最優解,在 十個語言時無法沿用——不可能在角落列九個連結。擁有者要一個自己寫的 選單,明確排除瀏覽器原生的 <select>

同時,第三幕的 colophon(版本記)擁有者判定「看起來很怪」,要求移除,改放 著作權聲明。

Solution

一、把「翻譯」的範圍縮到最小

頁面上大部分的字所有語言都維持英文,不翻譯:名字 Siao、座右銘 The summit is a lie. The boulder is real.、eyebrow elsewhere、出口名稱 (git.siao.ai 等)、email 位址、微標籤 contact / soon、著作權行。

理由是這些字不是在傳遞資訊,是這一頁的骨架與署名。座右銘是刻在東西上 的字,colophonelsewhere 這類排版元件名在多數語言裡根本沒有等價的 日常詞(版本記 就是硬造出來的)。維持英文既躲掉十種語言的機翻,也讓整頁 在任何語言下保有同一個形狀。

翻譯只留給真的在傳遞資訊的句子:兩句出口描述,以及頁面之外的 <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

語言的取得

  1. 作為母語不是英文的訪客,我想在頁面右上角看到一個看起來就是「語言名字」 的字串,這樣就算整頁我都讀不懂,我也知道那裡可以換語言。
  2. 作為開錯語言的訪客,我想在頁面一載入就看到那個切換入口、不需要捲動, 這樣我不必先讀完一頁我讀不懂的東西才找得到出路。
  3. 作為訪客,我想點一下就展開十個語言的清單,這樣我不需要猜測還有哪些語言。
  4. 作為訪客,我想清單裡的每個語言都以它自己的文字寫出(日本語 而非 Japanese),這樣我一定認得出哪一個是我的語言。
  5. 作為訪客,我想清單裡當前語言那一項看得出來是當前語言,這樣我知道自己 現在在哪。
  6. 作為訪客,我想選單裡每一項都是真的連結,這樣我可以中鍵開新分頁、可以 複製網址。
  7. 作為關閉 JavaScript 的訪客,我想選單仍然能展開、十個語言仍然都到得了, 這樣我不會因為關掉腳本而失去這一頁的一半內容。
  8. 作為鍵盤使用者,我想用 Tab 就能聚焦到切換器,且它是頁面上第一個可聚焦 的元素,這樣我不必走過整頁才碰得到它。
  9. 作為鍵盤使用者,我想用 Enter/Space 展開、方向鍵在項目間移動、Esc 收起, 這樣它的行為和我對選單的預期一致。
  10. 作為滑鼠使用者,我想點選單以外的任何地方就收起它,這樣我不必精準地 再點一次觸發器。
  11. 作為螢幕閱讀器使用者,我想這個切換器被讀成「可展開的語言選擇,內含 十個連結」,這樣我理解它是什麼而不是撞到一團沒有語意的文字。
  12. 作為螢幕閱讀器使用者,我想每個語言項目帶有正確的 langhreflang 標記,這樣朗讀引擎會用該語言的發音唸它,而不是用英文腔唸日文。
  13. 作為第一次進站的訪客,我想瀏覽器語言若在支援清單內就直接看到那個語言 的頁面,這樣我不必自己動手。
  14. 作為瀏覽器語言不在支援清單內的訪客,我想直接得到英文頁,這樣我不會被 丟到一個隨機的語言。
  15. 作為手動選過語言的訪客,我想那個選擇永遠優先於我的瀏覽器語言,這樣 我不必每次進站都重選一遍。
  16. 作為拿到別人分享的 /ja/ 網址的訪客,我想它就是打開日文頁,這樣一個 被明確指定的位址不會被系統二次猜測。
  17. 作為使用私密瀏覽(localStorage 不可用)的訪客,我想語言切換仍然 有效,這樣我最多是下次要再選一次,而不是完全不能用。

內容與語感

  1. 作為中文讀者,我想出口描述讀起來像中文寫的,而不是英文句子折過來的, 這樣我不會覺得自己在讀機器輸出。
  2. 作為任何語言的讀者,我想座右銘維持英文原文,這樣我讀到的是作者真正 寫下的那句話,而不是它的影子。
  3. 作為任何語言的讀者,我想出口描述短而具體,這樣它傳遞的是事實而不是 文案。
  4. 作為簡體中文讀者,我想看到簡體字頁面,這樣我不必辨讀繁體。
  5. 作為阿拉伯文讀者,我想整頁由右至左,切換器在左上角,這樣版面符合我的 閱讀方向。
  6. 作為阿拉伯文讀者,我想頁面上的英文(名字、座右銘、email)仍然以由左至 右呈現,這樣它們不會變成亂碼般的順序。
  7. 作為任何語言的讀者,我想頁面上的文字都用網站自己的字體渲染、不出現 豆腐方框,這樣我讀得到每一個字。
  8. 作為選單使用者,我想清單裡的日文、韓文、俄文、阿拉伯文名稱都顯示得 出來,就算我的作業系統沒裝那些字體,這樣我一定找得到我的語言。

版面

  1. 作為訪客,我想頁面回到兩幕、底部只有一行著作權,這樣我不會滑到一段 突兀的自述。
  2. 作為訪客,我想那行著作權小到不與任何東西爭奪注意力,這樣它只是收尾 而不是內容。
  3. 作為訪客,我想選單展開時能清楚讀到那十行字、不與底下的內容疊在一起, 這樣我不需要瞇著眼睛辨認。
  4. 作為訪客,我想選單看起來仍然是這一頁的一部分——沒有邊框、沒有陰影、 沒有卡片——這樣它不像從別的網站貼過來的元件。
  5. 作為手機訪客,我想選單在窄螢幕上一樣好按、十個項目都摸得到,這樣我 不需要放大。

機器可讀性

  1. 作為搜尋引擎,我想每個語言都有自己的 URL 與 canonical,這樣我不會把 十個語言判成重複內容。
  2. 作為搜尋引擎,我想每一頁都宣告全部十個 hreflang 互指與 x-default, 這樣我能把它們理解成同一頁的十個語言版本。
  3. 作為搜尋引擎,我想 sitemap 列出十個 URL 且各自帶語言標註,這樣我不必 靠爬行去發現它們。
  4. 作為社群平台的連結預覽,我想 og:locale 與各語言的標題、描述正確, 這樣貼在 LINE / Slack / Discord 的連結會用讀者的語言顯示。
  5. 作為 AI 檢索系統,我想每一頁都有一致的 JSON-LD Person,這樣「Siao」 這個名字有一個可援引的定義(延續 NOTES_HOMEPAGE.md ch.21 的消歧 問題)。
  6. 作為訪客,我想舊的 /zh/ 網址仍然可用、會轉到繁體頁,這樣既有的連結 與書籤不會壞掉。

網站擁有者

  1. 作為網站擁有者,我想改一句文案只需要編輯一份對照表,這樣我不必在十份 HTML 之間同步。
  2. 作為網站擁有者,我想新增第十一個語言只需要在對照表加一段、跑一次腳本, 這樣擴充的成本是線性而不是十倍。
  3. 作為網站擁有者,我想 CI 在我忘記重跑產生器時紅燈,這樣對照表與頁面 不會悄悄不同步。
  4. 作為網站擁有者,我想部署仍然不依賴任何建置鏈,這樣即使腳本壞掉, 已上線的站也不受影響。
  5. 作為網站擁有者,我想字檔仍然全部自架,這樣訪客的 IP 不會外流給任何 第三方。
  6. 作為網站擁有者,我想每一頁常態只載入它自己需要的字檔,這樣多九個語言 不會讓每一頁都變胖。
  7. 作為網站擁有者,我想缺字守衛涵蓋全部字檔,這樣我改文案忘記重產 subset 時 CI 會擋下來。
  8. 作為網站擁有者,我想視覺快照只有版面真的會分岔的那幾張,這樣測試不會 變成我習慣性用 --update-snapshots 繞過的東西。
  9. 作為網站擁有者,我想「無 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

翻譯範圍:哪些字不翻

所有語言都維持英文Siao(h1)、座右銘、eyebrow elsewhere、出口 名稱、email 位址、CSS 微標籤 contact / soon© 2026 Siao

依語言在地化:兩句出口描述、<title><meta name="description">og:title / og:description、JSON-LD 的 descriptionknowsAbout 不 在地化,理由見下方〈中繼資料〉。

styles.csshtml[lang^="zh"]聯絡 / 即將 的覆寫刪除。CJK 的排版微調(line-height: 1.9、eyebrow 字距)保留,並從 zh 擴及 jakozh-Hans

出口描述的寫作約束

擁有者只校對英文與繁體中文,其餘八種語言沒有母語者會讀過。訪談中一度把 這列為「已知並接受的風險」,擁有者明確否決:「不能接受讀起來奇怪。」

因此文案的形式本身要承擔這個保證,而不是靠事後校對:

繁體中文的兩句重寫,不沿用現有譯文(自架的 git 伺服器,以及為它寫的 前台 是英文語序)。擁有者原本抱怨的 一台會寄信的郵件伺服器 正是長句折 過來的產物,而它隨 colophon 一起消失了。

colophon 移除與著作權行

語言選單

骨架:<details> / <summary>

語言判斷與記憶

字體

九個自架字檔(規格初稿寫七個,實作時切換器拆成兩個、西里爾直接用 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(新) 十個自稱裡的諺文 僅選單展開時

產生器

中繼資料

404

阿拉伯文與 RTL

Testing Decisions

什麼是好的測試

沿用 SPEC.md 與前版的定義,不重新協商:只斷言外部可觀察的行為。不斷言 class 名稱、不斷言 CSS 屬性值、不斷言 JS 內部狀態。

接縫

三個,其中兩個已存在:

  1. 已渲染的頁面(Playwright) — 主要接縫,沿用。語言導向、選單行為、 <head> 內容、404 狀態碼、RTL 版面全部落在這裡。
  2. 字檔產物(tests/font-coverage.js 直接讀 woff2 的 cmap) — 沿用, 擴及全部七個字檔。理由不變:擁有者本機有 CJK 字體可 fallback, document.fonts 只回報宣告過什麼而非檔案裡有什麼,唯一誠實的答案是讀 檔案。
  3. 產生器的輸出(新增) — 「重跑產生器,輸出與 commit 內容零差異」。 這個接縫不可避免:i18n 對照表與十份 HTML 之間的漂移,在頁面接縫與 字檔接縫上都看不見。它斷言的是產物,不是實作細節。

不新增其他接縫。特別是:不為了可測性把語言判斷邏輯抽成可匯出模組——那是 為了測試而發明實作細節,然後測試那個發明物。

測試範圍

代表性抽樣,不是十個語言全測。 十個語言各跑一遍所有斷言、十張快照, 會讓任何一次字距微調造成全面紅燈,然後養成 --update-snapshots 的習慣, 那比沒有測試更糟(SPEC.md 已為同一理由拒絕過動畫中間影格的快照)。

  1. 語言判斷與記憶(Playwright context locale

    • zh-TW/zh-hant/zh-CN/zh-hans/ja/ja/
    • pt-BR(不支援)→ 留在 /
    • 直接進 /ja/ → 不被導走,不論瀏覽器語言
    • 手動選英文後,再以 zh-TW/ → 留在 /
    • /zh/ → 301 到 /zh-hant//en/ → 301 到 /
  2. 選單行為

    • 關閉時顯示當前語言自稱;展開後十個語言全在,且都是 <a>
    • 當前語言那一項不是連結
    • 切換器在載入時即位於視窗內、無需捲動(toBeInViewport)——這是它 位於頁首的整個理由,必須守住
    • 它是頁面上第一個可聚焦的元素
    • Esc 收起、點外面收起
    • 停用 JavaScript 時選單仍可展開、十個連結全在(Q9 承諾的守門)
  3. 中繼資料互指

    • 每頁 canonical 指向自己(抽樣三頁)
    • 十條 hreflang 互指完整、x-default/
    • <html lang>dir 正確
    • og:* / <title> / description 為該語言
    • JSON-LD 為合法 JSON,且不含身分細節(沿用 ch.20 的負面斷言)
    • sitemap-pages.xml 為合法 XML 且恰好十個 URL
  4. 缺字守衛(第二接縫,擴充)

    • 每個語言頁實際渲染出的文字,逐字元比對該頁載入的 .woff2 cmap
    • 選單的十個自稱,比對切換器字檔的 cmap
    • 缺任何字元即紅燈,訊息列出缺哪些字
  5. 產生器無漂移(第三接縫,新增)

    • CI 跑一次產生器,與工作區比對,任何差異即紅燈
  6. 視覺回歸——四張,不是十張

    • /(拉丁基準)
    • /zh-hant/(CJK 排版規則)
    • /ar/(RTL 鏡像)
    • 選單展開的狀態
    • 這三個語言是版面真的會分岔的三種情況;其餘七個在版面上與 / 沒有差別。既有的英文快照因頁首與底部變動需一併重產。
  7. 資產與狀態碼

    • 每個語言頁的字檔載入成功
    • 不存在的路徑回 404 而非 200
    • robots.txtSitemap: 指向取得得到的檔案
  8. 無障礙:axe 只跑 //ar/(LTR 與 RTL 各一)。

明確不測

Out of Scope

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 訊息裡: