@tuwaio/docs-ui 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -3
- package/bin/i18n-check.js +19 -0
- package/dist/components/DocsLayout.cjs +82 -0
- package/dist/components/DocsLayout.d.cts +50 -0
- package/dist/components/DocsLayout.d.ts +50 -0
- package/dist/components/DocsLayout.js +58 -0
- package/dist/components/LocalizedPage.cjs +42 -0
- package/dist/components/LocalizedPage.d.cts +37 -0
- package/dist/components/LocalizedPage.d.ts +37 -0
- package/dist/components/LocalizedPage.js +18 -0
- package/dist/docsI18n.cjs +33 -0
- package/dist/docsI18n.d.cts +16 -0
- package/dist/docsI18n.d.ts +16 -0
- package/dist/docsI18n.js +8 -0
- package/dist/docsLabels.cjs +140 -0
- package/dist/docsLabels.d.cts +72 -0
- package/dist/docsLabels.d.ts +72 -0
- package/dist/docsLabels.js +116 -0
- package/dist/docsPage.cjs +70 -0
- package/dist/docsPage.d.cts +56 -0
- package/dist/docsPage.d.ts +56 -0
- package/dist/docsPage.js +46 -0
- package/dist/i18n.d.cts +1 -1
- package/dist/i18n.d.ts +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.css +3 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/nextra.cjs +26 -0
- package/dist/nextra.d.cts +10 -0
- package/dist/nextra.d.ts +10 -0
- package/dist/nextra.js +3 -0
- package/dist/pageMap.cjs +55 -0
- package/dist/pageMap.d.cts +20 -0
- package/dist/pageMap.d.ts +20 -0
- package/dist/pageMap.js +31 -0
- package/dist/translationCheck.cjs +186 -0
- package/dist/translationCheck.d.cts +89 -0
- package/dist/translationCheck.d.ts +89 -0
- package/dist/translationCheck.js +157 -0
- package/package.json +15 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { extname, join, sep } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { DEFAULT_LOCALE, TUWA_LOCALES } from "./locale.js";
|
|
6
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
7
|
+
function splitFences(source) {
|
|
8
|
+
const blocks = [];
|
|
9
|
+
const prose = [];
|
|
10
|
+
let block = null;
|
|
11
|
+
let fence = "";
|
|
12
|
+
for (const line of source.split("\n")) {
|
|
13
|
+
if (block) {
|
|
14
|
+
block.push(line);
|
|
15
|
+
if (line.trim().startsWith(fence) && line.trim().replace(/[`~]/g, "") === "") {
|
|
16
|
+
blocks.push(block.join("\n"));
|
|
17
|
+
block = null;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const open = FENCE_RE.exec(line);
|
|
22
|
+
if (open) {
|
|
23
|
+
fence = open[1];
|
|
24
|
+
block = [line];
|
|
25
|
+
} else {
|
|
26
|
+
prose.push(line);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (block) blocks.push(block.join("\n"));
|
|
30
|
+
return { blocks, prose: prose.join("\n") };
|
|
31
|
+
}
|
|
32
|
+
function extractCodeBlocks(source) {
|
|
33
|
+
return splitFences(source).blocks;
|
|
34
|
+
}
|
|
35
|
+
function plainText(markdown) {
|
|
36
|
+
return markdown.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/(\*\*|__)(.+?)\1/g, "$2").replace(/(\*|_)(.+?)\1/g, "$2").replace(/`([^`]*)`/g, "$1").trim();
|
|
37
|
+
}
|
|
38
|
+
function extractHeadings(source) {
|
|
39
|
+
const headings = [];
|
|
40
|
+
for (const line of splitFences(source).prose.split("\n")) {
|
|
41
|
+
const match = /^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
42
|
+
if (!match) continue;
|
|
43
|
+
const custom = /\s*\[#([^\]]+)\]\s*$/.exec(match[2]);
|
|
44
|
+
const text = plainText(custom ? match[2].slice(0, custom.index) : match[2]);
|
|
45
|
+
headings.push({ depth: match[1].length, text, ...custom && { customId: custom[1] } });
|
|
46
|
+
}
|
|
47
|
+
return headings;
|
|
48
|
+
}
|
|
49
|
+
function extractLinks(source) {
|
|
50
|
+
const prose = splitFences(source).prose.replace(/`[^`\n]*`/g, "");
|
|
51
|
+
const targets = [];
|
|
52
|
+
for (const match of prose.matchAll(/\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)|\b(?:href|src)=["']([^"']+)["']/g)) {
|
|
53
|
+
targets.push(match[1] ?? match[2]);
|
|
54
|
+
}
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
function headingIds(headings, createSlugger) {
|
|
58
|
+
const slug = createSlugger();
|
|
59
|
+
return headings.filter((heading) => heading.depth > 1).map((heading) => slug(heading.customId ?? heading.text));
|
|
60
|
+
}
|
|
61
|
+
const sorted = (values) => [...values].sort();
|
|
62
|
+
function checkTranslation(english, translated, createSlugger) {
|
|
63
|
+
const issues = [];
|
|
64
|
+
const englishBlocks = extractCodeBlocks(english);
|
|
65
|
+
const translatedBlocks = extractCodeBlocks(translated);
|
|
66
|
+
if (englishBlocks.length !== translatedBlocks.length) {
|
|
67
|
+
issues.push(`has ${translatedBlocks.length} code blocks, the English page ${englishBlocks.length}`);
|
|
68
|
+
} else {
|
|
69
|
+
englishBlocks.forEach((block, index) => {
|
|
70
|
+
if (block !== translatedBlocks[index]) issues.push(`code block ${index + 1} differs from the English one`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const englishHeadings = extractHeadings(english);
|
|
74
|
+
const translatedHeadings = extractHeadings(translated);
|
|
75
|
+
const levels = (headings) => headings.map((heading) => "#".repeat(heading.depth)).join(" ");
|
|
76
|
+
if (levels(englishHeadings) !== levels(translatedHeadings)) {
|
|
77
|
+
issues.push(`heading levels "${levels(translatedHeadings)}" differ from the English "${levels(englishHeadings)}"`);
|
|
78
|
+
} else if (createSlugger) {
|
|
79
|
+
const englishIds = headingIds(englishHeadings, createSlugger);
|
|
80
|
+
const translatedIds = headingIds(translatedHeadings, createSlugger);
|
|
81
|
+
englishIds.forEach((id, index) => {
|
|
82
|
+
if (id !== translatedIds[index]) {
|
|
83
|
+
issues.push(
|
|
84
|
+
`heading "${translatedHeadings.filter((h) => h.depth > 1)[index].text}" has the id "${translatedIds[index]}", the English one "${id}" (add [#${id}])`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const englishLinks = sorted(extractLinks(english));
|
|
90
|
+
const translatedLinks = sorted(extractLinks(translated));
|
|
91
|
+
const missing = englishLinks.filter((link) => !translatedLinks.includes(link));
|
|
92
|
+
const extra = translatedLinks.filter((link) => !englishLinks.includes(link));
|
|
93
|
+
if (missing.length || extra.length || englishLinks.length !== translatedLinks.length) {
|
|
94
|
+
issues.push(
|
|
95
|
+
`links differ from the English page (missing: ${missing.join(", ") || "none"}; not in English: ${extra.join(", ") || "none"}; ${translatedLinks.length} vs ${englishLinks.length})`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
function pageFiles(dir) {
|
|
101
|
+
if (!existsSync(dir)) return [];
|
|
102
|
+
return readdirSync(dir, { recursive: true, encoding: "utf8" }).filter((file) => [".md", ".mdx"].includes(extname(file))).map((file) => file.split(sep).join("/")).sort();
|
|
103
|
+
}
|
|
104
|
+
const withoutExtension = (file) => file.slice(0, -extname(file).length);
|
|
105
|
+
async function loadNextraSlugger(cwd) {
|
|
106
|
+
try {
|
|
107
|
+
const nextra = createRequire(join(cwd, "package.json")).resolve("nextra/package.json");
|
|
108
|
+
const sluggerPath = createRequire(nextra).resolve("github-slugger");
|
|
109
|
+
const { default: GithubSlugger } = await import(pathToFileURL(sluggerPath).href);
|
|
110
|
+
return () => {
|
|
111
|
+
const slugger = new GithubSlugger();
|
|
112
|
+
return (text) => slugger.slug(text);
|
|
113
|
+
};
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function checkSiteTranslations(options = {}) {
|
|
119
|
+
const { cwd = process.cwd(), contentDir = "src/content", i18nDir = "src/i18n" } = options;
|
|
120
|
+
if (!existsSync(join(cwd, contentDir))) {
|
|
121
|
+
throw new Error(`No ${contentDir} in ${cwd}: run the check from the docs app of the site`);
|
|
122
|
+
}
|
|
123
|
+
const createSlugger = options.createSlugger ?? await loadNextraSlugger(cwd);
|
|
124
|
+
const locales = TUWA_LOCALES.filter((locale) => locale !== DEFAULT_LOCALE);
|
|
125
|
+
const filesByLocale = new Map(locales.map((locale) => [locale, pageFiles(join(cwd, i18nDir, locale))]));
|
|
126
|
+
const pages = [...new Set([...filesByLocale.values()].flat().map(withoutExtension))].sort();
|
|
127
|
+
const issues = [];
|
|
128
|
+
let files = 0;
|
|
129
|
+
for (const page of pages) {
|
|
130
|
+
const englishFile = [".mdx", ".md"].map((ext) => join(cwd, contentDir, page + ext)).find(existsSync);
|
|
131
|
+
for (const locale of locales) {
|
|
132
|
+
const translatedFile = filesByLocale.get(locale)?.find((file) => withoutExtension(file) === page);
|
|
133
|
+
const shown = join(i18nDir, locale, translatedFile ?? `${page}.mdx`).split(sep).join("/");
|
|
134
|
+
if (!translatedFile) {
|
|
135
|
+
issues.push({ file: shown, message: "is missing: every translated page exists in every language" });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
files += 1;
|
|
139
|
+
if (!englishFile) {
|
|
140
|
+
issues.push({ file: shown, message: `has no English page ${join(contentDir, page)}.mdx` });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const english = readFileSync(englishFile, "utf8");
|
|
144
|
+
const translated = readFileSync(join(cwd, i18nDir, locale, translatedFile), "utf8");
|
|
145
|
+
for (const message of checkTranslation(english, translated, createSlugger)) issues.push({ file: shown, message });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { files, issues };
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
checkSiteTranslations,
|
|
152
|
+
checkTranslation,
|
|
153
|
+
extractCodeBlocks,
|
|
154
|
+
extractHeadings,
|
|
155
|
+
extractLinks,
|
|
156
|
+
loadNextraSlugger
|
|
157
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuwaio/docs-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Oleksandr Tkach",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
-
"description": "Shared UI of the TUWA documentation sites: Nextra navbar and
|
|
7
|
+
"description": "Shared UI of the TUWA documentation sites: Nextra navbar, footer and layout in English, Ukrainian and Simplified Chinese, remote logo, theme and language switches, background effects, policy pages and styles.",
|
|
8
8
|
"main": "./dist/index.cjs",
|
|
9
9
|
"module": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"bin": {
|
|
12
|
-
"tuwa-docs-search-index": "./bin/search-index.js"
|
|
12
|
+
"tuwa-docs-search-index": "./bin/search-index.js",
|
|
13
|
+
"tuwa-docs-i18n-check": "./bin/i18n-check.js"
|
|
13
14
|
},
|
|
14
15
|
"type": "module",
|
|
15
16
|
"exports": {
|
|
@@ -33,6 +34,16 @@
|
|
|
33
34
|
"import": "./dist/i18n.js",
|
|
34
35
|
"require": "./dist/i18n.cjs"
|
|
35
36
|
},
|
|
37
|
+
"./nextra": {
|
|
38
|
+
"types": "./dist/nextra.d.ts",
|
|
39
|
+
"import": "./dist/nextra.js",
|
|
40
|
+
"require": "./dist/nextra.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./nextra/page": {
|
|
43
|
+
"types": "./dist/docsPage.d.ts",
|
|
44
|
+
"import": "./dist/docsPage.js",
|
|
45
|
+
"require": "./dist/docsPage.cjs"
|
|
46
|
+
},
|
|
36
47
|
"./dist/index.css": "./dist/index.css",
|
|
37
48
|
"./dist/*": "./dist/*"
|
|
38
49
|
},
|
|
@@ -79,6 +90,7 @@
|
|
|
79
90
|
"peerDependencies": {
|
|
80
91
|
"@heroicons/react": ">=2",
|
|
81
92
|
"next": ">=16.1",
|
|
93
|
+
"nextra": ">=4.6",
|
|
82
94
|
"nextra-theme-docs": ">=4.6",
|
|
83
95
|
"react": ">=19.2.3"
|
|
84
96
|
},
|