fix(i18n): enforce localized core doc links

Generated-By: looper 0.2.7 (runner=fixer, agent=gpt-5.5)
This commit is contained in:
nettee
2026-05-06 11:40:46 +08:00
parent ea3140369f
commit 8d0d8ca2f7
4 changed files with 83 additions and 11 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
<a href="#design-systems"><img alt="Design systems" src="https://img.shields.io/badge/design%20systems-72-orange?style=flat-square" /></a>
<a href="#skills"><img alt="Skills" src="https://img.shields.io/badge/skills-31-teal?style=flat-square" /></a>
<a href="https://discord.gg/qhbcCH8Am4"><img alt="Discord" src="https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
<a href="QUICKSTART.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
<a href="QUICKSTART.de.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.ko.md">한국어</a> · <a href="README.ja-JP.md">日本語</a> · <a href="README.ar.md">العربية</a> · <a href="README.ru.md">Русский</a> · <a href="README.uk.md">Українська</a></p>
+3 -3
View File
@@ -25,7 +25,7 @@
<img alt="Design systems" src="https://img.shields.io/badge/design%20systems-72-orange?style=flat-square" /></a>
<a href="#組み込み-skill"><img alt="Skills" src="https://img.shields.io/badge/skills-31-teal?style=flat-square" /></a>
<a href="https://discord.gg/qhbcCH8Am4"><img alt="Discord" src="https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
<a href="QUICKSTART.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
<a href="QUICKSTART.ja-JP.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.ko.md">한국어</a> · <b>日本語</b> · <a href="README.ar.md">العربية</a> · <a href="README.ru.md">Русский</a> · <a href="README.uk.md">Українська</a></p>
@@ -322,7 +322,7 @@ pnpm tools-dev run web
環境要件:Node `~24`、pnpm `10.33.x`。`nvm` / `fnm` はあくまでオプションのヘルパーです。使用する場合は `pnpm install` の前に `nvm install 24 && nvm use 24` または `fnm install 24 && fnm use 24` を実行してください。
デスクトップ / バックグラウンド起動、固定ポート再起動、メディア生成ディスパッチャの確認(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)は [`QUICKSTART.md`](QUICKSTART.md) を参照。
デスクトップ / バックグラウンド起動、固定ポート再起動、メディア生成ディスパッチャの確認(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)は [`QUICKSTART.ja-JP.md`](QUICKSTART.ja-JP.md) を参照。
初回ロード時:
@@ -350,7 +350,7 @@ Daemon はリポジトリルートに 1 つの隠しフォルダを管理しま
| 完全にリセット | `pnpm tools-dev stop` → `rm -rf .od` → `pnpm tools-dev run web` を再実行 |
| 別の場所に移動 | 未対応 — パスはリポジトリルートからの相対パスで固定 |
完全なファイルマップ、スクリプト、トラブルシューティング → [`QUICKSTART.md`](QUICKSTART.md)。
完全なファイルマップ、スクリプト、トラブルシューティング → [`QUICKSTART.ja-JP.md`](QUICKSTART.ja-JP.md)。
## リポジトリ構成
+3 -3
View File
@@ -24,7 +24,7 @@
<a href="#design-system"><img alt="Design systems" src="https://img.shields.io/badge/design%20systems-72-orange?style=flat-square" /></a>
<a href="#内置-skills"><img alt="Skills" src="https://img.shields.io/badge/skills-31-teal?style=flat-square" /></a>
<a href="https://discord.gg/qhbcCH8Am4"><img alt="Discord" src="https://img.shields.io/badge/discord-加入-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
<a href="QUICKSTART.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
<a href="QUICKSTART.zh-CN.md"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-3%20commands-green?style=flat-square" /></a>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>简体中文</b> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.ko.md">한국어</a> · <a href="README.ja-JP.md">日本語</a> · <a href="README.ar.md">العربية</a> · <a href="README.ru.md">Русский</a> · <a href="README.uk.md">Українська</a></p>
@@ -321,7 +321,7 @@ pnpm tools-dev run web
环境要求:Node `~24`,pnpm `10.33.x`。`nvm` / `fnm` 只是可选辅助工具,不是项目必需步骤;如果使用它们,先执行 `nvm install 24 && nvm use 24` 或 `fnm install 24 && fnm use 24`,再运行 `pnpm install`。
桌面端/后台启动、固定端口重启,以及 media 生成派发器检查(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)见 [`QUICKSTART.md`](QUICKSTART.md)。
桌面端/后台启动、固定端口重启,以及 media 生成派发器检查(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)见 [`QUICKSTART.zh-CN.md`](QUICKSTART.zh-CN.md)。
第一次加载会:
@@ -349,7 +349,7 @@ Daemon 在仓库根下维护一个隐藏目录,里面所有内容都已 gitign
| 完全清空,从零再来 | `pnpm tools-dev stop`,再 `rm -rf .od`,然后重新 `pnpm tools-dev run web` |
| 换到别的位置 | 暂不支持 —— 路径是相对仓库根写死的 |
完整文件地图、脚本、排错 → [`QUICKSTART.md`](QUICKSTART.md)。
完整文件地图、脚本、排错 → [`QUICKSTART.zh-CN.md`](QUICKSTART.zh-CN.md)。
## 仓库结构
+76 -4
View File
@@ -17,6 +17,14 @@ type ReadmeSwitcherEntry = {
bold: boolean;
};
type CoreDocLink = {
label: string;
target: string;
syntax: "html" | "markdown";
};
const coreDocTargetPattern = "(QUICKSTART(?:\\.[A-Za-z0-9-]+)?\\.md|CONTRIBUTING(?:\\.[A-Za-z0-9-]+)?\\.md)";
function repositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
@@ -111,6 +119,47 @@ function readmeTarget(fileName: string): string {
return fileName === "README.md" ? "README.md" : fileName;
}
function readmeLocale(fileName: string): string | null {
if (fileName === "README.md") return null;
const match = fileName.match(/^README\.([A-Za-z0-9-]+)\.md$/);
return match?.[1] ?? null;
}
function coreDocSourceName(target: string): "QUICKSTART.md" | "CONTRIBUTING.md" | null {
if (target.startsWith("QUICKSTART")) return "QUICKSTART.md";
if (target.startsWith("CONTRIBUTING")) return "CONTRIBUTING.md";
return null;
}
function localizedCoreDocName(sourceName: "QUICKSTART.md" | "CONTRIBUTING.md", locale: string): string {
return sourceName.replace(/\.md$/, `.${locale}.md`);
}
function isExplicitEnglishCoreDocLink(link: CoreDocLink): boolean {
return link.syntax === "markdown" && link.label.trim() === "English";
}
function extractCoreDocLinks(source: string): CoreDocLink[] {
const links: CoreDocLink[] = [];
const markdownPattern = new RegExp(`\\[([^\\]]*)\\]\\(${coreDocTargetPattern}\\)`, "g");
const htmlHrefPattern = new RegExp(`<a\\b[^>]*\\bhref=(["'])${coreDocTargetPattern}\\1[^>]*>`, "g");
for (const match of source.matchAll(markdownPattern)) {
const label = match[1];
const target = match[2];
if (label == null || target == null) continue;
links.push({ label, target, syntax: "markdown" });
}
for (const match of source.matchAll(htmlHrefPattern)) {
const target = match[2];
if (target == null) continue;
links.push({ label: "", target, syntax: "html" });
}
return links;
}
async function checkReadmeSwitchers(): Promise<CheckResult> {
const errors: string[] = [];
const readmes = await rootReadmeFiles();
@@ -169,15 +218,38 @@ async function checkReadmeSwitchers(): Promise<CheckResult> {
async function checkCoreDocLinks(): Promise<CheckResult> {
const errors: string[] = [];
const readmes = await rootReadmeFiles();
const coreDocPattern = /\]\((QUICKSTART(?:\.[A-Za-z0-9-]+)?\.md|CONTRIBUTING(?:\.[A-Za-z0-9-]+)?\.md)\)/g;
for (const readme of readmes) {
const source = await readFile(path.join(repoRoot, readme), "utf8");
for (const match of source.matchAll(coreDocPattern)) {
const target = match[1];
if (target && !(await pathExists(path.join(repoRoot, target)))) {
const locale = readmeLocale(readme);
const links = extractCoreDocLinks(source);
const linkedTargets = new Set(links.map((link) => link.target));
for (const link of links) {
const target = link.target;
if (!(await pathExists(path.join(repoRoot, target)))) {
errors.push(`${readme} links to missing core doc ${target}.`);
}
if (locale == null) continue;
const sourceName = coreDocSourceName(target);
if (sourceName == null || target !== sourceName || isExplicitEnglishCoreDocLink(link)) continue;
const localizedName = localizedCoreDocName(sourceName, locale);
if (await pathExists(path.join(repoRoot, localizedName))) {
errors.push(`${readme} links to ${sourceName}, but ${localizedName} exists for this README locale.`);
}
}
if (locale == null) continue;
for (const sourceName of ["QUICKSTART.md", "CONTRIBUTING.md"] as const) {
const localizedName = localizedCoreDocName(sourceName, locale);
if ((await pathExists(path.join(repoRoot, localizedName))) && links.some((link) => coreDocSourceName(link.target) === sourceName)) {
if (!linkedTargets.has(localizedName)) {
errors.push(`${readme} links to ${sourceName} docs, but does not link to localized ${localizedName}.`);
}
}
}
}