@tayacrystals/lore 0.1.3 → 1.0.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.
Files changed (49) hide show
  1. package/README.md +7 -0
  2. package/package.json +48 -36
  3. package/src/build.ts +148 -0
  4. package/src/config.ts +26 -0
  5. package/src/dev.ts +112 -0
  6. package/src/files.ts +193 -0
  7. package/src/i18n.ts +49 -0
  8. package/src/icons.ts +59 -0
  9. package/src/index.ts +28 -0
  10. package/src/mdx.ts +281 -0
  11. package/src/parse.ts +53 -0
  12. package/src/routing.ts +46 -0
  13. package/src/serve.ts +72 -0
  14. package/src/template.ts +747 -0
  15. package/src/types.ts +51 -0
  16. package/src/version.ts +33 -0
  17. package/components/docs/Breadcrumbs.astro +0 -41
  18. package/components/docs/PrevNext.astro +0 -50
  19. package/components/docs/Sidebar.astro +0 -28
  20. package/components/docs/SidebarGroup.astro +0 -72
  21. package/components/docs/SidebarItem.astro +0 -26
  22. package/components/docs/TableOfContents.astro +0 -82
  23. package/components/global/SearchModal.astro +0 -159
  24. package/components/mdx/Accordion.astro +0 -20
  25. package/components/mdx/Callout.astro +0 -53
  26. package/components/mdx/Card.astro +0 -26
  27. package/components/mdx/CardGrid.astro +0 -16
  28. package/components/mdx/CodeTabs.astro +0 -129
  29. package/components/mdx/FileTree.astro +0 -117
  30. package/components/mdx/Step.astro +0 -18
  31. package/components/mdx/Steps.astro +0 -6
  32. package/components/mdx/Tab.astro +0 -11
  33. package/components/mdx/Tabs.astro +0 -73
  34. package/components.ts +0 -11
  35. package/config.ts +0 -42
  36. package/index.ts +0 -2
  37. package/integration.ts +0 -68
  38. package/layouts/DocsLayout.astro +0 -277
  39. package/loaders.ts +0 -5
  40. package/routes/docs.astro +0 -211
  41. package/schema.ts +0 -13
  42. package/styles/global.css +0 -78
  43. package/styles/prose.css +0 -148
  44. package/utils/navigation.ts +0 -35
  45. package/utils/rehype-file-tree.ts +0 -229
  46. package/utils/sidebar.ts +0 -107
  47. package/utils/toc.ts +0 -28
  48. package/virtual.d.ts +0 -9
  49. package/vite-plugin.ts +0 -28
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # lore-2
2
+
3
+ Simple documentation site generator
4
+
5
+ - static site
6
+ - js is optional
7
+ - fast
package/package.json CHANGED
@@ -1,51 +1,63 @@
1
1
  {
2
2
  "name": "@tayacrystals/lore",
3
- "version": "0.1.3",
3
+ "version": "1.0.0",
4
+ "description": "Simple documentation site generator with versioning and internationalization support",
4
5
  "type": "module",
6
+ "bin": {
7
+ "lore": "src/index.ts"
8
+ },
5
9
  "exports": {
6
- ".": "./index.ts",
7
- "./schema": "./schema.ts",
8
- "./loaders": "./loaders.ts",
9
- "./components": "./components.ts",
10
- "./styles/*": "./styles/*"
10
+ ".": "./src/index.ts"
11
11
  },
12
12
  "files": [
13
- "*.ts",
14
- "*.d.ts",
15
- "components/",
16
- "layouts/",
17
- "routes/",
18
- "styles/",
19
- "utils/",
20
- "!**/*.test.ts"
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "build": "bun run src/index.ts build",
17
+ "dev": "bun run src/index.ts dev",
18
+ "serve": "bun run src/index.ts serve"
19
+ },
20
+ "keywords": [
21
+ "documentation",
22
+ "static-site-generator",
23
+ "docs",
24
+ "markdown",
25
+ "mdx",
26
+ "cli"
21
27
  ],
22
28
  "publishConfig": {
23
- "access": "public"
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tayacrystals/lore.git"
36
+ },
37
+ "homepage": "https://github.com/tayacrystals/lore#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/tayacrystals/lore/issues"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest",
43
+ "@types/js-yaml": "^4.0.9"
24
44
  },
25
45
  "peerDependencies": {
26
- "astro": "^5.0.0"
46
+ "typescript": "^5"
27
47
  },
28
48
  "dependencies": {
29
- "@astrojs/mdx": "^4.3.13",
30
- "@astrojs/sitemap": "^3.7.0",
31
- "@fontsource-variable/inter": "^5.2.8",
32
- "@fontsource/geist-mono": "^5.2.7",
33
- "@iconify-json/lucide": "^1.2.89",
34
- "@tailwindcss/vite": "^4.1.18",
35
- "astro-expressive-code": "^0.41.6",
36
- "astro-icon": "^1.1.5",
37
- "astro-pagefind": "^1.8.5",
38
- "hast-util-to-html": "^9.0.5",
39
- "hastscript": "^9.0.1",
40
- "rehype": "^13.0.2",
41
- "tailwindcss": "^4.1.18",
42
- "unist-util-visit": "^5.1.0",
43
- "zod": "^3.24.0"
49
+ "js-yaml": "^4.1.1",
50
+ "lucide": "^0.577.0",
51
+ "rehype-autolink-headings": "^7.1.0",
52
+ "rehype-slug": "^6.0.0",
53
+ "rehype-stringify": "^10.0.1",
54
+ "remark-gfm": "^4.0.1",
55
+ "remark-parse": "^11.0.0",
56
+ "remark-rehype": "^11.1.2",
57
+ "shiki": "^4.0.2",
58
+ "unified": "^11.0.5"
44
59
  },
45
- "devDependencies": {
46
- "@astrojs/check": "^0.9.6",
47
- "@types/hast": "^3.0.4",
48
- "typescript": "^5.9.3",
49
- "vite": "6"
60
+ "engines": {
61
+ "bun": ">=1.0.0"
50
62
  }
51
63
  }
package/src/build.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { renderMdx } from "./mdx.ts";
4
+ import { loadConfig } from "./config.ts";
5
+ import { buildDocTree } from "./files.ts";
6
+ import { renderPage } from "./template.ts";
7
+ import { detectVersions, getDefaultVersion } from "./version.ts";
8
+ import { detectLocales, getDefaultLocale } from "./i18n.ts";
9
+
10
+ async function findLogo(docsDir: string, outDir: string): Promise<string | undefined> {
11
+ for (const name of ["logo.svg", "logo.png"]) {
12
+ const src = Bun.file(`${docsDir}/${name}`);
13
+ if (await src.exists()) {
14
+ await Bun.write(`${outDir}/${name}`, src);
15
+ return `/${name}`;
16
+ }
17
+ }
18
+ }
19
+
20
+ export async function build(
21
+ docsDir: string,
22
+ outDir: string,
23
+ opts: { devMode?: boolean } = {}
24
+ ): Promise<void> {
25
+ console.log(`Building docs from ${docsDir} → ${outDir}`);
26
+
27
+ // Clear build directory first to avoid merging old and new artifacts
28
+ try {
29
+ await rm(outDir, { recursive: true, force: true });
30
+ } catch {
31
+ // Directory doesn't exist, which is fine
32
+ }
33
+
34
+ const config = await loadConfig(docsDir);
35
+ const logoSrc = await findLogo(docsDir, outDir);
36
+
37
+ const hasVersioning = config.versioning;
38
+ const hasI18n = config.internationalization;
39
+
40
+ const builds: Array<{ version?: string; locale?: string }> = [];
41
+
42
+ if (hasI18n) {
43
+ const locales = await detectLocales(docsDir);
44
+ for (const locale of locales) {
45
+ if (hasVersioning) {
46
+ const localeDir = path.join(docsDir, locale.code);
47
+ const versions = await detectVersions(localeDir);
48
+ for (const version of versions) {
49
+ builds.push({ locale: locale.code, version: version.name });
50
+ }
51
+ } else {
52
+ builds.push({ locale: locale.code });
53
+ }
54
+ }
55
+ } else if (hasVersioning) {
56
+ const versions = await detectVersions(docsDir);
57
+ for (const version of versions) {
58
+ builds.push({ version: version.name });
59
+ }
60
+ }
61
+
62
+ if (builds.length === 0) {
63
+ builds.push({});
64
+ }
65
+
66
+ let totalPages = 0;
67
+
68
+ for (const buildOpts of builds) {
69
+ const { pages, sidebar, versions, locales, currentVersion, currentLocale } =
70
+ await buildDocTree(docsDir, buildOpts);
71
+
72
+ const buildLabel = [
73
+ currentLocale,
74
+ currentVersion,
75
+ ].filter(Boolean).join("/") || "default";
76
+
77
+ console.log(`Building for ${buildLabel}: found ${pages.length} pages`);
78
+
79
+ for (const page of pages) {
80
+ const contentHtml = await renderMdx(page.content, {
81
+ locale: currentLocale,
82
+ version: currentVersion,
83
+ });
84
+
85
+ const html = renderPage({
86
+ config,
87
+ title: page.title,
88
+ description: page.description,
89
+ contentHtml,
90
+ sidebar,
91
+ currentUrl: page.url,
92
+ logoSrc,
93
+ devMode: opts.devMode,
94
+ versions,
95
+ locales,
96
+ currentVersion,
97
+ currentLocale,
98
+ translationOf: page.context.translationOf,
99
+ });
100
+
101
+ const urlParts: string[] = [];
102
+ if (currentLocale) urlParts.push(currentLocale);
103
+ if (currentVersion) urlParts.push(currentVersion);
104
+
105
+ const urlSegments = page.url.split("/").filter(Boolean);
106
+
107
+ let outputPath: string;
108
+ if (urlSegments.length === 0) {
109
+ outputPath = path.join(outDir, ...urlParts, "index.html");
110
+ } else {
111
+ outputPath = path.join(outDir, ...urlParts, ...urlSegments, "index.html");
112
+ }
113
+
114
+ await mkdir(path.dirname(outputPath), { recursive: true });
115
+ await Bun.write(outputPath, html);
116
+ console.log(` ${page.url} → ${path.relative(process.cwd(), outputPath)}`);
117
+ totalPages++;
118
+ }
119
+ }
120
+
121
+ const defaultLocale = config.internationalization
122
+ ? config.defaultLocale ?? "en"
123
+ : undefined;
124
+ const defaultVersion = config.versioning
125
+ ? config.defaultVersion ?? "latest"
126
+ : undefined;
127
+
128
+ const redirectPathParts: string[] = [];
129
+ if (defaultLocale) redirectPathParts.push(defaultLocale);
130
+ if (defaultVersion) redirectPathParts.push(defaultVersion);
131
+
132
+ if (redirectPathParts.length > 0 && totalPages > 0) {
133
+ const redirectPath = "/" + redirectPathParts.join("/") + "/";
134
+ const redirectHtml = `<!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <meta http-equiv="refresh" content="0; url=${redirectPath}">
138
+ <script>window.location.href = "${redirectPath}";</script>
139
+ </head>
140
+ <body></body>
141
+ </html>`;
142
+ const outputPath = path.join(outDir, "index.html");
143
+ await Bun.write(outputPath, redirectHtml);
144
+ console.log(` / → ${path.relative(process.cwd(), outputPath)} (redirect to ${redirectPath})`);
145
+ }
146
+
147
+ console.log(`\nDone! Built ${totalPages} pages to ${outDir}`);
148
+ }
package/src/config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import yaml from "js-yaml";
2
+ import type { Config } from "./types.ts";
3
+
4
+ export async function loadConfig(docsDir: string): Promise<Config> {
5
+ const file = Bun.file(`${docsDir}/lore.yml`);
6
+ if (!(await file.exists())) return {};
7
+ const text = await file.text();
8
+ return (yaml.load(text) as Config) ?? {};
9
+ }
10
+
11
+ // WCAG AA compliant colors (≥4.5:1 contrast on white)
12
+ const NAMED_COLORS: Record<string, string> = {
13
+ red: "#b91c1c",
14
+ orange: "#c2410c",
15
+ yellow: "#a16207",
16
+ green: "#15803d",
17
+ blue: "#1d4ed8",
18
+ purple: "#6d28d9",
19
+ pink: "#be185d",
20
+ gray: "#374151",
21
+ };
22
+
23
+ export function resolveColor(color?: string): string {
24
+ if (!color) return "#1d4ed8";
25
+ return NAMED_COLORS[color] ?? color;
26
+ }
package/src/dev.ts ADDED
@@ -0,0 +1,112 @@
1
+ import path from "node:path";
2
+ import { watch } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { build } from "./build.ts";
5
+ import { loadConfig } from "./config.ts";
6
+ import { getDefaultVersion } from "./version.ts";
7
+ import { getDefaultLocale } from "./i18n.ts";
8
+
9
+ export async function dev(docsDir: string, outDir: string): Promise<void> {
10
+ const port = 3000;
11
+
12
+ // Load config for redirect logic
13
+ const config = await loadConfig(docsDir);
14
+
15
+ // Initial build
16
+ await build(docsDir, outDir, { devMode: true });
17
+
18
+ // SSE clients waiting for reload signals
19
+ const sseClients = new Set<ReadableStreamDefaultController<string>>();
20
+
21
+ function notifyReload() {
22
+ for (const ctrl of sseClients) {
23
+ try {
24
+ ctrl.enqueue("data: reload\n\n");
25
+ } catch {
26
+ sseClients.delete(ctrl);
27
+ }
28
+ }
29
+ }
30
+
31
+ // Watch docs dir and rebuild on changes
32
+ let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
33
+ watch(docsDir, { recursive: true }, (_event, filename) => {
34
+ if (rebuildTimer) clearTimeout(rebuildTimer);
35
+ rebuildTimer = setTimeout(async () => {
36
+ console.log(`\n${filename} changed — rebuilding...`);
37
+ try {
38
+ await build(docsDir, outDir, { devMode: true });
39
+ notifyReload();
40
+ } catch (err) {
41
+ console.error("Build error:", err);
42
+ }
43
+ }, 100);
44
+ });
45
+
46
+ Bun.serve({
47
+ port,
48
+ idleTimeout: 0,
49
+ async fetch(req) {
50
+ const url = new URL(req.url);
51
+
52
+ // Redirect from root to default version/locale
53
+ if (url.pathname === "/") {
54
+ const defaultLocale = config.internationalization
55
+ ? getDefaultLocale(config)
56
+ : undefined;
57
+ const defaultVersion = config.versioning
58
+ ? getDefaultVersion(config)
59
+ : undefined;
60
+
61
+ const redirectPathParts: string[] = [];
62
+ if (defaultLocale) redirectPathParts.push(defaultLocale);
63
+ if (defaultVersion) redirectPathParts.push(defaultVersion);
64
+
65
+ if (redirectPathParts.length > 0) {
66
+ const redirectPath = "/" + redirectPathParts.join("/") + "/";
67
+ return Response.redirect(url.origin + redirectPath, 302);
68
+ }
69
+ }
70
+
71
+ // SSE endpoint for live reload
72
+ if (url.pathname === "/_lore/reload") {
73
+ let ctrl!: ReadableStreamDefaultController<string>;
74
+ const stream = new ReadableStream<string>({
75
+ start(c) {
76
+ ctrl = c;
77
+ sseClients.add(ctrl);
78
+ ctrl.enqueue(": connected\n\n");
79
+ },
80
+ cancel() {
81
+ sseClients.delete(ctrl);
82
+ },
83
+ });
84
+ return new Response(stream, {
85
+ headers: {
86
+ "Content-Type": "text/event-stream",
87
+ "Cache-Control": "no-cache",
88
+ "Connection": "keep-alive",
89
+ "X-Accel-Buffering": "no",
90
+ },
91
+ });
92
+ }
93
+
94
+ // Serve files from outDir
95
+ let filePath = path.join(outDir, url.pathname);
96
+ try {
97
+ const s = await stat(filePath);
98
+ if (s.isDirectory()) filePath = path.join(filePath, "index.html");
99
+ } catch {
100
+ filePath = path.join(outDir, url.pathname, "index.html");
101
+ }
102
+
103
+ const file = Bun.file(filePath);
104
+ if (await file.exists()) return new Response(file);
105
+
106
+ return new Response("Not found", { status: 404 });
107
+ },
108
+ });
109
+
110
+ console.log(`\nDev server running at http://localhost:${port}`);
111
+ console.log("Watching for changes...\n");
112
+ }
package/src/files.ts ADDED
@@ -0,0 +1,193 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parsePage, filenameToTitle } from "./parse.ts";
4
+ import type { PageInfo, SidebarItem, VersionInfo, LocaleInfo } from "./types.ts";
5
+ import { loadConfig } from "./config.ts";
6
+ import { detectVersions, getDefaultVersion } from "./version.ts";
7
+ import { detectLocales, getDefaultLocale } from "./i18n.ts";
8
+
9
+ export interface DocTree {
10
+ pages: PageInfo[];
11
+ sidebar: SidebarItem[];
12
+ versions?: VersionInfo[];
13
+ locales?: LocaleInfo[];
14
+ currentVersion?: string;
15
+ currentLocale?: string;
16
+ }
17
+
18
+ /** Strip numeric prefix: "01-configuration" → "configuration" */
19
+ function stripPrefix(name: string): string {
20
+ return name.replace(/^\d+-/, "");
21
+ }
22
+
23
+ /** Convert a filename (with .mdx) to a URL slug */
24
+ function toSlug(filename: string): string {
25
+ return stripPrefix(filename.replace(/\.mdx$/, ""));
26
+ }
27
+
28
+ /** Build the URL from a list of path parts relative to docs dir */
29
+ function buildUrl(parts: string[]): string {
30
+ const segments = parts.map((p) => toSlug(p));
31
+ const filtered = segments.filter((s) => s !== "index");
32
+ if (filtered.length === 0) return "/";
33
+ return "/" + filtered.join("/");
34
+ }
35
+
36
+ function generateListing(title: string, url: string, items: SidebarItem[]): PageInfo {
37
+ const lines = [`# ${title}`, ""];
38
+ for (const item of items) {
39
+ const href = item.type === "page" ? item.url : (item.url ?? "#");
40
+ lines.push(`- [${item.title}](${href})`);
41
+ }
42
+ return { filePath: url, url, title, content: lines.join("\n"), context: {} };
43
+ }
44
+
45
+ /** Sort entries: index.mdx first, then alphabetically */
46
+ function sortEntries(entries: string[]): string[] {
47
+ return entries.sort((a, b) => {
48
+ const aIsIndex = a === "index.mdx";
49
+ const bIsIndex = b === "index.mdx";
50
+ if (aIsIndex && !bIsIndex) return -1;
51
+ if (!aIsIndex && bIsIndex) return 1;
52
+ return a.localeCompare(b);
53
+ });
54
+ }
55
+
56
+ export async function buildDocTree(
57
+ docsDir: string,
58
+ opts: {
59
+ version?: string;
60
+ locale?: string;
61
+ } = {}
62
+ ): Promise<DocTree> {
63
+ const config = await loadConfig(docsDir);
64
+ const hasVersioning = config.versioning;
65
+ const hasI18n = config.internationalization;
66
+
67
+ let versions: VersionInfo[] = [];
68
+ let locales: LocaleInfo[] = [];
69
+
70
+ if (hasI18n) {
71
+ locales = await detectLocales(docsDir);
72
+ }
73
+
74
+ if (hasVersioning) {
75
+ if (hasI18n && locales.length > 0) {
76
+ const locale = opts.locale ?? getDefaultLocale(config);
77
+ const localeDir = path.join(docsDir, locale);
78
+ versions = await detectVersions(localeDir);
79
+ } else {
80
+ versions = await detectVersions(docsDir);
81
+ }
82
+ }
83
+
84
+ let contentDir = docsDir;
85
+ let currentVersion: string | undefined;
86
+ let currentLocale: string | undefined;
87
+
88
+ if (hasI18n) {
89
+ currentLocale = opts.locale ?? getDefaultLocale(config);
90
+ contentDir = path.join(contentDir, currentLocale);
91
+ }
92
+
93
+ if (hasVersioning) {
94
+ currentVersion = opts.version ?? getDefaultVersion(config);
95
+ contentDir = path.join(contentDir, currentVersion);
96
+ }
97
+
98
+ const pages: PageInfo[] = [];
99
+ const sidebar: SidebarItem[] = [];
100
+
101
+ await walkDir(
102
+ docsDir,
103
+ contentDir,
104
+ [],
105
+ pages,
106
+ sidebar,
107
+ true,
108
+ currentLocale,
109
+ currentVersion
110
+ );
111
+
112
+ return {
113
+ pages,
114
+ sidebar,
115
+ versions: versions.length > 0 ? versions : undefined,
116
+ locales: locales.length > 0 ? locales : undefined,
117
+ currentVersion,
118
+ currentLocale,
119
+ };
120
+ }
121
+
122
+ async function walkDir(
123
+ docsDir: string,
124
+ dir: string,
125
+ pathParts: string[],
126
+ pages: PageInfo[],
127
+ sidebar: SidebarItem[],
128
+ isRoot: boolean,
129
+ currentLocale?: string,
130
+ currentVersion?: string
131
+ ): Promise<void> {
132
+ const entries = sortEntries(await readdir(dir));
133
+
134
+ for (const entry of entries) {
135
+ if (entry === "lore.yml" || entry.startsWith(".")) continue;
136
+
137
+ const fullPath = path.join(dir, entry);
138
+ const s = await stat(fullPath);
139
+
140
+ if (s.isDirectory()) {
141
+ const sectionTitle = filenameToTitle(entry);
142
+ const sectionItems: SidebarItem[] = [];
143
+ const sectionPages: PageInfo[] = [];
144
+
145
+ await walkDir(docsDir, fullPath, [...pathParts, entry], sectionPages, sectionItems, false, currentLocale, currentVersion);
146
+
147
+ const sectionUrl = buildUrl([...pathParts, entry]);
148
+ const hasIndex = sectionPages.some((p) => p.url === sectionUrl);
149
+
150
+ // Auto-generate a directory listing page if there's no explicit index.mdx
151
+ if (!hasIndex) {
152
+ sectionPages.push(generateListing(sectionTitle, sectionUrl, sectionItems));
153
+ }
154
+
155
+ pages.push(...sectionPages);
156
+
157
+ sidebar.push({
158
+ type: "section",
159
+ title: sectionTitle,
160
+ url: sectionUrl,
161
+ items: sectionItems,
162
+ });
163
+ } else if (entry.endsWith(".mdx")) {
164
+ const url = buildUrl([...pathParts, entry]);
165
+ const raw = await Bun.file(fullPath).text();
166
+ const parsed = parsePage(raw);
167
+ const baseName = entry.replace(/\.mdx$/, "");
168
+ const title = parsed.h1Title ?? filenameToTitle(baseName);
169
+
170
+ const page: PageInfo = {
171
+ filePath: fullPath,
172
+ url,
173
+ title,
174
+ description: parsed.description,
175
+ content: parsed.content,
176
+ context: {
177
+ version: currentVersion,
178
+ locale: currentLocale,
179
+ translationOf: parsed.frontmatter["translation-of"] as string | undefined,
180
+ },
181
+ };
182
+ pages.push(page);
183
+
184
+ const isIndex = toSlug(entry) === "index";
185
+ // Root index.mdx goes into sidebar; subdirectory index.mdx is shown
186
+ // via the section header link and not as a separate sidebar item.
187
+ if (!isIndex || isRoot) {
188
+ const sidebarTitle = isRoot && isIndex ? "Home" : title;
189
+ sidebar.push({ type: "page", title: sidebarTitle, url });
190
+ }
191
+ }
192
+ }
193
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { LocaleInfo } from "./types.ts";
4
+ import type { Config } from "./types.ts";
5
+
6
+ export async function detectLocales(docsDir: string): Promise<LocaleInfo[]> {
7
+ const entries = await readdir(docsDir);
8
+ const locales: LocaleInfo[] = [];
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(docsDir, entry);
12
+ const s = await stat(fullPath);
13
+
14
+ if (s.isDirectory() && /^[a-z]{2}(-[a-z]{2})?$/.test(entry)) {
15
+ locales.push({ code: entry });
16
+ }
17
+ }
18
+
19
+ return locales.sort((a, b) => a.code.localeCompare(b.code));
20
+ }
21
+
22
+ export function getDefaultLocale(config: Config): string {
23
+ return config.defaultLocale ?? "en";
24
+ }
25
+
26
+ export function getLocaleLabel(code: string): string {
27
+ const labels: Record<string, string> = {
28
+ en: "English",
29
+ es: "Español",
30
+ fr: "Français",
31
+ de: "Deutsch",
32
+ ja: "日本語",
33
+ zh: "中文",
34
+ ko: "한국어",
35
+ pt: "Português",
36
+ ru: "Русский",
37
+ it: "Italiano",
38
+ nl: "Nederlands",
39
+ pl: "Polski",
40
+ tr: "Türkçe",
41
+ ar: "العربية",
42
+ hi: "हिन्दी",
43
+ vi: "Tiếng Việt",
44
+ th: "ไทย",
45
+ id: "Bahasa Indonesia",
46
+ ms: "Bahasa Melayu",
47
+ };
48
+ return labels[code] ?? code.toUpperCase();
49
+ }