astro-llm-translator 0.1.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.
@@ -0,0 +1,182 @@
1
+ // src/starlight/index.ts
2
+ import fs2 from "fs/promises";
3
+ import path2 from "path";
4
+
5
+ // src/starlight/sidebar-utils.ts
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import GithubSlugger from "github-slugger";
9
+ import matter from "gray-matter";
10
+ function getMetadata(filePath) {
11
+ try {
12
+ const content = fs.readFileSync(filePath, "utf-8");
13
+ const parsed = matter(content);
14
+ return {
15
+ title: parsed.data.title,
16
+ slug: parsed.data.slug,
17
+ sidebar: parsed.data.sidebar || {}
18
+ };
19
+ } catch {
20
+ return { title: void 0, slug: void 0, sidebar: {} };
21
+ }
22
+ }
23
+ function getTranslations(baseContentDir, relPath, targetLocales, sourceLocale) {
24
+ const translations = {};
25
+ for (const lang of targetLocales) {
26
+ const targetFullPath = path.join(baseContentDir, lang, relPath);
27
+ if (fs.existsSync(targetFullPath)) {
28
+ const { title, sidebar } = getMetadata(targetFullPath);
29
+ if (sidebar.label) {
30
+ translations[lang] = sidebar.label;
31
+ } else if (title) {
32
+ translations[lang] = title;
33
+ }
34
+ }
35
+ }
36
+ return translations;
37
+ }
38
+ function generateWebPath(relativeDir, fileName, frontmatterSlug) {
39
+ if (frontmatterSlug) {
40
+ return frontmatterSlug;
41
+ }
42
+ const baseName = path.basename(fileName, path.extname(fileName));
43
+ const segments = relativeDir ? relativeDir.split(path.sep) : [];
44
+ segments.push(baseName);
45
+ return segments.map((s) => new GithubSlugger().slug(s)).join("/");
46
+ }
47
+ function generateMultilingualSidebar(baseContentDir, relativeDir, sourceLocale, targetLocales, segmentState) {
48
+ const scanRoot = sourceLocale === "root" ? baseContentDir : path.join(baseContentDir, sourceLocale);
49
+ const fullScanDir = path.join(scanRoot, relativeDir);
50
+ if (!fs.existsSync(fullScanDir)) return [];
51
+ const items = [];
52
+ let entries = [];
53
+ try {
54
+ entries = fs.readdirSync(fullScanDir).sort((a, b) => a.localeCompare(b));
55
+ } catch (e) {
56
+ return [];
57
+ }
58
+ for (const entry of entries) {
59
+ if (entry.startsWith(".")) continue;
60
+ const fullPath = path.join(fullScanDir, entry);
61
+ let stat;
62
+ try {
63
+ stat = fs.statSync(fullPath);
64
+ } catch {
65
+ continue;
66
+ }
67
+ if (stat.isDirectory()) {
68
+ const segment = entry;
69
+ const translations = {};
70
+ if (segmentState[segment]) {
71
+ targetLocales.forEach((lang) => {
72
+ if (segmentState[segment][lang]) {
73
+ translations[lang] = segmentState[segment][lang];
74
+ }
75
+ });
76
+ }
77
+ const children = generateMultilingualSidebar(
78
+ baseContentDir,
79
+ path.join(relativeDir, entry),
80
+ sourceLocale,
81
+ targetLocales,
82
+ segmentState
83
+ );
84
+ if (children.length > 0) {
85
+ const label = segment.charAt(0).toUpperCase() + segment.slice(1);
86
+ items.push({
87
+ label,
88
+ items: children,
89
+ translations
90
+ });
91
+ }
92
+ } else if (/\.mdx?$/.test(entry)) {
93
+ const { title, slug, sidebar } = getMetadata(fullPath);
94
+ if (sidebar.hidden) continue;
95
+ const label = sidebar.label || title || path.basename(entry, path.extname(entry));
96
+ const relPath = path.join(relativeDir, entry);
97
+ const translations = getTranslations(
98
+ baseContentDir,
99
+ relPath,
100
+ targetLocales,
101
+ sourceLocale
102
+ );
103
+ const link = generateWebPath(relativeDir, entry, slug);
104
+ const { hidden, order, label: _, ...extraProps } = sidebar;
105
+ items.push({
106
+ label,
107
+ link,
108
+ translations,
109
+ ...extraProps
110
+ });
111
+ }
112
+ }
113
+ return items;
114
+ }
115
+
116
+ // src/starlight/index.ts
117
+ function starlightTranslator() {
118
+ return {
119
+ name: "astro-llm-translator-starlight",
120
+ hooks: {
121
+ async setup({ config, updateConfig, logger }) {
122
+ let segmentState = {};
123
+ try {
124
+ const statePath = path2.resolve(
125
+ process.cwd(),
126
+ "src",
127
+ ".translator-segments.json"
128
+ );
129
+ const content = await fs2.readFile(statePath, "utf-8");
130
+ segmentState = JSON.parse(content);
131
+ } catch (e) {
132
+ }
133
+ const locales = config.locales ? Object.keys(config.locales) : ["root"];
134
+ const sourceLocale = config.defaultLocale || "root";
135
+ const targetLocales = locales.filter((l) => l !== sourceLocale);
136
+ const expandItems = (items) => {
137
+ return items.flatMap((item) => {
138
+ if (item.autogenerate) {
139
+ const dir = item.autogenerate.directory;
140
+ const generated = generateMultilingualSidebar(
141
+ "src/content/docs",
142
+ dir,
143
+ sourceLocale,
144
+ targetLocales,
145
+ segmentState
146
+ );
147
+ if (item.label) {
148
+ return [
149
+ {
150
+ label: item.label,
151
+ translations: item.translations,
152
+ items: generated,
153
+ collapsed: item.collapsed
154
+ }
155
+ ];
156
+ }
157
+ return generated;
158
+ } else if (item.items) {
159
+ return [
160
+ {
161
+ ...item,
162
+ items: expandItems(item.items)
163
+ }
164
+ ];
165
+ }
166
+ return [item];
167
+ });
168
+ };
169
+ if (Array.isArray(config.sidebar)) {
170
+ const newSidebar = expandItems(config.sidebar);
171
+ updateConfig({
172
+ sidebar: newSidebar
173
+ });
174
+ logger.info("Starlight sidebar expanded with multilingual labels.");
175
+ }
176
+ }
177
+ }
178
+ };
179
+ }
180
+ export {
181
+ starlightTranslator as default
182
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "astro-llm-translator",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Astro integration to automatically translate content using LLMs.",
6
+ "keywords": [
7
+ "astro",
8
+ "astro-integration",
9
+ "i18n",
10
+ "translation",
11
+ "llm",
12
+ "openai"
13
+ ],
14
+ "author": "Noa Virellia",
15
+ "license": "GPL-3.0-only",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/integration/index.d.ts",
19
+ "import": "./dist/integration/index.js"
20
+ },
21
+ "./starlight": {
22
+ "types": "./dist/starlight/index.d.ts",
23
+ "import": "./dist/starlight/index.js"
24
+ }
25
+ },
26
+ "main": "./dist/integration/index.js",
27
+ "types": "./dist/integration/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "dev": "astro dev",
35
+ "build": "astro build",
36
+ "build:lib": "tsup src/integration/index.ts src/starlight/index.ts --format esm --dts --clean",
37
+ "preview": "astro preview",
38
+ "astro": "astro",
39
+ "test": "vitest run",
40
+ "format": "biome format --write .",
41
+ "lint": "biome check ."
42
+ },
43
+ "dependencies": {
44
+ "dotenv": "^17.2.3",
45
+ "github-slugger": "^2.0.0",
46
+ "glob": "^13.0.0",
47
+ "gray-matter": "^4.0.3",
48
+ "zod": "3"
49
+ },
50
+ "peerDependencies": {
51
+ "astro": "^5.0.0",
52
+ "openai": "^4.0.0 || ^5.0.0 || ^6.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@astrojs/starlight": "^0.37.3",
56
+ "@biomejs/biome": "2.3.11",
57
+ "@types/github-slugger": "^2.0.0",
58
+ "@types/js-yaml": "^4.0.9",
59
+ "@types/node": "^25.0.9",
60
+ "astro": "^5.16.11",
61
+ "openai": "^6.16.0",
62
+ "tsup": "^8.5.1",
63
+ "vitest": "^4.0.17"
64
+ }
65
+ }