feat(i18n): full app + docs localization (de/en)
Build & Push Docker Image / build (push) Successful in 2m33s
Build & Push Docker Image / build (push) Successful in 2m33s
- translate remaining pages/components (admin, analytics, redundancy, trash, compare, browse, watchlist, admin tools tab, category combobox, theme toggle, tool preview card, tool-new/edit) to t() calls - locales: 453 keys each, parity verified - docs: bilingual handbook + release notes via *.en.md variants, language-aware docs.tsx (markdown paths, nav titles, search index), LanguageSwitcher in docs header - generate-docs: emit per-locale handbook/release/search output, localized index.json fields (fileEn/titleEn), en-aware snapshots
This commit is contained in:
@@ -26,7 +26,12 @@ const releasesDir = resolve(root, "docs/releases");
|
||||
const targetDir = resolve(root, "artifacts/toolrate/public/docs");
|
||||
|
||||
const isReleaseFile = (name) => /^v\d+\.\d+\.\d+\.md$/.test(name);
|
||||
const isHandbookFile = (name) => /\.md$/.test(name);
|
||||
const isEnReleaseFile = (name) => /^v\d+\.\d+\.\d+\.en\.md$/.test(name);
|
||||
const isHandbookFile = (name) => /\.md$/.test(name) && !name.endsWith(".en.md");
|
||||
const isEnHandbookFile = (name) => /\.en\.md$/.test(name);
|
||||
|
||||
const enName = (file) => (file.endsWith(".md") ? `${file.slice(0, -3)}.en.md` : null);
|
||||
const deName = (file) => (file.endsWith(".en.md") ? `${file.slice(0, -6)}.md` : null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Version helpers
|
||||
@@ -196,6 +201,15 @@ function slugify(name) {
|
||||
return name.replace(/\.md$/, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
||||
}
|
||||
|
||||
async function readFileOptional(path) {
|
||||
try {
|
||||
return await readFile(path, "utf8");
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function readHandbookPages() {
|
||||
let files;
|
||||
try {
|
||||
@@ -208,12 +222,18 @@ async function readHandbookPages() {
|
||||
for (const file of files) {
|
||||
const content = await readFile(join(handbookDir, file), "utf8");
|
||||
const { frontmatter, body } = parseHandbook(content);
|
||||
const enFile = enName(file);
|
||||
const enContent = enFile ? await readFileOptional(join(handbookDir, enFile)) : null;
|
||||
const en = enContent ? parseHandbook(enContent) : null;
|
||||
pages.push({
|
||||
slug: slugify(basename(file)),
|
||||
file,
|
||||
title: frontmatter.title ?? extractTitle(body) ?? basename(file),
|
||||
order: typeof frontmatter.order === "number" ? frontmatter.order : 999,
|
||||
body,
|
||||
fileEn: en ? enFile : null,
|
||||
titleEn: en ? en.frontmatter.title ?? extractTitle(en.body) ?? null : null,
|
||||
bodyEn: en ? en.body : null,
|
||||
});
|
||||
}
|
||||
pages.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
||||
@@ -225,11 +245,12 @@ async function writeHandbookTo(dir) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
for (const page of pages) {
|
||||
await writeFile(join(dir, page.file), page.body);
|
||||
if (page.bodyEn) await writeFile(join(dir, page.fileEn), page.bodyEn);
|
||||
}
|
||||
await writeFile(
|
||||
join(dir, "index.json"),
|
||||
JSON.stringify(
|
||||
pages.map(({ slug, file, title, order }) => ({ slug, file, title, order })),
|
||||
pages.map(({ slug, file, title, order, fileEn, titleEn }) => ({ slug, file, title, order, fileEn, titleEn })),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
@@ -256,6 +277,7 @@ function stripMarkdown(md) {
|
||||
|
||||
async function buildSearchIndex(reference, handbookPages, releaseVersions) {
|
||||
const entries = [];
|
||||
const entriesEn = [];
|
||||
|
||||
for (const page of handbookPages) {
|
||||
const file = join(handbookDir, page.file);
|
||||
@@ -267,44 +289,58 @@ async function buildSearchIndex(reference, handbookPages, releaseVersions) {
|
||||
kind: "guide",
|
||||
text: stripMarkdown(body),
|
||||
});
|
||||
if (page.bodyEn) {
|
||||
entriesEn.push({
|
||||
title: page.titleEn ?? page.title,
|
||||
href: `/docs/handbook/${page.slug}`,
|
||||
kind: "guide",
|
||||
text: stripMarkdown(page.bodyEn),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of reference.tags) {
|
||||
for (const ep of tag.endpoints) {
|
||||
entries.push({
|
||||
const entry = {
|
||||
title: `${ep.method} ${ep.path}`,
|
||||
href: `/docs/reference/endpoints/${tag.name}#${ep.operationId}`,
|
||||
kind: "endpoint",
|
||||
text: `${ep.summary} ${ep.description} ${ep.parameters
|
||||
.map((p) => `${p.name} ${p.description}`)
|
||||
.join(" ")}`.trim(),
|
||||
});
|
||||
};
|
||||
entries.push(entry);
|
||||
entriesEn.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (const schema of reference.schemas) {
|
||||
for (const field of schema.fields) {
|
||||
entries.push({
|
||||
const entry = {
|
||||
title: `${schema.name}.${field.name}`,
|
||||
href: `/docs/reference/schemas/${schema.name}#${field.name}`,
|
||||
kind: "field",
|
||||
text: `${field.description} ${field.constraints} ${field.type.value ?? ""}`.trim(),
|
||||
});
|
||||
};
|
||||
entries.push(entry);
|
||||
entriesEn.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of releaseVersions) {
|
||||
const file = join(releasesDir, `${version}.md`);
|
||||
const content = await readFile(file, "utf8");
|
||||
entries.push({
|
||||
const base = {
|
||||
title: version,
|
||||
href: `/docs/releases/${version}`,
|
||||
kind: "release",
|
||||
text: stripMarkdown(content),
|
||||
});
|
||||
};
|
||||
entries.push({ ...base, text: stripMarkdown(content) });
|
||||
const enContent = await readFileOptional(join(releasesDir, `${version}.en.md`));
|
||||
entriesEn.push({ ...base, text: stripMarkdown(enContent ?? content) });
|
||||
}
|
||||
|
||||
return entries;
|
||||
return { entries, entriesEn };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -348,7 +384,6 @@ async function main() {
|
||||
throw err;
|
||||
}
|
||||
releaseNames.sort(cmp).reverse();
|
||||
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
await mkdir(join(targetDir, "handbook"), { recursive: true });
|
||||
await mkdir(join(targetDir, "releases"), { recursive: true });
|
||||
@@ -363,7 +398,18 @@ async function main() {
|
||||
const handbookPages = await buildHandbookIndex();
|
||||
await writeFile(
|
||||
join(targetDir, "handbook/index.json"),
|
||||
JSON.stringify(handbookPages, null, 2),
|
||||
JSON.stringify(
|
||||
handbookPages.map(({ slug, file, title, order, fileEn, titleEn }) => ({
|
||||
slug,
|
||||
file,
|
||||
title,
|
||||
order,
|
||||
fileEn,
|
||||
titleEn,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// 3. Release notes + versioned docs snapshots (last 7 versions)
|
||||
@@ -372,6 +418,9 @@ async function main() {
|
||||
const version = parseVersion(name);
|
||||
const content = await readFile(join(releasesDir, name), "utf8");
|
||||
await copyFile(join(releasesDir, name), join(targetDir, "releases", name));
|
||||
const enName = `${version}.en.md`;
|
||||
const enContent = await readFileOptional(join(releasesDir, enName));
|
||||
if (enContent) await copyFile(join(releasesDir, enName), join(targetDir, "releases", enName));
|
||||
const snapDir = join(releasesDir, version);
|
||||
const hasReference = await stat(join(snapDir, "reference.json"))
|
||||
.then(() => true)
|
||||
@@ -399,12 +448,20 @@ async function main() {
|
||||
join(snapDir, "handbook", page.file),
|
||||
join(targetDir, "versions", version, "handbook", page.file),
|
||||
);
|
||||
if (page.fileEn) {
|
||||
await copyFile(
|
||||
join(snapDir, "handbook", page.fileEn),
|
||||
join(targetDir, "versions", version, "handbook", page.fileEn),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
versions.push({
|
||||
version,
|
||||
file: `releases/${name}`,
|
||||
fileEn: enContent ? `releases/${enName}` : null,
|
||||
title: extractTitle(content) ?? version,
|
||||
titleEn: enContent ? extractTitle(enContent) ?? version : null,
|
||||
date: extractDate(content) ?? null,
|
||||
hasReference,
|
||||
hasHandbook,
|
||||
@@ -414,12 +471,13 @@ async function main() {
|
||||
await writeFile(join(targetDir, "index.json"), JSON.stringify(versions, null, 2));
|
||||
|
||||
// 4. Search index
|
||||
const searchIndex = await buildSearchIndex(reference, handbookPages, versions.map((v) => v.version));
|
||||
await writeFile(join(targetDir, "search.json"), JSON.stringify(searchIndex, null, 2));
|
||||
const { entries, entriesEn } = await buildSearchIndex(reference, handbookPages, versions.map((v) => v.version));
|
||||
await writeFile(join(targetDir, "search.json"), JSON.stringify(entries, null, 2));
|
||||
await writeFile(join(targetDir, "search.en.json"), JSON.stringify(entriesEn, null, 2));
|
||||
|
||||
console.log(
|
||||
`[generate-docs] synced ${versions.length} release(s), ${handbookPages.length} handbook page(s), ` +
|
||||
`${reference.schemas.length} schema(s), ${searchIndex.length} search entries`,
|
||||
`${reference.schemas.length} schema(s), ${entries.length} search entries`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user