chore(docs): move translated docs into docs/i18n/ (#3621)

* chore(docs): move translated docs into docs/i18n/

Collect the translated README/QUICKSTART/CONTRIBUTING/MAINTAINERS files
(including the Korean set) into docs/i18n/, leaving only the English sources
in the repo root so the GitHub project home page file list stays clean.
Rewrite internal links for the new layout (../../ for repo-root resources,
sibling filenames between translations), update both switcher conventions,
the i18n-check mixed-layout support, the contributors-wall workflow globs,
TRANSLATIONS.md guidance, and drop now-dead root translation paths from the
fork-PR docs allowlist.

* fix(docs): correct root-relative links in Korean contribution guide

Prefix repo-root targets (scripts/sync-design-systems.ts, TRANSLATIONS.md,
package.json, .github/pull_request_template.md) with ../../ so they resolve
from the new docs/i18n/ depth; sibling translated docs stay bare.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
This commit is contained in:
elihahah666
2026-06-04 06:55:16 +00:00
committed by GitHub
co-authored by qiongyu1999
parent fa80a084ec
commit 67077fd36f
41 changed files with 1279 additions and 1221 deletions
-2
View File
@@ -145,9 +145,7 @@ export function isDeniedChangedPath(path: string): boolean {
function isDocsPath(path: string): boolean {
return (
path === "README.md" ||
path === "README.zh-CN.md" ||
path === "CONTRIBUTING.md" ||
path === "CONTRIBUTING.zh-CN.md" ||
path === "QUICKSTART.md" ||
path.startsWith("docs/")
);
+68 -24
View File
@@ -5,6 +5,9 @@ import { LOCALE_LABEL, LOCALES, type Locale } from "../apps/web/src/i18n/types.t
const repoRoot = path.resolve(import.meta.dirname, "..");
const localesDirectory = path.join(repoRoot, "apps/web/src/i18n/locales");
const i18nIndexPath = path.join(repoRoot, "apps/web/src/i18n/index.tsx");
// English canonical docs (README.md, CONTRIBUTING.md, ...) stay at repo root for
// GitHub project-page visibility; their translations live under docs/i18n/.
const translationsDir = "docs/i18n";
type CheckResult = {
name: string;
@@ -23,7 +26,8 @@ type CoreDocLink = {
syntax: "html" | "markdown";
};
const coreDocTargetPattern = "(QUICKSTART(?:\\.[A-Za-z0-9-]+)?\\.md|CONTRIBUTING(?:\\.[A-Za-z0-9-]+)?\\.md)";
const coreDocTargetPattern =
"((?:(?:\\.\\./\\.\\./|docs/i18n/))?(?: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("/");
@@ -90,12 +94,43 @@ async function checkUiLocaleRegistration(): Promise<CheckResult> {
return { name: "UI locale registration", errors };
}
// README.md (English) lives at repo root; README.<locale>.md live under docs/i18n/.
// Returns canonical names (README.md, README.zh-CN.md, ...) sorted with English first.
async function rootReadmeFiles(): Promise<string[]> {
const entries = await readdir(repoRoot, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && /^README(?:\.[A-Za-z0-9-]+)?\.md$/.test(entry.name))
.map((entry) => entry.name)
.sort((left, right) => (left === "README.md" ? -1 : right === "README.md" ? 1 : left.localeCompare(right)));
const names = new Set<string>();
const rootEntries = await readdir(repoRoot, { withFileTypes: true });
for (const entry of rootEntries) {
if (entry.isFile() && entry.name === "README.md") names.add(entry.name);
}
const translationEntries = await readdir(path.join(repoRoot, translationsDir), { withFileTypes: true });
for (const entry of translationEntries) {
if (entry.isFile() && /^README\.[A-Za-z0-9-]+\.md$/.test(entry.name)) names.add(entry.name);
}
return Array.from(names).sort((left, right) =>
left === "README.md" ? -1 : right === "README.md" ? 1 : left.localeCompare(right),
);
}
// Physical path of a canonical doc name: English (no locale) at root, translations under docs/i18n/.
function docPhysicalPath(canonicalName: string): string {
const isEnglish = /^(README|CONTRIBUTING|QUICKSTART|MAINTAINERS)\.md$/.test(canonicalName);
return isEnglish ? path.join(repoRoot, canonicalName) : path.join(repoRoot, translationsDir, canonicalName);
}
// The href a document at `fromName` should use to link to canonical doc `toName`.
// Same directory (both translations, or both root English) → bare name.
// Translation → English root → ../../NAME. English root → translation → docs/i18n/NAME.
function expectedDocHref(fromName: string, toName: string): string {
const fromEnglish = /^(README|CONTRIBUTING|QUICKSTART|MAINTAINERS)\.md$/.test(fromName);
const toEnglish = /^(README|CONTRIBUTING|QUICKSTART|MAINTAINERS)\.md$/.test(toName);
if (fromEnglish === toEnglish) return toName;
if (!fromEnglish && toEnglish) return `../../${toName}`;
return `${translationsDir}/${toName}`;
}
// Strip any docs/i18n/ or ../../ prefix to recover the canonical doc name from a link target.
function canonicalDocName(target: string): string {
return target.replace(/^(?:\.\.\/\.\.\/|docs\/i18n\/)/, "");
}
function extractReadmeSwitcher(source: string): ReadmeSwitcherEntry[] | null {
@@ -165,38 +200,40 @@ async function checkReadmeSwitchers(): Promise<CheckResult> {
const readmes = await rootReadmeFiles();
const readmeSet = new Set(readmes);
const canonicalName = "README.md";
const canonicalSource = await readFile(path.join(repoRoot, canonicalName), "utf8");
const canonicalSource = await readFile(docPhysicalPath(canonicalName), "utf8");
const canonicalEntries = extractReadmeSwitcher(canonicalSource);
if (!canonicalEntries) {
return { name: "root README language switchers", errors: [`${canonicalName} has no root README language switcher.`] };
}
const canonicalTargets = canonicalEntries.map((entry) => entry.href ?? canonicalName);
// Normalize each switcher entry's href to a canonical README name, so files in
// different directories (root English vs docs/i18n translations) compare equal.
const canonicalOrder = canonicalEntries.map((entry) => (entry.href == null ? canonicalName : canonicalDocName(entry.href)));
const expectedTargets = new Set(readmes.map(readmeTarget));
const canonicalTargetSet = new Set(canonicalTargets);
const canonicalTargetSet = new Set(canonicalOrder);
if (
canonicalTargetSet.size !== expectedTargets.size ||
canonicalTargets.some((target) => !expectedTargets.has(target)) ||
canonicalOrder.some((target) => !expectedTargets.has(target)) ||
Array.from(expectedTargets).some((target) => !canonicalTargetSet.has(target))
) {
errors.push(
`${canonicalName} switcher targets differ from root README files. Expected ${Array.from(expectedTargets).join(", ")}; found ${canonicalTargets.join(", ")}.`,
`${canonicalName} switcher targets differ from README files. Expected ${Array.from(expectedTargets).join(", ")}; found ${canonicalOrder.join(", ")}.`,
);
}
for (const readme of readmes) {
const source = await readFile(path.join(repoRoot, readme), "utf8");
const source = await readFile(docPhysicalPath(readme), "utf8");
const entries = extractReadmeSwitcher(source);
if (!entries) {
errors.push(`${readme} has no root README language switcher.`);
continue;
}
const targets = entries.map((entry) => entry.href ?? readme);
if (targets.join("\n") !== canonicalTargets.join("\n")) {
errors.push(`${readme} switcher order differs. Expected ${canonicalTargets.join(", ")}; found ${targets.join(", ")}.`);
const order = entries.map((entry) => (entry.href == null ? readme : canonicalDocName(entry.href)));
if (order.join("\n") !== canonicalOrder.join("\n")) {
errors.push(`${readme} switcher order differs. Expected ${canonicalOrder.join(", ")}; found ${order.join(", ")}.`);
}
const boldEntries = entries.filter((entry) => entry.bold);
@@ -206,8 +243,14 @@ async function checkReadmeSwitchers(): Promise<CheckResult> {
for (const entry of entries) {
if (entry.href == null) continue;
if (!readmeSet.has(entry.href)) {
errors.push(`${readme} links to missing root README ${entry.href}.`);
const targetName = canonicalDocName(entry.href);
if (!readmeSet.has(targetName)) {
errors.push(`${readme} links to missing README ${entry.href}.`);
continue;
}
const expectedHref = expectedDocHref(readme, targetName);
if (entry.href !== expectedHref) {
errors.push(`${readme} switcher links to ${entry.href} but should use ${expectedHref} from its location.`);
}
}
}
@@ -220,15 +263,16 @@ async function checkCoreDocLinks(): Promise<CheckResult> {
const readmes = await rootReadmeFiles();
for (const readme of readmes) {
const source = await readFile(path.join(repoRoot, readme), "utf8");
const source = await readFile(docPhysicalPath(readme), "utf8");
const locale = readmeLocale(readme);
const links = extractCoreDocLinks(source);
const linkedTargets = new Set(links.map((link) => link.target));
// Normalize link targets to canonical doc names, dropping any docs/i18n/ or ../../ prefix.
const linkedTargets = new Set(links.map((link) => canonicalDocName(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}.`);
const target = canonicalDocName(link.target);
if (!(await pathExists(docPhysicalPath(target)))) {
errors.push(`${readme} links to missing core doc ${link.target}.`);
}
if (locale == null) continue;
@@ -237,7 +281,7 @@ async function checkCoreDocLinks(): Promise<CheckResult> {
if (sourceName == null || target !== sourceName || isExplicitEnglishCoreDocLink(link)) continue;
const localizedName = localizedCoreDocName(sourceName, locale);
if (await pathExists(path.join(repoRoot, localizedName))) {
if (await pathExists(docPhysicalPath(localizedName))) {
errors.push(`${readme} links to ${sourceName}, but ${localizedName} exists for this README locale.`);
}
}
@@ -245,7 +289,7 @@ async function checkCoreDocLinks(): Promise<CheckResult> {
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 ((await pathExists(docPhysicalPath(localizedName))) && links.some((link) => coreDocSourceName(canonicalDocName(link.target)) === sourceName)) {
if (!linkedTargets.has(localizedName)) {
errors.push(`${readme} links to ${sourceName} docs, but does not link to localized ${localizedName}.`);
}