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
package/dist/index.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FRONTMATTER_TRANSLATABLE_FIELDS,
|
|
3
|
+
normalize,
|
|
4
|
+
parseMdx
|
|
5
|
+
} from "./chunk-7HWWVRNS.js";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
function defineConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
async function loadConfig(path2) {
|
|
12
|
+
const configPath = path2 ?? "docs-i18n.config.ts";
|
|
13
|
+
try {
|
|
14
|
+
const mod = await import(
|
|
15
|
+
/* @vite-ignore */
|
|
16
|
+
`${process.cwd()}/${configPath}`
|
|
17
|
+
);
|
|
18
|
+
return mod.default ?? mod;
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(`Cannot load config from ${configPath}. Create a docs-i18n.config.ts file.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function flattenSources(config) {
|
|
24
|
+
const projects = Object.entries(config.projects);
|
|
25
|
+
const singleProject = projects.length === 1;
|
|
26
|
+
return projects.flatMap(
|
|
27
|
+
([projectId, project]) => Object.entries(project.sources).map(([version, sourcePath]) => ({
|
|
28
|
+
project: projectId,
|
|
29
|
+
version,
|
|
30
|
+
sourcePath,
|
|
31
|
+
versionKey: singleProject ? version : `${projectId}/${version}`
|
|
32
|
+
}))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/core/assembler.ts
|
|
37
|
+
var NEEDS_TRANSLATION_START = "<!-- NEEDS_TRANSLATION -->";
|
|
38
|
+
var NEEDS_TRANSLATION_END = "<!-- /NEEDS_TRANSLATION -->";
|
|
39
|
+
function assemble(rawContent, lang, cache, sourceFilePath, fallbackToSource = false) {
|
|
40
|
+
const normalizedContent = normalize(rawContent);
|
|
41
|
+
const nodes = parseMdx(rawContent);
|
|
42
|
+
let result = "";
|
|
43
|
+
let lastEnd = 0;
|
|
44
|
+
let cachedCount = 0;
|
|
45
|
+
let uncachedCount = 0;
|
|
46
|
+
let totalTranslatable = 0;
|
|
47
|
+
for (const node of nodes) {
|
|
48
|
+
result += normalizedContent.substring(lastEnd, node.startOffset);
|
|
49
|
+
if (node.needsTranslation) {
|
|
50
|
+
totalTranslatable++;
|
|
51
|
+
const cached = node.md5 ? cache.get(lang, node.md5) : void 0;
|
|
52
|
+
if (node.md5 && sourceFilePath) {
|
|
53
|
+
const line = normalizedContent.substring(0, node.startOffset).split("\n").length;
|
|
54
|
+
cache.updateSource(lang, node.md5, sourceFilePath, line);
|
|
55
|
+
}
|
|
56
|
+
if (cached) {
|
|
57
|
+
result += cached;
|
|
58
|
+
cachedCount++;
|
|
59
|
+
} else if (fallbackToSource) {
|
|
60
|
+
result += node.rawText;
|
|
61
|
+
uncachedCount++;
|
|
62
|
+
} else {
|
|
63
|
+
result += `${NEEDS_TRANSLATION_START}
|
|
64
|
+
${node.rawText}
|
|
65
|
+
${NEEDS_TRANSLATION_END}`;
|
|
66
|
+
uncachedCount++;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
result += node.rawText;
|
|
70
|
+
}
|
|
71
|
+
lastEnd = node.endOffset;
|
|
72
|
+
}
|
|
73
|
+
result += normalizedContent.substring(lastEnd);
|
|
74
|
+
return {
|
|
75
|
+
content: result,
|
|
76
|
+
allCached: uncachedCount === 0,
|
|
77
|
+
cachedCount,
|
|
78
|
+
uncachedCount,
|
|
79
|
+
totalTranslatable,
|
|
80
|
+
parsedNodes: nodes
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/core/cache.ts
|
|
85
|
+
import fs from "fs";
|
|
86
|
+
import path from "path";
|
|
87
|
+
|
|
88
|
+
// src/core/sqlite.ts
|
|
89
|
+
import { createRequire } from "module";
|
|
90
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
91
|
+
function openDatabase(filepath) {
|
|
92
|
+
if (isBun) {
|
|
93
|
+
const req2 = createRequire(import.meta.url);
|
|
94
|
+
const mod = req2("bun:sqlite");
|
|
95
|
+
const Database2 = mod.Database ?? mod.default ?? mod;
|
|
96
|
+
const raw = new Database2(filepath);
|
|
97
|
+
return {
|
|
98
|
+
prepare: (sql) => {
|
|
99
|
+
const stmt = raw.query(sql);
|
|
100
|
+
return {
|
|
101
|
+
run: (...params) => stmt.run(...params),
|
|
102
|
+
get: (...params) => stmt.get(...params),
|
|
103
|
+
all: (...params) => stmt.all(...params)
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
exec: (sql) => raw.run(sql),
|
|
107
|
+
pragma: (p) => raw.run(`PRAGMA ${p}`),
|
|
108
|
+
transaction: (fn) => raw.transaction(fn),
|
|
109
|
+
close: () => raw.close()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const req = createRequire(import.meta.url);
|
|
113
|
+
const Database = req("better-sqlite3");
|
|
114
|
+
return new Database(filepath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/core/cache.ts
|
|
118
|
+
var TranslationCache = class {
|
|
119
|
+
db;
|
|
120
|
+
cacheDir;
|
|
121
|
+
// Prepared statements (lazily initialized)
|
|
122
|
+
_stmts;
|
|
123
|
+
constructor(cacheDir) {
|
|
124
|
+
this.cacheDir = cacheDir;
|
|
125
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
126
|
+
const dbPath = path.join(cacheDir, "translations.db");
|
|
127
|
+
this.db = openDatabase(dbPath);
|
|
128
|
+
this.db.pragma("journal_mode = WAL");
|
|
129
|
+
this.db.pragma("busy_timeout = 5000");
|
|
130
|
+
this.db.pragma("synchronous = NORMAL");
|
|
131
|
+
this.db.pragma("foreign_keys = ON");
|
|
132
|
+
this.db.exec(`
|
|
133
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
134
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
135
|
+
text TEXT NOT NULL,
|
|
136
|
+
type TEXT NOT NULL DEFAULT 'paragraph'
|
|
137
|
+
) WITHOUT ROWID;
|
|
138
|
+
|
|
139
|
+
CREATE TABLE IF NOT EXISTS source_files (
|
|
140
|
+
key TEXT NOT NULL,
|
|
141
|
+
file TEXT NOT NULL,
|
|
142
|
+
line INTEGER NOT NULL,
|
|
143
|
+
version TEXT NOT NULL DEFAULT 'latest',
|
|
144
|
+
PRIMARY KEY (version, key, file, line)
|
|
145
|
+
) WITHOUT ROWID;
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS translations (
|
|
148
|
+
lang TEXT NOT NULL,
|
|
149
|
+
key TEXT NOT NULL,
|
|
150
|
+
value TEXT NOT NULL,
|
|
151
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
152
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
153
|
+
PRIMARY KEY (lang, key)
|
|
154
|
+
) WITHOUT ROWID;
|
|
155
|
+
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_translations_lang ON translations(lang);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_source_files_version ON source_files(version);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_source_files_file ON source_files(version, file);
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
get stmts() {
|
|
162
|
+
if (!this._stmts) {
|
|
163
|
+
this._stmts = {
|
|
164
|
+
getTranslation: this.db.prepare(
|
|
165
|
+
"SELECT value FROM translations WHERE lang = ? AND key = ?"
|
|
166
|
+
),
|
|
167
|
+
setTranslation: this.db.prepare(`
|
|
168
|
+
INSERT INTO translations (lang, key, value)
|
|
169
|
+
VALUES (?, ?, ?)
|
|
170
|
+
ON CONFLICT(lang, key) DO UPDATE SET value = excluded.value, updated_at = unixepoch()
|
|
171
|
+
`),
|
|
172
|
+
deleteTranslation: this.db.prepare(
|
|
173
|
+
"DELETE FROM translations WHERE lang = ? AND key = ?"
|
|
174
|
+
),
|
|
175
|
+
countByLang: this.db.prepare(
|
|
176
|
+
"SELECT COUNT(*) as count FROM translations WHERE lang = ?"
|
|
177
|
+
),
|
|
178
|
+
allByLang: this.db.prepare(
|
|
179
|
+
"SELECT key, value FROM translations WHERE lang = ?"
|
|
180
|
+
),
|
|
181
|
+
keysByLang: this.db.prepare(
|
|
182
|
+
"SELECT key FROM translations WHERE lang = ?"
|
|
183
|
+
),
|
|
184
|
+
setSource: this.db.prepare(`
|
|
185
|
+
INSERT INTO sources (key, text, type) VALUES (?, ?, ?)
|
|
186
|
+
ON CONFLICT(key) DO UPDATE SET text = excluded.text, type = excluded.type
|
|
187
|
+
`),
|
|
188
|
+
getSource: this.db.prepare(
|
|
189
|
+
"SELECT text, type FROM sources WHERE key = ?"
|
|
190
|
+
),
|
|
191
|
+
clearSourceFiles: this.db.prepare(
|
|
192
|
+
"DELETE FROM source_files WHERE version = ?"
|
|
193
|
+
),
|
|
194
|
+
upsertSourceFile: this.db.prepare(`
|
|
195
|
+
INSERT OR IGNORE INTO source_files (key, file, line, version) VALUES (?, ?, ?, ?)
|
|
196
|
+
`),
|
|
197
|
+
deleteSourceFilesByLang: this.db.prepare(
|
|
198
|
+
"DELETE FROM source_files WHERE version = ?"
|
|
199
|
+
)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return this._stmts;
|
|
203
|
+
}
|
|
204
|
+
// ── Public API (backward-compatible with JSONL version) ──
|
|
205
|
+
/**
|
|
206
|
+
* Load cache for a language. No-op for SQLite (data is always available).
|
|
207
|
+
* Kept for API compatibility.
|
|
208
|
+
*/
|
|
209
|
+
load(_lang) {
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Save cache for a language. No-op for SQLite (writes are immediate).
|
|
213
|
+
* Kept for API compatibility.
|
|
214
|
+
*/
|
|
215
|
+
save(_lang) {
|
|
216
|
+
}
|
|
217
|
+
/** Get a cached translation */
|
|
218
|
+
get(lang, md5) {
|
|
219
|
+
const row = this.stmts.getTranslation.get(lang, md5);
|
|
220
|
+
return row?.value;
|
|
221
|
+
}
|
|
222
|
+
/** Set a cached translation (writes immediately to disk) */
|
|
223
|
+
set(lang, md5, translation) {
|
|
224
|
+
this.stmts.setTranslation.run(lang, md5, translation);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Set a cached translation with its EN source text.
|
|
228
|
+
* This also stores the source in the `sources` table for dashboard use.
|
|
229
|
+
*/
|
|
230
|
+
setWithSource(lang, md5, translation, sourceText, sourceType = "paragraph") {
|
|
231
|
+
this.db.transaction(() => {
|
|
232
|
+
this.stmts.setSource.run(md5, sourceText, sourceType);
|
|
233
|
+
this.stmts.setTranslation.run(lang, md5, translation);
|
|
234
|
+
})();
|
|
235
|
+
}
|
|
236
|
+
/** Delete a cached translation */
|
|
237
|
+
delete(lang, md5) {
|
|
238
|
+
const result = this.stmts.deleteTranslation.run(lang, md5);
|
|
239
|
+
return result.changes > 0;
|
|
240
|
+
}
|
|
241
|
+
/** Iterate all entries for a language */
|
|
242
|
+
*entries(lang) {
|
|
243
|
+
const rows = this.stmts.allByLang.all(lang);
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
yield [row.key, { v: row.value, src: [] }];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Get all cached keys for a language as a Set (fast for coverage checks) */
|
|
249
|
+
keys(lang) {
|
|
250
|
+
const rows = this.stmts.keysByLang.all(lang);
|
|
251
|
+
return new Set(rows.map((r) => r.key));
|
|
252
|
+
}
|
|
253
|
+
/** Remove entries not in the provided set of md5s */
|
|
254
|
+
prune(lang, usedMd5s) {
|
|
255
|
+
const allKeys = this.keys(lang);
|
|
256
|
+
let removed = 0;
|
|
257
|
+
const deleteStmt = this.stmts.deleteTranslation;
|
|
258
|
+
this.db.transaction(() => {
|
|
259
|
+
for (const md5 of allKeys) {
|
|
260
|
+
if (!usedMd5s.has(md5)) {
|
|
261
|
+
deleteStmt.run(lang, md5);
|
|
262
|
+
removed++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
return removed;
|
|
267
|
+
}
|
|
268
|
+
/** Get cache stats for a language */
|
|
269
|
+
stats(lang) {
|
|
270
|
+
const row = this.stmts.countByLang.get(lang);
|
|
271
|
+
return { size: row?.count ?? 0 };
|
|
272
|
+
}
|
|
273
|
+
// ── Source tracking ──
|
|
274
|
+
/**
|
|
275
|
+
* Update source locations for a md5 hash.
|
|
276
|
+
* In SQLite mode, this inserts into source_files table.
|
|
277
|
+
*/
|
|
278
|
+
updateSource(_lang, md5, filePath, line, version = "latest") {
|
|
279
|
+
this.stmts.upsertSourceFile.run(md5, filePath, line, version);
|
|
280
|
+
}
|
|
281
|
+
/** Clear all source file mappings for a version (call before a full rebuild) */
|
|
282
|
+
clearSources(_lang, version = "latest") {
|
|
283
|
+
this.stmts.clearSourceFiles.run(version);
|
|
284
|
+
}
|
|
285
|
+
/** Store an EN source text in the sources table */
|
|
286
|
+
setSource(md5, text, type = "paragraph") {
|
|
287
|
+
this.stmts.setSource.run(md5, text, type);
|
|
288
|
+
}
|
|
289
|
+
/** Get an EN source text from the sources table */
|
|
290
|
+
getSource(md5) {
|
|
291
|
+
const row = this.stmts.getSource.get(md5);
|
|
292
|
+
return row ?? void 0;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get all untranslated source keys for a language and version.
|
|
296
|
+
* Returns keys with their EN source text and type.
|
|
297
|
+
*/
|
|
298
|
+
untranslatedKeys(lang, version = "latest", limit = 0, files) {
|
|
299
|
+
let fileFilter = "";
|
|
300
|
+
const params = [lang, version];
|
|
301
|
+
if (files && files.length > 0) {
|
|
302
|
+
fileFilter = ` AND sf.file IN (${files.map(() => "?").join(",")})`;
|
|
303
|
+
params.push(...files);
|
|
304
|
+
}
|
|
305
|
+
const sql = `
|
|
306
|
+
SELECT DISTINCT s.key, s.text, s.type
|
|
307
|
+
FROM source_files sf
|
|
308
|
+
JOIN sources s ON s.key = sf.key
|
|
309
|
+
LEFT JOIN translations t ON t.key = sf.key AND t.lang = ?
|
|
310
|
+
WHERE sf.version = ? AND t.key IS NULL${fileFilter}
|
|
311
|
+
${limit > 0 ? `LIMIT ${limit}` : ""}
|
|
312
|
+
`;
|
|
313
|
+
return this.db.prepare(sql).all(...params);
|
|
314
|
+
}
|
|
315
|
+
// ── Dashboard queries ──
|
|
316
|
+
/** Get translation stats for all languages */
|
|
317
|
+
allLangStats() {
|
|
318
|
+
return this.db.prepare("SELECT lang, COUNT(*) as count FROM translations GROUP BY lang").all();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get file-level coverage for a version and language.
|
|
322
|
+
* Returns how many source nodes each file has and how many are translated.
|
|
323
|
+
*/
|
|
324
|
+
fileCoverage(version, lang) {
|
|
325
|
+
return this.db.prepare(
|
|
326
|
+
`
|
|
327
|
+
SELECT
|
|
328
|
+
sf.file,
|
|
329
|
+
COUNT(DISTINCT sf.key) as total,
|
|
330
|
+
COUNT(DISTINCT t.key) as translated
|
|
331
|
+
FROM source_files sf
|
|
332
|
+
LEFT JOIN translations t ON t.key = sf.key AND t.lang = ?
|
|
333
|
+
WHERE sf.version = ?
|
|
334
|
+
GROUP BY sf.file
|
|
335
|
+
ORDER BY sf.file
|
|
336
|
+
`
|
|
337
|
+
).all(lang, version);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Get node-level detail for a specific file: EN source + translation side by side.
|
|
341
|
+
*/
|
|
342
|
+
fileDetail(version, lang, file) {
|
|
343
|
+
return this.db.prepare(
|
|
344
|
+
`
|
|
345
|
+
SELECT
|
|
346
|
+
s.key,
|
|
347
|
+
s.text as source,
|
|
348
|
+
s.type,
|
|
349
|
+
t.value as translation,
|
|
350
|
+
sf.line
|
|
351
|
+
FROM source_files sf
|
|
352
|
+
JOIN sources s ON s.key = sf.key
|
|
353
|
+
LEFT JOIN translations t ON t.key = sf.key AND t.lang = ?
|
|
354
|
+
WHERE sf.version = ? AND sf.file = ?
|
|
355
|
+
ORDER BY sf.line
|
|
356
|
+
`
|
|
357
|
+
).all(lang, version, file);
|
|
358
|
+
}
|
|
359
|
+
/** Get total source node count for a version */
|
|
360
|
+
sourceCount(version) {
|
|
361
|
+
const row = this.db.prepare(
|
|
362
|
+
"SELECT COUNT(DISTINCT key) as count FROM source_files WHERE version = ?"
|
|
363
|
+
).get(version);
|
|
364
|
+
return row?.count ?? 0;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get section-level stats for a version and language.
|
|
368
|
+
* Section is derived from file path prefix (docs/, blog/, learn/).
|
|
369
|
+
*/
|
|
370
|
+
sectionStats(version, lang) {
|
|
371
|
+
return this.db.prepare(
|
|
372
|
+
`
|
|
373
|
+
WITH file_coverage AS (
|
|
374
|
+
SELECT
|
|
375
|
+
sf.file,
|
|
376
|
+
CASE
|
|
377
|
+
WHEN sf.file LIKE 'docs/%' THEN 'docs'
|
|
378
|
+
WHEN sf.file LIKE 'blog/%' THEN 'blog'
|
|
379
|
+
WHEN sf.file LIKE 'learn/%' THEN 'learn'
|
|
380
|
+
ELSE 'other'
|
|
381
|
+
END as section,
|
|
382
|
+
COUNT(DISTINCT sf.key) as total_nodes,
|
|
383
|
+
COUNT(DISTINCT t.key) as translated_nodes
|
|
384
|
+
FROM source_files sf
|
|
385
|
+
LEFT JOIN translations t ON t.key = sf.key AND t.lang = ?
|
|
386
|
+
WHERE sf.version = ?
|
|
387
|
+
GROUP BY sf.file
|
|
388
|
+
)
|
|
389
|
+
SELECT
|
|
390
|
+
section,
|
|
391
|
+
COUNT(*) as totalFiles,
|
|
392
|
+
SUM(CASE WHEN translated_nodes = total_nodes THEN 1 ELSE 0 END) as translatedFiles,
|
|
393
|
+
SUM(total_nodes) as totalNodes,
|
|
394
|
+
SUM(translated_nodes) as translatedNodes
|
|
395
|
+
FROM file_coverage
|
|
396
|
+
GROUP BY section
|
|
397
|
+
ORDER BY section
|
|
398
|
+
`
|
|
399
|
+
).all(lang, version);
|
|
400
|
+
}
|
|
401
|
+
// ── Export / Import ──
|
|
402
|
+
/** Export cache to JSONL format (for backward compatibility / git tracking) */
|
|
403
|
+
exportJsonl(lang, outputPath) {
|
|
404
|
+
const rows = this.db.prepare(
|
|
405
|
+
"SELECT key, value FROM translations WHERE lang = ? ORDER BY key"
|
|
406
|
+
).all(lang);
|
|
407
|
+
const lines = [];
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
const srcs = this.db.prepare(
|
|
410
|
+
"SELECT file || ':' || line as loc FROM source_files WHERE key = ? ORDER BY file, line"
|
|
411
|
+
).all(row.key);
|
|
412
|
+
const entry = { k: row.key, v: row.value };
|
|
413
|
+
if (srcs.length > 0) {
|
|
414
|
+
entry.src = srcs.map((s) => s.loc);
|
|
415
|
+
}
|
|
416
|
+
lines.push(JSON.stringify(entry));
|
|
417
|
+
}
|
|
418
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
419
|
+
fs.writeFileSync(outputPath, `${lines.join("\n")}
|
|
420
|
+
`, "utf8");
|
|
421
|
+
return rows.length;
|
|
422
|
+
}
|
|
423
|
+
/** Import translations from a JSONL file */
|
|
424
|
+
importJsonl(lang, inputPath) {
|
|
425
|
+
if (!fs.existsSync(inputPath)) return 0;
|
|
426
|
+
const lines = fs.readFileSync(inputPath, "utf8").split("\n");
|
|
427
|
+
let count = 0;
|
|
428
|
+
this.db.transaction(() => {
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
const trimmed = line.trim();
|
|
431
|
+
if (!trimmed) continue;
|
|
432
|
+
const entry = JSON.parse(trimmed);
|
|
433
|
+
this.stmts.setTranslation.run(lang, entry.k, entry.v);
|
|
434
|
+
count++;
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
return count;
|
|
438
|
+
}
|
|
439
|
+
/** Export a readable markdown index for IDE navigation */
|
|
440
|
+
exportIndex(lang) {
|
|
441
|
+
const indexPath = path.join(this.cacheDir, `${lang}.index.md`);
|
|
442
|
+
const rows = this.db.prepare(
|
|
443
|
+
`
|
|
444
|
+
SELECT t.key, t.value,
|
|
445
|
+
GROUP_CONCAT(sf.file || ':' || sf.line, char(10)) as sources
|
|
446
|
+
FROM translations t
|
|
447
|
+
LEFT JOIN source_files sf ON sf.key = t.key
|
|
448
|
+
WHERE t.lang = ?
|
|
449
|
+
GROUP BY t.key
|
|
450
|
+
HAVING sources IS NOT NULL
|
|
451
|
+
ORDER BY t.key
|
|
452
|
+
`
|
|
453
|
+
).all(lang);
|
|
454
|
+
const lines = [`# Translation Index (${lang})`, ""];
|
|
455
|
+
for (const row of rows) {
|
|
456
|
+
if (!row.sources) continue;
|
|
457
|
+
const preview = row.value.split("\n")[0].substring(0, 80);
|
|
458
|
+
lines.push(`## \`${row.key}\` ${preview}`);
|
|
459
|
+
lines.push("");
|
|
460
|
+
for (const src of row.sources.split("\n")) {
|
|
461
|
+
const [filePath] = src.split(":");
|
|
462
|
+
lines.push(
|
|
463
|
+
`- [${src}](../content/en/${filePath}#L${src.split(":")[1]})`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
lines.push("");
|
|
467
|
+
}
|
|
468
|
+
fs.writeFileSync(indexPath, lines.join("\n"), "utf8");
|
|
469
|
+
}
|
|
470
|
+
/** Close the database connection */
|
|
471
|
+
close() {
|
|
472
|
+
this._stmts = void 0;
|
|
473
|
+
this.db.close();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/core/frontmatter.ts
|
|
478
|
+
import { parseDocument } from "yaml";
|
|
479
|
+
function getNestedValue(doc, path2) {
|
|
480
|
+
const parts = path2.split(".");
|
|
481
|
+
let current = doc;
|
|
482
|
+
for (const part of parts) {
|
|
483
|
+
if (current == null) return void 0;
|
|
484
|
+
if (typeof current.get === "function") {
|
|
485
|
+
current = current.get(part);
|
|
486
|
+
} else if (typeof current === "object") {
|
|
487
|
+
current = current[part];
|
|
488
|
+
} else {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return current;
|
|
493
|
+
}
|
|
494
|
+
function setNestedValue(doc, path2, value) {
|
|
495
|
+
const parts = path2.split(".");
|
|
496
|
+
if (parts.length === 1) {
|
|
497
|
+
if (doc.has(parts[0])) doc.set(parts[0], value);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
let current = doc;
|
|
501
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
502
|
+
if (typeof current.get === "function") {
|
|
503
|
+
current = current.get(parts[i]);
|
|
504
|
+
} else {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const lastKey = parts[parts.length - 1];
|
|
509
|
+
if (typeof current.set === "function") {
|
|
510
|
+
current.set(lastKey, value);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function extractTranslatableFields(frontmatterText) {
|
|
514
|
+
const yaml = frontmatterText.replace(/^---\n/, "").replace(/\n---$/, "");
|
|
515
|
+
const doc = parseDocument(yaml);
|
|
516
|
+
const fields = {};
|
|
517
|
+
for (const field of FRONTMATTER_TRANSLATABLE_FIELDS) {
|
|
518
|
+
const value = getNestedValue(doc, field);
|
|
519
|
+
if (typeof value === "string" && value.trim()) {
|
|
520
|
+
fields[field] = value;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return fields;
|
|
524
|
+
}
|
|
525
|
+
function reconstructFrontmatter(frontmatterText, translatedFields) {
|
|
526
|
+
const yaml = frontmatterText.replace(/^---\n/, "").replace(/\n---$/, "");
|
|
527
|
+
const doc = parseDocument(yaml);
|
|
528
|
+
for (const [field, value] of Object.entries(translatedFields)) {
|
|
529
|
+
setNestedValue(doc, field, value);
|
|
530
|
+
}
|
|
531
|
+
return `---
|
|
532
|
+
${doc.toString().trimEnd()}
|
|
533
|
+
---`;
|
|
534
|
+
}
|
|
535
|
+
export {
|
|
536
|
+
TranslationCache,
|
|
537
|
+
assemble,
|
|
538
|
+
defineConfig,
|
|
539
|
+
extractTranslatableFields,
|
|
540
|
+
flattenSources,
|
|
541
|
+
loadConfig,
|
|
542
|
+
normalize,
|
|
543
|
+
parseMdx,
|
|
544
|
+
reconstructFrontmatter
|
|
545
|
+
};
|