docs-i18n 0.6.3 → 0.7.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/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +27 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -7
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +54 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import * as unified from 'unified';
|
|
2
|
+
import * as mdast from 'mdast';
|
|
3
|
+
import { Root } from 'hast';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* docs-i18n configuration schema.
|
|
3
7
|
* Each project provides a `docs-i18n.config.ts` file.
|
|
@@ -248,6 +252,36 @@ declare function extractTranslatableFields(frontmatterText: string): Record<stri
|
|
|
248
252
|
*/
|
|
249
253
|
declare function reconstructFrontmatter(frontmatterText: string, translatedFields: Record<string, string>): string;
|
|
250
254
|
|
|
255
|
+
interface Heading {
|
|
256
|
+
id: string;
|
|
257
|
+
text: string;
|
|
258
|
+
level: number;
|
|
259
|
+
}
|
|
260
|
+
interface RenderedMarkdown {
|
|
261
|
+
html: string;
|
|
262
|
+
headings: Heading[];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The base unified processor for markdown → HTML rendering.
|
|
266
|
+
*
|
|
267
|
+
* Pipeline: remarkParse → remarkGfm → remarkRehype → rehypeRaw →
|
|
268
|
+
* rehypeCallouts → rehypeSlug → rehypeAutolinkHeadings →
|
|
269
|
+
* rehypeCollectHeadings → rehypeStringify
|
|
270
|
+
*
|
|
271
|
+
* Exported so consumers can extend it with additional plugins via `.use()`.
|
|
272
|
+
*/
|
|
273
|
+
declare const processor: unified.Processor<mdast.Root, mdast.Root, Root, Root, string>;
|
|
274
|
+
/**
|
|
275
|
+
* Synchronous markdown → HTML rendering.
|
|
276
|
+
* Suitable for use inside React `useMemo` or server-side rendering.
|
|
277
|
+
*/
|
|
278
|
+
declare function renderMarkdown(markdown: string): RenderedMarkdown;
|
|
279
|
+
/**
|
|
280
|
+
* Extract headings from an already-rendered HTML string.
|
|
281
|
+
* This is an alternative to the plugin approach — useful when you only have HTML.
|
|
282
|
+
*/
|
|
283
|
+
declare function extractHeadings(html: string): Heading[];
|
|
284
|
+
|
|
251
285
|
/**
|
|
252
286
|
* Preprocess MDX content to ensure JSX tags (<AppOnly>, <PagesOnly>, <details>, <div>)
|
|
253
287
|
* are separated from surrounding content by blank lines.
|
|
@@ -256,4 +290,10 @@ declare function reconstructFrontmatter(frontmatterText: string, translatedField
|
|
|
256
290
|
*/
|
|
257
291
|
declare function normalize(content: string): string;
|
|
258
292
|
|
|
259
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Walk up from the current file to find the docs-i18n package.json and return its version.
|
|
295
|
+
* Falls back to 'unknown' if not found.
|
|
296
|
+
*/
|
|
297
|
+
declare function getPackageVersion(): string;
|
|
298
|
+
|
|
299
|
+
export { type DocsI18nConfig, type Heading, type ProjectConfig, type RenderedMarkdown, TranslationCache, assemble, defineConfig, extractHeadings, extractTranslatableFields, flattenSources, getPackageVersion, loadConfig, normalize, parseMdx, processor, reconstructFrontmatter, renderMarkdown };
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,15 @@ function defineConfig(config) {
|
|
|
10
10
|
}
|
|
11
11
|
async function loadConfig(path2) {
|
|
12
12
|
const configPath = path2 ?? "docs-i18n.config.ts";
|
|
13
|
+
const fullPath = configPath.startsWith("/") ? configPath : `${process.cwd()}/${configPath}`;
|
|
13
14
|
try {
|
|
14
15
|
const mod = await import(
|
|
15
16
|
/* @vite-ignore */
|
|
16
|
-
|
|
17
|
+
fullPath
|
|
17
18
|
);
|
|
18
19
|
return mod.default ?? mod;
|
|
19
20
|
} catch {
|
|
20
|
-
throw new Error(`Cannot load config from ${
|
|
21
|
+
throw new Error(`Cannot load config from ${fullPath}. Create a docs-i18n.config.ts file.`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
function flattenSources(config) {
|
|
@@ -532,14 +533,102 @@ function reconstructFrontmatter(frontmatterText, translatedFields) {
|
|
|
532
533
|
${doc.toString().trimEnd()}
|
|
533
534
|
---`;
|
|
534
535
|
}
|
|
536
|
+
|
|
537
|
+
// src/core/markdown.ts
|
|
538
|
+
import { unified } from "unified";
|
|
539
|
+
import remarkParse from "remark-parse";
|
|
540
|
+
import remarkGfm from "remark-gfm";
|
|
541
|
+
import remarkRehype from "remark-rehype";
|
|
542
|
+
import rehypeRaw from "rehype-raw";
|
|
543
|
+
import rehypeCallouts from "rehype-callouts";
|
|
544
|
+
import rehypeSlug from "rehype-slug";
|
|
545
|
+
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
546
|
+
import rehypeStringify from "rehype-stringify";
|
|
547
|
+
function getTextContent(node) {
|
|
548
|
+
if (node.type === "text") {
|
|
549
|
+
return node.value;
|
|
550
|
+
}
|
|
551
|
+
if ("children" in node) {
|
|
552
|
+
return node.children.map((child) => getTextContent(child)).join("");
|
|
553
|
+
}
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
function rehypeCollectHeadings() {
|
|
557
|
+
return (tree, file) => {
|
|
558
|
+
const headings = [];
|
|
559
|
+
function visit(node) {
|
|
560
|
+
if (node.type === "element") {
|
|
561
|
+
const match = node.tagName.match(/^h([1-6])$/);
|
|
562
|
+
if (match) {
|
|
563
|
+
const level = Number.parseInt(match[1], 10);
|
|
564
|
+
const id = node.properties?.id ?? "";
|
|
565
|
+
const text = getTextContent(node);
|
|
566
|
+
headings.push({ id, text, level });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if ("children" in node) {
|
|
570
|
+
for (const child of node.children) {
|
|
571
|
+
if (child.type === "element") {
|
|
572
|
+
visit(child);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
visit(tree);
|
|
578
|
+
file.data.headings = headings;
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
var processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeCallouts).use(rehypeSlug).use(rehypeAutolinkHeadings).use(rehypeCollectHeadings).use(rehypeStringify);
|
|
582
|
+
function renderMarkdown(markdown) {
|
|
583
|
+
const file = processor.processSync(markdown);
|
|
584
|
+
const headings = file.data.headings ?? [];
|
|
585
|
+
const html = String(file);
|
|
586
|
+
return { html, headings };
|
|
587
|
+
}
|
|
588
|
+
function extractHeadings(html) {
|
|
589
|
+
const headings = [];
|
|
590
|
+
const regex = /<h([1-6])(?:\s[^>]*?\bid="([^"]*)"[^>]*?)?>([\s\S]*?)<\/h\1>/gi;
|
|
591
|
+
let match;
|
|
592
|
+
while ((match = regex.exec(html)) !== null) {
|
|
593
|
+
const level = Number.parseInt(match[1], 10);
|
|
594
|
+
const id = match[2] ?? "";
|
|
595
|
+
const text = match[3].replace(/<[^>]+>/g, "").trim();
|
|
596
|
+
headings.push({ id, text, level });
|
|
597
|
+
}
|
|
598
|
+
return headings;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/core/version.ts
|
|
602
|
+
import { readFileSync } from "fs";
|
|
603
|
+
import { resolve, dirname } from "path";
|
|
604
|
+
import { fileURLToPath } from "url";
|
|
605
|
+
function getPackageVersion() {
|
|
606
|
+
try {
|
|
607
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
608
|
+
for (let i = 0; i < 5; i++) {
|
|
609
|
+
try {
|
|
610
|
+
const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf-8"));
|
|
611
|
+
if (pkg.name === "docs-i18n") return pkg.version;
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
dir = dirname(dir);
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
return "unknown";
|
|
619
|
+
}
|
|
535
620
|
export {
|
|
536
621
|
TranslationCache,
|
|
537
622
|
assemble,
|
|
538
623
|
defineConfig,
|
|
624
|
+
extractHeadings,
|
|
539
625
|
extractTranslatableFields,
|
|
540
626
|
flattenSources,
|
|
627
|
+
getPackageVersion,
|
|
541
628
|
loadConfig,
|
|
542
629
|
normalize,
|
|
543
630
|
parseMdx,
|
|
544
|
-
|
|
631
|
+
processor,
|
|
632
|
+
reconstructFrontmatter,
|
|
633
|
+
renderMarkdown
|
|
545
634
|
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
init_parser,
|
|
3
3
|
parseMdx
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JHBSHTXC.js";
|
|
5
5
|
import {
|
|
6
6
|
TranslationCache
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QKIR7RKQ.js";
|
|
8
8
|
import {
|
|
9
9
|
flattenSources
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-TRURQFP4.js";
|
|
11
|
+
import "./chunk-L64GJ4OB.js";
|
|
12
|
+
import "./chunk-PNKVD2UK.js";
|
|
11
13
|
|
|
12
14
|
// src/commands/rescan.ts
|
|
13
15
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TranslationCache
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QKIR7RKQ.js";
|
|
4
4
|
import {
|
|
5
5
|
flattenSources
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TRURQFP4.js";
|
|
7
|
+
import "./chunk-L64GJ4OB.js";
|
|
8
|
+
import "./chunk-PNKVD2UK.js";
|
|
7
9
|
|
|
8
10
|
// src/commands/status.ts
|
|
9
11
|
import { resolve } from "path";
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-CLYUAWZE.js";
|
|
2
2
|
import {
|
|
3
3
|
FRONTMATTER_TRANSLATABLE_FIELDS,
|
|
4
4
|
init_parser
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JHBSHTXC.js";
|
|
6
6
|
import {
|
|
7
7
|
TranslationCache
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QKIR7RKQ.js";
|
|
9
|
+
import {
|
|
10
|
+
flattenSources
|
|
11
|
+
} from "./chunk-TRURQFP4.js";
|
|
12
|
+
import "./chunk-L64GJ4OB.js";
|
|
9
13
|
import {
|
|
10
14
|
__esm,
|
|
11
15
|
__export,
|
|
12
|
-
__toCommonJS
|
|
13
|
-
|
|
14
|
-
} from "./chunk-SKKZIV3L.js";
|
|
16
|
+
__toCommonJS
|
|
17
|
+
} from "./chunk-PNKVD2UK.js";
|
|
15
18
|
|
|
16
19
|
// src/core/frontmatter.ts
|
|
17
20
|
var frontmatter_exports = {};
|
|
@@ -262,7 +265,6 @@ function buildJsonUserMessage(uncached, nodeTypes) {
|
|
|
262
265
|
const type = nodeTypes[md5] ?? "paragraph";
|
|
263
266
|
if (type === "frontmatter") {
|
|
264
267
|
const fields = extractTranslatableFields2(text);
|
|
265
|
-
if (Object.keys(fields).length === 0) continue;
|
|
266
268
|
const fieldKeys = {};
|
|
267
269
|
for (const [field, value] of Object.entries(fields)) {
|
|
268
270
|
const virtualKey = `fm:${md5}:${field}`;
|
|
@@ -477,6 +479,11 @@ ${choice.message.content}
|
|
|
477
479
|
log(
|
|
478
480
|
`\u2705 Frontmatter ${fmMd5.substring(0, 8)}: ${Object.keys(fields).join(", ")}`
|
|
479
481
|
);
|
|
482
|
+
} else if (Object.keys(info.fieldKeys).length === 0) {
|
|
483
|
+
translations[fmMd5] = info.source;
|
|
484
|
+
log(
|
|
485
|
+
`\u2705 Frontmatter ${fmMd5.substring(0, 8)}: no translatable fields, cached as-is`
|
|
486
|
+
);
|
|
480
487
|
}
|
|
481
488
|
if (!allFound) {
|
|
482
489
|
const vKeys = new Set(Object.values(info.fieldKeys));
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openDatabase
|
|
3
|
+
} from "./chunk-L64GJ4OB.js";
|
|
4
|
+
import "./chunk-PNKVD2UK.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/upload.ts
|
|
7
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
8
|
+
import { resolve, relative, join } from "path";
|
|
9
|
+
function collectContentFiles(projectRoot) {
|
|
10
|
+
const contentDir = resolve(projectRoot, "content");
|
|
11
|
+
if (!existsSync(contentDir)) {
|
|
12
|
+
throw new Error(`Content directory not found: ${contentDir}`);
|
|
13
|
+
}
|
|
14
|
+
const rows = [];
|
|
15
|
+
walkDir(contentDir, contentDir, rows);
|
|
16
|
+
return rows;
|
|
17
|
+
}
|
|
18
|
+
function walkDir(dir, contentRoot, rows) {
|
|
19
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
23
|
+
walkDir(fullPath, contentRoot, rows);
|
|
24
|
+
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".json"))) {
|
|
25
|
+
const relativePath = relative(contentRoot, fullPath);
|
|
26
|
+
const body = readFileSync(fullPath, "utf-8");
|
|
27
|
+
const parts = relativePath.split("/");
|
|
28
|
+
const { project, version, lang } = parseContentPath(parts);
|
|
29
|
+
rows.push({
|
|
30
|
+
path: relativePath,
|
|
31
|
+
body,
|
|
32
|
+
project,
|
|
33
|
+
version,
|
|
34
|
+
lang,
|
|
35
|
+
updated_at: Math.floor(Date.now() / 1e3)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parseContentPath(parts) {
|
|
41
|
+
if (parts.length === 2) {
|
|
42
|
+
return { project: "default", version: "latest", lang: parts[0] };
|
|
43
|
+
}
|
|
44
|
+
if (parts.length === 3) {
|
|
45
|
+
if (isLangCode(parts[0])) {
|
|
46
|
+
return { project: "default", version: "latest", lang: parts[0] };
|
|
47
|
+
}
|
|
48
|
+
return { project: parts[0], version: "latest", lang: parts[1] };
|
|
49
|
+
}
|
|
50
|
+
if (parts.length >= 4) {
|
|
51
|
+
return { project: parts[0], version: parts[1], lang: parts[2] };
|
|
52
|
+
}
|
|
53
|
+
return { project: "default", version: "latest", lang: "en" };
|
|
54
|
+
}
|
|
55
|
+
function isLangCode(s) {
|
|
56
|
+
return /^[a-z]{2}(-[a-z]{2,})?$/.test(s);
|
|
57
|
+
}
|
|
58
|
+
function generateContentSql(rows) {
|
|
59
|
+
const statements = [];
|
|
60
|
+
statements.push(
|
|
61
|
+
`CREATE TABLE IF NOT EXISTS content (
|
|
62
|
+
path TEXT PRIMARY KEY NOT NULL,
|
|
63
|
+
body TEXT NOT NULL,
|
|
64
|
+
project TEXT NOT NULL,
|
|
65
|
+
version TEXT NOT NULL,
|
|
66
|
+
lang TEXT NOT NULL,
|
|
67
|
+
updated_at INTEGER
|
|
68
|
+
);`
|
|
69
|
+
);
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const escapedBody = row.body.replace(/'/g, "''");
|
|
72
|
+
const escapedPath = row.path.replace(/'/g, "''");
|
|
73
|
+
statements.push(
|
|
74
|
+
`INSERT OR REPLACE INTO content (path, body, project, version, lang, updated_at) VALUES ('${escapedPath}', '${escapedBody}', '${row.project}', '${row.version}', '${row.lang}', ${row.updated_at});`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return statements;
|
|
78
|
+
}
|
|
79
|
+
function collectTranslations(projectRoot) {
|
|
80
|
+
const dbPath = resolve(projectRoot, ".docs-i18n", "translations.db");
|
|
81
|
+
if (!existsSync(dbPath)) {
|
|
82
|
+
return { sources: [], translations: [] };
|
|
83
|
+
}
|
|
84
|
+
const db = openDatabase(dbPath);
|
|
85
|
+
try {
|
|
86
|
+
const sources = db.prepare("SELECT key, text, type FROM sources").all();
|
|
87
|
+
const translations = db.prepare("SELECT lang, key, value FROM translations").all();
|
|
88
|
+
return { sources, translations };
|
|
89
|
+
} finally {
|
|
90
|
+
db.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function generateTranslationSql(data) {
|
|
94
|
+
const statements = [];
|
|
95
|
+
statements.push(
|
|
96
|
+
`CREATE TABLE IF NOT EXISTS sources (
|
|
97
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
98
|
+
text TEXT NOT NULL,
|
|
99
|
+
type TEXT NOT NULL DEFAULT 'paragraph'
|
|
100
|
+
);`
|
|
101
|
+
);
|
|
102
|
+
statements.push(
|
|
103
|
+
`CREATE TABLE IF NOT EXISTS translations (
|
|
104
|
+
lang TEXT NOT NULL,
|
|
105
|
+
key TEXT NOT NULL,
|
|
106
|
+
value TEXT NOT NULL,
|
|
107
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
108
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
109
|
+
PRIMARY KEY (lang, key)
|
|
110
|
+
);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_translations_lang ON translations(lang);`
|
|
112
|
+
);
|
|
113
|
+
for (const s of data.sources) {
|
|
114
|
+
const escapedText = s.text.replace(/'/g, "''");
|
|
115
|
+
statements.push(
|
|
116
|
+
`INSERT OR REPLACE INTO sources (key, text, type) VALUES ('${s.key}', '${escapedText}', '${s.type}');`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
for (const t of data.translations) {
|
|
120
|
+
const escapedValue = t.value.replace(/'/g, "''");
|
|
121
|
+
statements.push(
|
|
122
|
+
`INSERT INTO translations (lang, key, value) VALUES ('${t.lang}', '${t.key}', '${escapedValue}') ON CONFLICT(lang, key) DO UPDATE SET value = excluded.value, updated_at = unixepoch();`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return statements;
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
collectContentFiles,
|
|
129
|
+
collectTranslations,
|
|
130
|
+
generateContentSql,
|
|
131
|
+
generateTranslationSql
|
|
132
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docs-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Universal documentation translation engine — parse, translate, cache, assemble, manage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"
|
|
17
|
+
"admin",
|
|
18
|
+
"template",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
"dev": "tsup --watch",
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"typecheck": "tsc --noEmit",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
26
|
+
"prepack": "rm -rf admin template && cp -r ../admin admin && cp -r ../template template && rm -rf admin/node_modules template/node_modules",
|
|
27
|
+
"postpack": "rm -rf admin template",
|
|
27
28
|
"prepublishOnly": "rm -rf dist && tsup && chmod +x dist/cli.js"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
@@ -31,25 +32,26 @@
|
|
|
31
32
|
"glob": "^11.0.2",
|
|
32
33
|
"hono": "^4.12.0",
|
|
33
34
|
"openai": "^5.1.1",
|
|
35
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
36
|
+
"rehype-callouts": "^2.0.0",
|
|
37
|
+
"rehype-raw": "^7.0.0",
|
|
38
|
+
"rehype-slug": "^6.0.0",
|
|
39
|
+
"rehype-stringify": "^10.0.1",
|
|
34
40
|
"remark": "^15.0.1",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"@tanstack/react-query": "^5.0.0"
|
|
41
|
+
"remark-gfm": "^4.0.1",
|
|
42
|
+
"remark-parse": "^11.0.0",
|
|
43
|
+
"remark-rehype": "^11.1.2",
|
|
44
|
+
"unified": "^11.0.5",
|
|
45
|
+
"yaml": "^2.8.2"
|
|
41
46
|
},
|
|
42
47
|
"devDependencies": {
|
|
43
48
|
"@biomejs/biome": "^2.0.0",
|
|
44
49
|
"@types/better-sqlite3": "^7.6.13",
|
|
45
|
-
"@types/
|
|
50
|
+
"@types/hast": "^3.0.0",
|
|
46
51
|
"@types/node": "^22.0.0",
|
|
47
|
-
"@types/react": "^19.2.14",
|
|
48
|
-
"@types/react-dom": "^19.2.3",
|
|
49
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
50
52
|
"tsup": "^8.5.0",
|
|
51
53
|
"typescript": "^5.8.0",
|
|
52
|
-
"
|
|
54
|
+
"vfile": "^6.0.0",
|
|
53
55
|
"vitest": "^3.2.0"
|
|
54
56
|
},
|
|
55
57
|
"publishConfig": {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog article component.
|
|
3
|
+
*
|
|
4
|
+
* Renders a single blog post with:
|
|
5
|
+
* - Author and date byline prepended to markdown
|
|
6
|
+
* - Full markdown content with TOC when headings > 1
|
|
7
|
+
* - Breadcrumb navigation back to blog list
|
|
8
|
+
* - Fallback banner when showing untranslated content
|
|
9
|
+
*
|
|
10
|
+
* Layout matches tanstack.com blog article page.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as React from 'react'
|
|
14
|
+
import { Link } from '@tanstack/react-router'
|
|
15
|
+
import { renderMarkdown } from '~/utils/markdown'
|
|
16
|
+
import { format } from '~/utils/dates'
|
|
17
|
+
import { formatAuthors } from '~/utils/blog'
|
|
18
|
+
import { MarkdownContent } from '~/components/markdown'
|
|
19
|
+
import { Toc } from '~/components/Toc'
|
|
20
|
+
import { Breadcrumbs } from '~/components/Breadcrumbs'
|
|
21
|
+
import { FallbackBanner } from '~/components/FallbackBanner'
|
|
22
|
+
import type { LoadedBlogPost } from '~/utils/blog'
|
|
23
|
+
|
|
24
|
+
type BlogArticleProps = {
|
|
25
|
+
post: LoadedBlogPost
|
|
26
|
+
lang: string
|
|
27
|
+
/** Locale display name for fallback banner */
|
|
28
|
+
locale?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function BlogArticle({ post, lang, locale }: BlogArticleProps) {
|
|
32
|
+
const { title, content, authors, published, filePath, isFallback } = post
|
|
33
|
+
|
|
34
|
+
// Prepend byline to content (matches tanstack.com pattern)
|
|
35
|
+
const blogContent = `<small>_by ${formatAuthors(authors)} on ${format(
|
|
36
|
+
new Date(published || 0),
|
|
37
|
+
'MMMM d, yyyy',
|
|
38
|
+
)}._</small>
|
|
39
|
+
|
|
40
|
+
${content}`
|
|
41
|
+
|
|
42
|
+
const { headings, markup } = React.useMemo(
|
|
43
|
+
() => renderMarkdown(blogContent),
|
|
44
|
+
[blogContent],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const isTocVisible = headings.length > 1
|
|
48
|
+
|
|
49
|
+
const markdownContainerRef = React.useRef<HTMLDivElement>(null)
|
|
50
|
+
const [activeHeadings, setActiveHeadings] = React.useState<Array<string>>([])
|
|
51
|
+
|
|
52
|
+
const headingElementRefs = React.useRef<
|
|
53
|
+
Record<string, IntersectionObserverEntry>
|
|
54
|
+
>({})
|
|
55
|
+
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const callback = (headingsList: Array<IntersectionObserverEntry>) => {
|
|
58
|
+
headingElementRefs.current = headingsList.reduce(
|
|
59
|
+
(map, headingElement) => {
|
|
60
|
+
map[headingElement.target.id] = headingElement
|
|
61
|
+
return map
|
|
62
|
+
},
|
|
63
|
+
headingElementRefs.current,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const visibleHeadings: Array<IntersectionObserverEntry> = []
|
|
67
|
+
Object.keys(headingElementRefs.current).forEach((key) => {
|
|
68
|
+
const headingElement = headingElementRefs.current[key]
|
|
69
|
+
if (headingElement.isIntersecting) {
|
|
70
|
+
visibleHeadings.push(headingElement)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if (visibleHeadings.length >= 1) {
|
|
75
|
+
setActiveHeadings(visibleHeadings.map((h) => h.target.id))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const observer = new IntersectionObserver(callback, {
|
|
80
|
+
rootMargin: '0px',
|
|
81
|
+
threshold: 0.2,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const headingElements = Array.from(
|
|
85
|
+
markdownContainerRef.current?.querySelectorAll(
|
|
86
|
+
'h2[id], h3[id], h4[id], h5[id], h6[id]',
|
|
87
|
+
) ?? [],
|
|
88
|
+
)
|
|
89
|
+
headingElements.forEach((el) => observer.observe(el))
|
|
90
|
+
|
|
91
|
+
return () => observer.disconnect()
|
|
92
|
+
}, [headings])
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={`
|
|
97
|
+
min-h-[calc(100dvh-var(--navbar-height))]
|
|
98
|
+
flex flex-col
|
|
99
|
+
w-full transition-all duration-300`}
|
|
100
|
+
>
|
|
101
|
+
<div className="flex flex-col max-w-full min-w-0 w-full min-h-0 relative mb-8">
|
|
102
|
+
<div className="min-w-0 flex justify-center w-full min-h-[88dvh] lg:min-h-0 mx-auto">
|
|
103
|
+
<div className="flex-1 flex flex-col w-full min-w-0">
|
|
104
|
+
<div className="px-4 pt-4 lg:pt-6">
|
|
105
|
+
<div className="w-full max-w-[1100px] mx-auto">
|
|
106
|
+
{isFallback && locale && <FallbackBanner locale={locale} />}
|
|
107
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
108
|
+
<div className="w-full flex justify-center">
|
|
109
|
+
<div
|
|
110
|
+
className={[
|
|
111
|
+
'w-full p-2 lg:p-4 xl:p-6',
|
|
112
|
+
isTocVisible ? 'max-w-full' : 'max-w-[768px]',
|
|
113
|
+
].join(' ')}
|
|
114
|
+
>
|
|
115
|
+
<Breadcrumbs
|
|
116
|
+
section="Blog"
|
|
117
|
+
sectionTo={`/${lang}/blog`}
|
|
118
|
+
headings={isTocVisible ? headings : undefined}
|
|
119
|
+
tocHiddenBreakpoint="lg"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
{isTocVisible && (
|
|
123
|
+
<div className="pl-4 w-32 lg:w-36 xl:w-44 2xl:w-56 3xl:w-64 shrink-0 hidden lg:block" />
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
<div
|
|
127
|
+
className={[
|
|
128
|
+
'w-full flex justify-center mx-auto',
|
|
129
|
+
isTocVisible ? 'max-w-full' : 'max-w-[768px]',
|
|
130
|
+
].join(' ')}
|
|
131
|
+
>
|
|
132
|
+
<div className="flex overflow-auto flex-col w-full p-2 lg:p-4 xl:p-6 pt-0">
|
|
133
|
+
<MarkdownContent
|
|
134
|
+
title={title}
|
|
135
|
+
htmlMarkup={markup}
|
|
136
|
+
repo=""
|
|
137
|
+
branch=""
|
|
138
|
+
filePath={filePath}
|
|
139
|
+
containerRef={markdownContainerRef}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
{isTocVisible && (
|
|
143
|
+
<div className="pl-4 w-32 lg:w-36 xl:w-44 2xl:w-56 3xl:w-64 shrink-0 hidden lg:block py-4 transition-all">
|
|
144
|
+
<Toc
|
|
145
|
+
headings={headings}
|
|
146
|
+
activeHeadings={activeHeadings}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|