docs-i18n 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.
- package/README.md +59 -0
- package/dist/assemble-IOHQYYHI.js +74 -0
- package/dist/build-4EQEL4NI.js +12 -0
- package/dist/build2-3W5WMFHZ.js +4901 -0
- package/dist/chunk-3YNFMSJH.js +30 -0
- package/dist/chunk-55MBYBVK.js +368 -0
- package/dist/chunk-7HWWVRNS.js +104 -0
- package/dist/chunk-AKLW2MUS.js +54 -0
- package/dist/chunk-FYDB7MZX.js +38944 -0
- package/dist/chunk-O35QHRY6.js +6 -0
- package/dist/chunk-PTIH4GGE.js +44 -0
- package/dist/chunk-QSVWLTGQ.js +60 -0
- package/dist/chunk-SUIDX6IZ.js +122 -0
- package/dist/chunk-VKKNQBDN.js +6487 -0
- package/dist/chunk-XEOYZUHS.js +396 -0
- package/dist/cli.js +104 -0
- package/dist/dist-6C32URTL.js +19 -0
- package/dist/dist-HOWMMQFV.js +6677 -0
- package/dist/false-JGP4AGWN.js +7 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +545 -0
- package/dist/main-QVE5TVA3.js +2505 -0
- package/dist/node-4GLCLDJ6.js +875 -0
- package/dist/node-NUDVMOF2.js +129 -0
- package/dist/postcss-3SK7VUC2.js +5886 -0
- package/dist/postcss-import-JD46KA2Z.js +458 -0
- package/dist/prompt-BYQIwEjg-TG7DLENB.js +915 -0
- package/dist/rescan-VB2PILB2.js +74 -0
- package/dist/serve.d.ts +37 -0
- package/dist/serve.js +38 -0
- package/dist/server-ER56DGPR.js +548 -0
- package/dist/status-EWQEACVF.js +43 -0
- package/dist/translate-F3AQFN6X.js +707 -0
- package/package.json +55 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init_parser,
|
|
3
|
+
parseMdx
|
|
4
|
+
} from "./chunk-SUIDX6IZ.js";
|
|
5
|
+
import {
|
|
6
|
+
TranslationCache
|
|
7
|
+
} from "./chunk-XEOYZUHS.js";
|
|
8
|
+
import {
|
|
9
|
+
flattenSources
|
|
10
|
+
} from "./chunk-3YNFMSJH.js";
|
|
11
|
+
import "./chunk-AKLW2MUS.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/rescan.ts
|
|
14
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
init_parser();
|
|
17
|
+
function walkFiles(dir, patterns) {
|
|
18
|
+
const results = [];
|
|
19
|
+
if (!existsSync(dir)) return results;
|
|
20
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const fullPath = join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
results.push(...walkFiles(fullPath, patterns));
|
|
24
|
+
} else if (patterns.some((p) => entry.name.endsWith(p.replace("**/*", "")))) {
|
|
25
|
+
results.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
async function rescan(config, opts) {
|
|
31
|
+
const cacheDir = resolve(config.cacheDir ?? ".cache");
|
|
32
|
+
const cache = new TranslationCache(cacheDir);
|
|
33
|
+
const sources = flattenSources(config);
|
|
34
|
+
const extensions = (config.include ?? ["**/*.mdx", "**/*.md"]).map(
|
|
35
|
+
(p) => p.replace("**/*", "")
|
|
36
|
+
);
|
|
37
|
+
const t0 = Date.now();
|
|
38
|
+
for (const source of sources) {
|
|
39
|
+
if (opts.project && source.project !== opts.project) continue;
|
|
40
|
+
if (opts.version && source.version !== opts.version) continue;
|
|
41
|
+
const enDir = resolve(source.sourcePath);
|
|
42
|
+
if (!existsSync(enDir)) {
|
|
43
|
+
console.log(`\u26A0 ${source.versionKey}: source dir not found, skipping`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const files = walkFiles(enDir, extensions);
|
|
47
|
+
cache.clearSources("", source.versionKey);
|
|
48
|
+
let nodeCount = 0;
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const relPath = file.slice(enDir.length + 1);
|
|
51
|
+
const content = readFileSync(file, "utf8");
|
|
52
|
+
const nodes = parseMdx(content);
|
|
53
|
+
for (const node of nodes) {
|
|
54
|
+
if (node.needsTranslation && node.md5) {
|
|
55
|
+
const line = content.substring(0, node.startOffset).split("\n").length;
|
|
56
|
+
cache.setSource(node.md5, node.rawText, node.type);
|
|
57
|
+
cache.updateSource("", node.md5, relPath, line, source.versionKey);
|
|
58
|
+
nodeCount++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log(`\u2705 ${source.versionKey}: ${files.length} files, ${nodeCount} nodes`);
|
|
63
|
+
}
|
|
64
|
+
const orphanT = cache.db.prepare("DELETE FROM translations WHERE key NOT IN (SELECT DISTINCT key FROM source_files)").run();
|
|
65
|
+
if (orphanT.changes > 0) console.log(`\u{1F5D1}\uFE0F Deleted ${orphanT.changes} orphan translations`);
|
|
66
|
+
const orphanS = cache.db.prepare("DELETE FROM sources WHERE key NOT IN (SELECT DISTINCT key FROM source_files)").run();
|
|
67
|
+
if (orphanS.changes > 0) console.log(`\u{1F5D1}\uFE0F Deleted ${orphanS.changes} orphan sources`);
|
|
68
|
+
console.log(`
|
|
69
|
+
Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s`);
|
|
70
|
+
cache.db.close();
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
rescan
|
|
74
|
+
};
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime translation middleware for Cloudflare Workers + D1.
|
|
3
|
+
* Used by projects that do SSR (e.g., TanStack).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { createTranslator } from 'docs-i18n/serve';
|
|
8
|
+
*
|
|
9
|
+
* const translator = createTranslator();
|
|
10
|
+
* const translated = await translator.translate(enMarkdown, 'zh-hans', env.DB);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
interface D1Database {
|
|
14
|
+
prepare(sql: string): {
|
|
15
|
+
bind(...args: unknown[]): {
|
|
16
|
+
all(): Promise<{
|
|
17
|
+
results: Array<{
|
|
18
|
+
key: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a runtime translator that queries D1 for cached translations.
|
|
27
|
+
*/
|
|
28
|
+
declare function createTranslator(): {
|
|
29
|
+
/**
|
|
30
|
+
* Translate markdown content using D1 cache.
|
|
31
|
+
* Parses into nodes, batch-queries translations, assembles result.
|
|
32
|
+
* Untranslated nodes fall back to original English.
|
|
33
|
+
*/
|
|
34
|
+
translate(markdown: string, lang: string, db: D1Database): Promise<string>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { createTranslator };
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMdx
|
|
3
|
+
} from "./chunk-7HWWVRNS.js";
|
|
4
|
+
|
|
5
|
+
// src/serve.ts
|
|
6
|
+
function createTranslator() {
|
|
7
|
+
return {
|
|
8
|
+
/**
|
|
9
|
+
* Translate markdown content using D1 cache.
|
|
10
|
+
* Parses into nodes, batch-queries translations, assembles result.
|
|
11
|
+
* Untranslated nodes fall back to original English.
|
|
12
|
+
*/
|
|
13
|
+
async translate(markdown, lang, db) {
|
|
14
|
+
if (lang === "en") return markdown;
|
|
15
|
+
const nodes = parseMdx(markdown);
|
|
16
|
+
const translatableNodes = nodes.filter((n) => n.md5);
|
|
17
|
+
const keys = translatableNodes.map((n) => n.md5);
|
|
18
|
+
if (keys.length === 0) return markdown;
|
|
19
|
+
const placeholders = keys.map(() => "?").join(",");
|
|
20
|
+
const { results } = await db.prepare(
|
|
21
|
+
`SELECT key, value FROM translations WHERE lang = ? AND key IN (${placeholders})`
|
|
22
|
+
).bind(lang, ...keys).all();
|
|
23
|
+
const cache = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const row of results) {
|
|
25
|
+
cache.set(row.key, row.value);
|
|
26
|
+
}
|
|
27
|
+
return nodes.map((n) => {
|
|
28
|
+
if (n.md5 && cache.has(n.md5)) {
|
|
29
|
+
return cache.get(n.md5);
|
|
30
|
+
}
|
|
31
|
+
return n.rawText;
|
|
32
|
+
}).join("\n\n");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
createTranslator
|
|
38
|
+
};
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init_parser,
|
|
3
|
+
parseMdx
|
|
4
|
+
} from "./chunk-SUIDX6IZ.js";
|
|
5
|
+
import {
|
|
6
|
+
TranslationCache
|
|
7
|
+
} from "./chunk-XEOYZUHS.js";
|
|
8
|
+
import {
|
|
9
|
+
flattenSources
|
|
10
|
+
} from "./chunk-3YNFMSJH.js";
|
|
11
|
+
import "./chunk-AKLW2MUS.js";
|
|
12
|
+
|
|
13
|
+
// src/admin/server/index.ts
|
|
14
|
+
import { createServer } from "http";
|
|
15
|
+
import { resolve as resolve2 } from "path";
|
|
16
|
+
import { Hono as Hono4 } from "hono";
|
|
17
|
+
|
|
18
|
+
// src/admin/server/services/status.ts
|
|
19
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
20
|
+
import { join, resolve } from "path";
|
|
21
|
+
init_parser();
|
|
22
|
+
var _config;
|
|
23
|
+
var _cache = null;
|
|
24
|
+
function initStatus(config) {
|
|
25
|
+
_config = config;
|
|
26
|
+
_cache = null;
|
|
27
|
+
}
|
|
28
|
+
function getVersions() {
|
|
29
|
+
return flattenSources(_config).map((s) => s.versionKey);
|
|
30
|
+
}
|
|
31
|
+
function getLangs() {
|
|
32
|
+
return _config.languages;
|
|
33
|
+
}
|
|
34
|
+
function getCache() {
|
|
35
|
+
if (!_cache) {
|
|
36
|
+
_cache = new TranslationCache(resolve(_config.cacheDir ?? ".cache"));
|
|
37
|
+
}
|
|
38
|
+
return _cache;
|
|
39
|
+
}
|
|
40
|
+
function getSourcePath(versionKey) {
|
|
41
|
+
const source = flattenSources(_config).find((s) => s.versionKey === versionKey);
|
|
42
|
+
return source ? resolve(source.sourcePath) : null;
|
|
43
|
+
}
|
|
44
|
+
function walkFiles(dir) {
|
|
45
|
+
const results = [];
|
|
46
|
+
if (!existsSync(dir)) return results;
|
|
47
|
+
const exts = (_config.include ?? ["**/*.mdx", "**/*.md"]).map((p) => p.replace("**/*", ""));
|
|
48
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const fullPath = join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) results.push(...walkFiles(fullPath));
|
|
51
|
+
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(fullPath);
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
function ensureScanned(version) {
|
|
56
|
+
const cache = getCache();
|
|
57
|
+
const enDir = getSourcePath(version);
|
|
58
|
+
if (!enDir || !existsSync(enDir)) return;
|
|
59
|
+
const files = walkFiles(enDir);
|
|
60
|
+
const currentCount = cache.sourceCount(version);
|
|
61
|
+
if (currentCount >= files.length) return;
|
|
62
|
+
cache.clearSources("", version);
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const relPath = file.slice(enDir.length + 1);
|
|
65
|
+
const content = readFileSync(file, "utf8");
|
|
66
|
+
const nodes = parseMdx(content);
|
|
67
|
+
for (const node of nodes) {
|
|
68
|
+
if (node.needsTranslation && node.md5) {
|
|
69
|
+
const line = content.substring(0, node.startOffset).split("\n").length;
|
|
70
|
+
cache.setSource(node.md5, node.rawText, node.type);
|
|
71
|
+
cache.updateSource("", node.md5, relPath, line, version);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function rescan(version) {
|
|
77
|
+
const cache = getCache();
|
|
78
|
+
const enDir = getSourcePath(version);
|
|
79
|
+
if (!enDir || !existsSync(enDir)) return 0;
|
|
80
|
+
const files = walkFiles(enDir);
|
|
81
|
+
cache.clearSources("", version);
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const relPath = file.slice(enDir.length + 1);
|
|
84
|
+
const content = readFileSync(file, "utf8");
|
|
85
|
+
const nodes = parseMdx(content);
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
if (node.needsTranslation && node.md5) {
|
|
88
|
+
const line = content.substring(0, node.startOffset).split("\n").length;
|
|
89
|
+
cache.setSource(node.md5, node.rawText, node.type);
|
|
90
|
+
cache.updateSource("", node.md5, relPath, line, version);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return files.length;
|
|
95
|
+
}
|
|
96
|
+
function getOverview() {
|
|
97
|
+
const cache = getCache();
|
|
98
|
+
const versions = getVersions();
|
|
99
|
+
const langs = getLangs();
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const version of versions) {
|
|
102
|
+
ensureScanned(version);
|
|
103
|
+
const enDir = getSourcePath(version);
|
|
104
|
+
const enFileCount = enDir && existsSync(enDir) ? walkFiles(enDir).length : 0;
|
|
105
|
+
const langStats = {};
|
|
106
|
+
for (const lang of langs) {
|
|
107
|
+
const sections = cache.sectionStats(version, lang);
|
|
108
|
+
let totalFiles = 0, translatedFiles = 0, totalNodes = 0, translatedNodes = 0;
|
|
109
|
+
const sectionMap = {};
|
|
110
|
+
for (const s of sections) {
|
|
111
|
+
sectionMap[s.section] = s;
|
|
112
|
+
totalFiles += s.totalFiles;
|
|
113
|
+
translatedFiles += s.translatedFiles;
|
|
114
|
+
totalNodes += s.totalNodes;
|
|
115
|
+
translatedNodes += s.translatedNodes;
|
|
116
|
+
}
|
|
117
|
+
langStats[lang] = { sections: sectionMap, totalFiles, translatedFiles, totalNodes, translatedNodes };
|
|
118
|
+
}
|
|
119
|
+
result[version] = { enFileCount, langs: langStats };
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function getFileCoverage(version, lang) {
|
|
124
|
+
ensureScanned(version);
|
|
125
|
+
return getCache().fileCoverage(version, lang);
|
|
126
|
+
}
|
|
127
|
+
function getFileBlocks(version, lang, file) {
|
|
128
|
+
const enDir = getSourcePath(version);
|
|
129
|
+
if (!enDir) return null;
|
|
130
|
+
const enPath = join(enDir, file);
|
|
131
|
+
if (!existsSync(enPath)) return null;
|
|
132
|
+
const cache = getCache();
|
|
133
|
+
const content = readFileSync(enPath, "utf8");
|
|
134
|
+
const nodes = parseMdx(content);
|
|
135
|
+
const blocks = [];
|
|
136
|
+
let lastEnd = 0;
|
|
137
|
+
for (const node of nodes) {
|
|
138
|
+
if (node.startOffset > lastEnd) {
|
|
139
|
+
const gap = content.substring(lastEnd, node.startOffset);
|
|
140
|
+
blocks.push({ md5: null, type: "gap", source: gap, translation: null });
|
|
141
|
+
}
|
|
142
|
+
if (node.needsTranslation && node.md5) {
|
|
143
|
+
const translation = lang === "en" ? null : cache.get(lang, node.md5) ?? null;
|
|
144
|
+
blocks.push({ md5: node.md5, type: node.type, source: node.rawText, translation });
|
|
145
|
+
} else {
|
|
146
|
+
blocks.push({ md5: null, type: node.type, source: node.rawText, translation: null });
|
|
147
|
+
}
|
|
148
|
+
lastEnd = node.endOffset;
|
|
149
|
+
}
|
|
150
|
+
if (lastEnd < content.length) {
|
|
151
|
+
const tail = content.substring(lastEnd);
|
|
152
|
+
if (tail.trim()) blocks.push({ md5: null, type: "gap", source: tail, translation: null });
|
|
153
|
+
}
|
|
154
|
+
return blocks;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/admin/server/routes/jobs.ts
|
|
158
|
+
import { Hono } from "hono";
|
|
159
|
+
import { streamSSE } from "hono/streaming";
|
|
160
|
+
|
|
161
|
+
// src/admin/server/services/job-manager.ts
|
|
162
|
+
import { spawn } from "child_process";
|
|
163
|
+
var PROJECT_ROOT = process.cwd();
|
|
164
|
+
var JobManager = class {
|
|
165
|
+
jobs = /* @__PURE__ */ new Map();
|
|
166
|
+
processes = /* @__PURE__ */ new Map();
|
|
167
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
168
|
+
nextId = 1;
|
|
169
|
+
start(opts) {
|
|
170
|
+
for (const [, job2] of this.jobs) {
|
|
171
|
+
if (job2.lang === opts.lang && job2.version === opts.version && job2.status === "running") {
|
|
172
|
+
throw new Error(`Job already running for ${opts.lang}/${opts.version}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const id = `job-${this.nextId++}`;
|
|
176
|
+
const job = {
|
|
177
|
+
id,
|
|
178
|
+
lang: opts.lang,
|
|
179
|
+
version: opts.version,
|
|
180
|
+
project: opts.project ?? "",
|
|
181
|
+
status: "running",
|
|
182
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
183
|
+
logLines: [],
|
|
184
|
+
translatedFiles: 0,
|
|
185
|
+
totalFiles: 0,
|
|
186
|
+
toTranslate: 0,
|
|
187
|
+
errorFiles: 0
|
|
188
|
+
};
|
|
189
|
+
this.jobs.set(id, job);
|
|
190
|
+
const args = [
|
|
191
|
+
"src/cli.ts",
|
|
192
|
+
// In dev; in production would be the installed bin
|
|
193
|
+
"translate",
|
|
194
|
+
"--lang",
|
|
195
|
+
opts.lang,
|
|
196
|
+
"--version",
|
|
197
|
+
opts.version,
|
|
198
|
+
"--max",
|
|
199
|
+
String(opts.max ?? 999),
|
|
200
|
+
"--concurrency",
|
|
201
|
+
String(opts.concurrency ?? 3)
|
|
202
|
+
];
|
|
203
|
+
if (opts.project) args.push("--project", opts.project);
|
|
204
|
+
if (opts.model) args.push("--model", opts.model);
|
|
205
|
+
if (opts.files?.length) args.push("--files", opts.files.join(","));
|
|
206
|
+
const proc = spawn("bun", args, {
|
|
207
|
+
cwd: PROJECT_ROOT,
|
|
208
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
209
|
+
env: { ...process.env, NO_TTY: "1", FORCE_COLOR: "0" }
|
|
210
|
+
});
|
|
211
|
+
this.processes.set(id, proc);
|
|
212
|
+
proc.stdout?.on("data", (d) => {
|
|
213
|
+
for (const line of d.toString().split("\n").filter(Boolean)) {
|
|
214
|
+
this.addLog(id, line);
|
|
215
|
+
this.parseProgress(job, line);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
proc.stderr?.on("data", (d) => {
|
|
219
|
+
for (const line of d.toString().split("\n").filter(Boolean)) {
|
|
220
|
+
this.addLog(id, line);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
proc.on("exit", (code) => {
|
|
224
|
+
job.status = code === 0 ? "completed" : "failed";
|
|
225
|
+
job.exitCode = code;
|
|
226
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
|
+
job.currentFile = void 0;
|
|
228
|
+
this.addLog(id, `Process exited with code ${code}`);
|
|
229
|
+
this.processes.delete(id);
|
|
230
|
+
this.emit(id, { type: "exit", code });
|
|
231
|
+
});
|
|
232
|
+
proc.on("error", (err) => {
|
|
233
|
+
job.status = "failed";
|
|
234
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
235
|
+
this.addLog(id, `Process error: ${err.message}`);
|
|
236
|
+
this.processes.delete(id);
|
|
237
|
+
this.emit(id, { type: "exit", code: -1 });
|
|
238
|
+
});
|
|
239
|
+
return job;
|
|
240
|
+
}
|
|
241
|
+
cancel(id) {
|
|
242
|
+
const proc = this.processes.get(id);
|
|
243
|
+
const job = this.jobs.get(id);
|
|
244
|
+
if (!proc || !job) return false;
|
|
245
|
+
proc.kill("SIGTERM");
|
|
246
|
+
job.status = "cancelled";
|
|
247
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
248
|
+
this.processes.delete(id);
|
|
249
|
+
this.emit(id, { type: "exit", code: null });
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
list() {
|
|
253
|
+
return [...this.jobs.values()].sort(
|
|
254
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
get(id) {
|
|
258
|
+
return this.jobs.get(id);
|
|
259
|
+
}
|
|
260
|
+
remove(id) {
|
|
261
|
+
const job = this.jobs.get(id);
|
|
262
|
+
if (!job || job.status === "running") return false;
|
|
263
|
+
this.jobs.delete(id);
|
|
264
|
+
this.subscribers.delete(id);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
subscribe(id, callback) {
|
|
268
|
+
if (!this.subscribers.has(id)) this.subscribers.set(id, /* @__PURE__ */ new Set());
|
|
269
|
+
this.subscribers.get(id)?.add(callback);
|
|
270
|
+
return () => {
|
|
271
|
+
this.subscribers.get(id)?.delete(callback);
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
addLog(id, line) {
|
|
275
|
+
const job = this.jobs.get(id);
|
|
276
|
+
if (!job) return;
|
|
277
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
278
|
+
const entry = `[${ts}] ${line}`;
|
|
279
|
+
job.logLines = job.logLines ?? [];
|
|
280
|
+
job.logLines.push(entry);
|
|
281
|
+
if (job.logLines.length > 500) job.logLines.shift();
|
|
282
|
+
this.emit(id, { type: "log", data: entry });
|
|
283
|
+
}
|
|
284
|
+
emit(id, event) {
|
|
285
|
+
this.subscribers.get(id)?.forEach((cb) => cb(event));
|
|
286
|
+
}
|
|
287
|
+
parseProgress(job, line) {
|
|
288
|
+
const cached = line.match(/\+(\d+) cached/);
|
|
289
|
+
if (cached) job.translatedFiles += Number.parseInt(cached[1], 10);
|
|
290
|
+
const untranslated = line.match(/(\d+) untranslated keys/);
|
|
291
|
+
if (untranslated) job.toTranslate = Number.parseInt(untranslated[1], 10);
|
|
292
|
+
const chunk = line.match(/chunk \d+\/(\d+)/);
|
|
293
|
+
if (chunk) job.totalFiles = Number.parseInt(chunk[1], 10);
|
|
294
|
+
if (line.includes("\u23F3")) job.currentFile = line.replace(/^⏳\s*/, "");
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var jobManager = new JobManager();
|
|
298
|
+
|
|
299
|
+
// src/admin/server/routes/jobs.ts
|
|
300
|
+
var app = new Hono();
|
|
301
|
+
app.get("/", (c) => {
|
|
302
|
+
return c.json(
|
|
303
|
+
jobManager.list().map((j) => ({
|
|
304
|
+
...j,
|
|
305
|
+
logLines: (j.logLines ?? []).slice(-20)
|
|
306
|
+
// Last 20 lines in list view
|
|
307
|
+
}))
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
app.post("/", async (c) => {
|
|
311
|
+
const body = await c.req.json();
|
|
312
|
+
if (!body.lang || !body.version) {
|
|
313
|
+
return c.json({ error: "Missing lang or version" }, 400);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const job = jobManager.start(body);
|
|
317
|
+
return c.json(job, 201);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return c.json(
|
|
320
|
+
{ error: err instanceof Error ? err.message : "Unknown error" },
|
|
321
|
+
409
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
app.get("/:id", (c) => {
|
|
326
|
+
const job = jobManager.get(c.req.param("id"));
|
|
327
|
+
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
328
|
+
return c.json(job);
|
|
329
|
+
});
|
|
330
|
+
app.delete("/:id", (c) => {
|
|
331
|
+
const id = c.req.param("id");
|
|
332
|
+
const job = jobManager.get(id);
|
|
333
|
+
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
334
|
+
if (job.status === "running") {
|
|
335
|
+
jobManager.cancel(id);
|
|
336
|
+
} else {
|
|
337
|
+
jobManager.remove(id);
|
|
338
|
+
}
|
|
339
|
+
return c.json({ ok: true });
|
|
340
|
+
});
|
|
341
|
+
app.get("/:id/stream", (c) => {
|
|
342
|
+
const id = c.req.param("id");
|
|
343
|
+
const job = jobManager.get(id);
|
|
344
|
+
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
345
|
+
return streamSSE(c, async (stream) => {
|
|
346
|
+
await stream.writeSSE({
|
|
347
|
+
data: JSON.stringify({
|
|
348
|
+
type: "state",
|
|
349
|
+
data: { ...job, logLines: (job.logLines ?? []).slice(-50) }
|
|
350
|
+
}),
|
|
351
|
+
event: "message"
|
|
352
|
+
});
|
|
353
|
+
const unsubscribe = jobManager.subscribe(id, async (event) => {
|
|
354
|
+
try {
|
|
355
|
+
await stream.writeSSE({
|
|
356
|
+
data: JSON.stringify(event),
|
|
357
|
+
event: "message"
|
|
358
|
+
});
|
|
359
|
+
} catch {
|
|
360
|
+
unsubscribe();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
while (true) {
|
|
364
|
+
const currentJob = jobManager.get(id);
|
|
365
|
+
if (!currentJob || currentJob.status !== "running") {
|
|
366
|
+
await stream.writeSSE({
|
|
367
|
+
data: JSON.stringify({ type: "done", data: currentJob }),
|
|
368
|
+
event: "message"
|
|
369
|
+
});
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
await stream.sleep(1e3);
|
|
373
|
+
}
|
|
374
|
+
unsubscribe();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
var jobs_default = app;
|
|
378
|
+
|
|
379
|
+
// src/admin/server/routes/models.ts
|
|
380
|
+
import { Hono as Hono2 } from "hono";
|
|
381
|
+
var app2 = new Hono2();
|
|
382
|
+
var cachedResult = null;
|
|
383
|
+
var cacheTime = 0;
|
|
384
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
385
|
+
app2.get("/", async (c) => {
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
if (cachedResult && now - cacheTime < CACHE_TTL) {
|
|
388
|
+
return c.json(cachedResult);
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch("https://openrouter.ai/api/v1/models");
|
|
392
|
+
if (!res.ok) {
|
|
393
|
+
return c.json({ error: `OpenRouter API error: ${res.status}` }, 502);
|
|
394
|
+
}
|
|
395
|
+
const { data } = await res.json();
|
|
396
|
+
cachedResult = formatModels(data);
|
|
397
|
+
cacheTime = now;
|
|
398
|
+
return c.json(cachedResult);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return c.json(
|
|
401
|
+
{ error: err instanceof Error ? err.message : "Failed to fetch models" },
|
|
402
|
+
500
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
function formatModels(models) {
|
|
407
|
+
return models.filter((m) => {
|
|
408
|
+
if (!m.pricing) return false;
|
|
409
|
+
const pp = Number.parseFloat(m.pricing.prompt);
|
|
410
|
+
const cp = Number.parseFloat(m.pricing.completion);
|
|
411
|
+
if (pp < 0 || cp < 0) return false;
|
|
412
|
+
if (!m.architecture?.modality?.startsWith("text")) return false;
|
|
413
|
+
if (!m.architecture.output_modalities?.includes("text")) return false;
|
|
414
|
+
return true;
|
|
415
|
+
}).map((m) => {
|
|
416
|
+
const pp = Number.parseFloat(m.pricing.prompt) * 1e6;
|
|
417
|
+
const cp = Number.parseFloat(m.pricing.completion) * 1e6;
|
|
418
|
+
return {
|
|
419
|
+
id: m.id,
|
|
420
|
+
name: m.name,
|
|
421
|
+
promptPrice: pp,
|
|
422
|
+
completionPrice: cp,
|
|
423
|
+
contextLength: m.context_length,
|
|
424
|
+
maxOutput: m.top_provider?.max_completion_tokens ?? 0,
|
|
425
|
+
isFree: pp === 0 && cp === 0,
|
|
426
|
+
supportsJson: m.supported_parameters?.includes("response_format") || m.supported_parameters?.includes("structured_outputs"),
|
|
427
|
+
supportsTools: m.supported_parameters?.includes("tools"),
|
|
428
|
+
provider: m.id.split("/")[0]
|
|
429
|
+
};
|
|
430
|
+
}).sort((a, b) => a.promptPrice - b.promptPrice);
|
|
431
|
+
}
|
|
432
|
+
var models_default = app2;
|
|
433
|
+
|
|
434
|
+
// src/admin/server/routes/status.ts
|
|
435
|
+
import { Hono as Hono3 } from "hono";
|
|
436
|
+
var app3 = new Hono3();
|
|
437
|
+
app3.get("/", (c) => {
|
|
438
|
+
return c.json({
|
|
439
|
+
versions: getVersions(),
|
|
440
|
+
langs: ["en", ...getLangs()],
|
|
441
|
+
data: getOverview()
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
app3.get("/:version/:lang", (c) => {
|
|
445
|
+
const { version, lang } = c.req.param();
|
|
446
|
+
if (lang === "en") {
|
|
447
|
+
const anyLang = getLangs()[0];
|
|
448
|
+
const coverage = getFileCoverage(version, anyLang);
|
|
449
|
+
return c.json(coverage.map((f) => ({ file: f.file, total: f.total, translated: f.total })));
|
|
450
|
+
}
|
|
451
|
+
return c.json(getFileCoverage(version, lang));
|
|
452
|
+
});
|
|
453
|
+
app3.get("/:version/:lang/blocks", (c) => {
|
|
454
|
+
const { version, lang } = c.req.param();
|
|
455
|
+
const filePath = c.req.query("path");
|
|
456
|
+
if (!filePath) return c.json({ error: "Missing path query param" }, 400);
|
|
457
|
+
const blocks = getFileBlocks(version, lang, filePath);
|
|
458
|
+
if (!blocks) return c.json({ error: "File not found" }, 404);
|
|
459
|
+
return c.json({ file: filePath, lang, version, blocks });
|
|
460
|
+
});
|
|
461
|
+
app3.delete("/:version/:lang/cache", (c) => {
|
|
462
|
+
const { lang } = c.req.param();
|
|
463
|
+
const key = c.req.query("key");
|
|
464
|
+
if (!key) return c.json({ error: "Missing key query param" }, 400);
|
|
465
|
+
const cache = getCache();
|
|
466
|
+
cache.db.prepare("DELETE FROM translations WHERE lang = ? AND key = ?").run(lang, key);
|
|
467
|
+
return c.json({ deleted: key, lang });
|
|
468
|
+
});
|
|
469
|
+
app3.post("/:version/rescan", (c) => {
|
|
470
|
+
const { version } = c.req.param();
|
|
471
|
+
const count = rescan(version);
|
|
472
|
+
return c.json({ version, files: count });
|
|
473
|
+
});
|
|
474
|
+
var status_default = app3;
|
|
475
|
+
|
|
476
|
+
// src/admin/server/index.ts
|
|
477
|
+
async function startAdmin(config, port = 3456) {
|
|
478
|
+
initStatus(config);
|
|
479
|
+
const app4 = new Hono4();
|
|
480
|
+
app4.route("/api/status", status_default);
|
|
481
|
+
app4.route("/api/jobs", jobs_default);
|
|
482
|
+
app4.route("/api/models", models_default);
|
|
483
|
+
app4.get("/api/health", (c) => c.json({ ok: true }));
|
|
484
|
+
app4.get("/api/config", (c) => c.json({ projectRoot: process.cwd() }));
|
|
485
|
+
app4.post("/api/open-file", async (c) => {
|
|
486
|
+
const { file } = await c.req.json();
|
|
487
|
+
if (!file) return c.json({ error: "Missing file" }, 400);
|
|
488
|
+
const fullPath = resolve2(process.cwd(), file);
|
|
489
|
+
if (!fullPath.startsWith(process.cwd())) return c.json({ error: "Invalid path" }, 400);
|
|
490
|
+
const candidates = process.env.EDITOR_CMD ? [process.env.EDITOR_CMD] : ["code", "cursor", "zed"];
|
|
491
|
+
for (const cmd of candidates) {
|
|
492
|
+
const which = Bun.spawn(["which", cmd], { stdio: ["ignore", "pipe", "ignore"] });
|
|
493
|
+
await which.exited;
|
|
494
|
+
if (which.exitCode === 0) {
|
|
495
|
+
Bun.spawn([cmd, fullPath], { stdio: ["ignore", "ignore", "ignore"] });
|
|
496
|
+
return c.json({ opened: fullPath, editor: cmd });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const fallback = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
500
|
+
Bun.spawn([fallback, fullPath], { stdio: ["ignore", "ignore", "ignore"] });
|
|
501
|
+
return c.json({ opened: fullPath, editor: fallback });
|
|
502
|
+
});
|
|
503
|
+
const adminRoot = resolve2(import.meta.dir, "..");
|
|
504
|
+
try {
|
|
505
|
+
const { createServer: createViteServer } = await import("./node-NUDVMOF2.js");
|
|
506
|
+
const vite = await createViteServer({
|
|
507
|
+
root: adminRoot,
|
|
508
|
+
server: { middlewareMode: true },
|
|
509
|
+
appType: "spa"
|
|
510
|
+
});
|
|
511
|
+
const server = createServer(async (req, res) => {
|
|
512
|
+
const url = req.url ?? "/";
|
|
513
|
+
if (url.startsWith("/api")) {
|
|
514
|
+
const headers = new Headers();
|
|
515
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
516
|
+
if (v) headers.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
517
|
+
}
|
|
518
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await new Promise((r) => {
|
|
519
|
+
let data = "";
|
|
520
|
+
req.on("data", (c) => {
|
|
521
|
+
data += c.toString();
|
|
522
|
+
});
|
|
523
|
+
req.on("end", () => r(data));
|
|
524
|
+
}) : void 0;
|
|
525
|
+
const webReq = new Request(`http://localhost:${port}${url}`, {
|
|
526
|
+
method: req.method,
|
|
527
|
+
headers,
|
|
528
|
+
body
|
|
529
|
+
});
|
|
530
|
+
const webRes = await app4.fetch(webReq);
|
|
531
|
+
res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
|
|
532
|
+
res.end(Buffer.from(await webRes.arrayBuffer()));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
vite.middlewares(req, res);
|
|
536
|
+
});
|
|
537
|
+
server.listen(port, () => {
|
|
538
|
+
console.log(`\u{1F310} docs-i18n admin \u2192 http://localhost:${port}`);
|
|
539
|
+
});
|
|
540
|
+
} catch {
|
|
541
|
+
console.error("Failed to start admin UI (vite not available). API-only mode.");
|
|
542
|
+
const server = Bun.serve({ port, fetch: app4.fetch });
|
|
543
|
+
console.log(`\u{1F310} docs-i18n admin (API only) \u2192 http://localhost:${port}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export {
|
|
547
|
+
startAdmin
|
|
548
|
+
};
|