diff --git a/docs/explorations/2026-08-23-seven-languages-audit.md b/docs/explorations/2026-08-23-seven-languages-audit.md
new file mode 100644
index 00000000..73c20b77
--- /dev/null
+++ b/docs/explorations/2026-08-23-seven-languages-audit.md
@@ -0,0 +1,69 @@
+# 七语言上线后的整体复查(2026-08-23)
+
+站点从 2 种语言变成 7 种、落地页全部生成化之后,做了一次横向复查。下面是查出来的东西,
+按"用户会不会撞到"排序。
+
+## 1. 韩语编辑器在空白文档上弹错误框(已修,PR #194)
+
+单独记在 CLAUDE.md 的 vendor 补丁一节。要点:vendor 的 44 个非英文 locale **没有一个**
+对得齐 `en.json`,而有些字符串只存在于 locale 文件里,缺翻译就是 `undefined`,
+`updateHint` 直接 `hint[0]` → TypeError → 被 app 当成文档错误。两道防线:
+`bin/locale-fill.mjs`(补齐站点 7 种语言)+ 守卫 11(`updateHint(undefined)` 空操作)。
+
+## 2. 语言菜单把语言名切两半(已修)
+
+触发器宽 104px 是当年按 "Deutsch" 定的。组件在 shadow root 里 `overflow: hidden`,
+**并且面板宽度按触发器宽度算**,所以 "Português" 同时在两处被切:列表里 `Portug…`,
+触发器里文字压到 caret 上。改成 132px,并由 `test/e2e/language-menu.spec.ts` 逐语言
+量 `scrollWidth <= clientWidth`——这个数字从此是量出来的,不是猜的。
+
+## 3. 加宽触发器之后,手机顶栏塞不下(同一轮修掉)
+
+390px 下 132px 的触发器把自己的 caret 推出视口,"Document Editor" 被挤成两行。
+≤620px 隐藏顶栏的 GitHub 链接(每个页脚都有它),语言切换是那里唯一不能省的控件。
+用例同样落在 language-menu.spec.ts。
+
+## 4. 首页 body 还带着浏览器默认的 8px margin(已修)
+
+`landing.css` 一直有 reset,`home.css` 从来没有——因为首页自建 chrome、不加载
+landing.css。白色主题下看不出来,暗色下就是页面四周一圈底色。`design-contract`
+现在要求两个样式表都有这条,E2E 在四类页面上验计算值。
+
+## 5. `/history` 与 `/404` 的语言切换只有 2 种(已修)
+
+这两个页面是手写的(一个应用页、一个错误页,都不经生成器),语言列表没跟着 `LOCALES`
+走,站点都七种语言了它们还只列 en + zh。**没有任何东西会发现**:两者都不在 sitemap、
+不在 hreflang 图里。补齐 7 项,并加了一条单测按 `Object.keys(LOCALES)` 比对。
+
+## 6. 葡萄牙语在站点与应用之间不是同一种葡语(已修)
+
+`/pt/` 的页面、`bin/build-pages.mjs` 的 UI 表、编辑器加载的 vendor 包(`pt.json` 而不是
+`pt-pt.json`)都是巴西葡语,而 `packages/shared` 的 pt 词条表混着欧洲葡语:
+`ficheiro` / `guardados` / `Definições` / `Não é possível anular` / `está a meio`。
+于是同一个用户在落地页看到巴西葡语、进 `/history` 看到欧洲葡语、编辑器又回到巴西葡语。
+14 条词条改写为巴西葡语(`arquivo` / `salvos` / `Configurações` / `Não dá para desfazer`)。
+
+顺带修掉了更早一轮自己引入的同类问题:新增的 history 词条当时写成了欧洲葡语。
+
+## 7. `llms-full.txt` 被翻译撑到 7 倍(已修)
+
+它遍历 `public/` 下所有 HTML,七种语言进来之后从 97 KB 涨到 680 KB——同样的事实说七遍。
+对读它的模型只有害处。改成只收英文页面,开头说明镜像在哪;`/llms.txt` 仍然逐语言列出。
+
+## 复查过但没有问题的
+
+- **SEO 覆盖**:7 种语言 × 21 页齐全;hreflang 是完整的七语言互指 + x-default;
+ sitemap 154 条;robots 允许全部。
+- **编辑器多语言**:不是七个 HTML,而是 `?locale=` 驱动的同一个 app。逐语言实测过
+ `` 与 vendor frame 的 `lang=`,工具栏确实是对应语言。
+- **横向溢出**:7 种语言 × 桌面/手机两档,除了上面第 3 条之外没有元素越界。
+- **德语长词**:`Installieren und im Flugzeug nutzen`(35 字符)在 pillars 列里正常换行,
+ 没有溢出。
+- **`/embed-demo`**:仍然是英文,这是 CLAUDE.md 的规矩(共用页面用英文),不是遗漏。
+
+## 还没做的
+
+- **`bin/design-audit.mjs` 只审英文页面**。七种语言的行宽/字号没有被审计脚本覆盖,
+ 目前靠 E2E 的溢出检查兜底。
+- **`/history` 与 `/404` 仍是手写页**。它们的 chrome 与生成页重复了一份,这次的语言列表
+ 漏更新就是这个重复的代价。要么把它们也纳入生成器,要么就靠刚加的那条单测钉着。
diff --git a/history.html b/history.html
index 292180a5..2bd07351 100644
--- a/history.html
+++ b/history.html
@@ -65,6 +65,11 @@
EN
中文
+ 日本語
+ Deutsch
+ Español
+ 한국어
+ Português
diff --git a/packages/shared/src/i18n.ts b/packages/shared/src/i18n.ts
index de15b689..46d8c770 100644
--- a/packages/shared/src/i18n.ts
+++ b/packages/shared/src/i18n.ts
@@ -971,7 +971,7 @@ const partialMessages: Record
EN
中文
+ 日本語
+ Deutsch
+ Español
+ 한국어
+ Português
diff --git a/public/home.css b/public/home.css
index 5269d056..3f6953b0 100644
--- a/public/home.css
+++ b/public/home.css
@@ -1035,6 +1035,17 @@ body {
display: none;
}
}
+@media (max-width: 620px) {
+ /* Phone: the bar is brand + GitHub + a language name, and at 390px the
+ language trigger (wide enough for "Português") pushed the caret off the
+ viewport and broke the brand onto two lines. GitHub loses: it is one line
+ down in the footer of every page, while the language switch is the only
+ way to change the language. */
+ #landing-hero .bar nav .navlink.gh {
+ display: none;
+ }
+}
+
@media (max-width: 560px) {
#landing-hero .dw-body {
grid-template-columns: 1fr;
diff --git a/public/landing.css b/public/landing.css
index 1a540368..9e084f64 100644
--- a/public/landing.css
+++ b/public/landing.css
@@ -143,6 +143,17 @@ r-select.lang-select:not(:defined) {
--ran-dropdown-option-active-font-weight: 600;
}
+@media (max-width: 620px) {
+ /* Phone: the bar is brand + GitHub + a language name, and at 390px the
+ language trigger (wide enough for "Português") pushed the caret off the
+ viewport and broke the brand onto two lines. GitHub loses: it is one line
+ down in the footer of every page, while the language switch is the only
+ way to change the language. */
+ header.bar nav > a[href*='github.com'] {
+ display: none;
+ }
+}
+
/* ---------- article column ---------- */
.wrap {
max-width: 720px;
diff --git a/test/e2e/language-menu.spec.ts b/test/e2e/language-menu.spec.ts
index 7546fc10..7d10c42e 100644
--- a/test/e2e/language-menu.spec.ts
+++ b/test/e2e/language-menu.spec.ts
@@ -58,6 +58,29 @@ test.describe('language menu', () => {
});
}
+ /**
+ * The trigger is sized for the longest endonym, which on a 390px phone is
+ * most of what is left after the brand: at 132px it pushed its own caret past
+ * the viewport edge and wrapped "Document Editor" onto two lines. The bar
+ * drops GitHub there (it is in every footer) rather than clipping the only
+ * control that changes the language.
+ */
+ test('fits on a phone, in the longest language, on both kinds of page', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 800 });
+ for (const route of ['/pt/', '/pt/open/docx']) {
+ await page.goto(route);
+ const fits = await page
+ .locator('r-select.lang-select')
+ .first()
+ .evaluate((el) => {
+ const box = el.getBoundingClientRect();
+ return { right: box.right, left: box.left, vw: window.innerWidth };
+ });
+ expect(fits.right, `${route}: the language trigger runs past the viewport`).toBeLessThanOrEqual(fits.vw);
+ expect(fits.left, `${route}: the language trigger starts off-screen`).toBeGreaterThanOrEqual(0);
+ }
+ });
+
test('a satellite page offers the same menu as the homepage', async ({ page }) => {
await page.goto('/pt/open/docx');
const select = page.locator('r-select.lang-select').first();
diff --git a/test/unit/landing-pages.test.ts b/test/unit/landing-pages.test.ts
index 0a5ff6d5..e687f89e 100644
--- a/test/unit/landing-pages.test.ts
+++ b/test/unit/landing-pages.test.ts
@@ -254,6 +254,22 @@ describe('landing pages', () => {
}
});
+ /**
+ * /history and /404 are hand-written (an app surface and an error page, both
+ * outside the generator), so their language switch does not come from
+ * LOCALES -- it was still offering en and zh long after the site had seven
+ * languages. Nothing else would have noticed: neither is in the sitemap or
+ * the hreflang graph.
+ */
+ it.each([
+ ['history.html', resolve(ROOT, 'history.html')],
+ ['404.html', resolve(PUBLIC, '404.html')],
+ ])('%s offers every language the site has', (_label, file) => {
+ const html = readFileSync(file, 'utf8');
+ const offered = [...html.matchAll(/ m[1]);
+ expect(offered).toEqual(Object.keys(LOCALES));
+ });
+
it('llms.txt states the limitations, not just the features', () => {
const llms = readFileSync(resolve(PUBLIC, 'llms.txt'), 'utf8');
expect(llms).toMatch(/^## Limitations/m);