scribe-cms 0.0.2 → 0.0.5
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 +2 -3
- package/bin/scribe.js +2 -11
- package/dist/cli/index.cjs +1437 -225
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1437 -226
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-translate.d.ts +16 -0
- package/dist/cli/prompt-translate.d.ts.map +1 -0
- package/dist/cli/translate-progress.d.ts +11 -0
- package/dist/cli/translate-progress.d.ts.map +1 -0
- package/dist/index.cjs +591 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +587 -148
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +14 -2
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +14 -2
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/field.d.ts +2 -0
- package/dist/src/core/field.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +5 -1
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/export/build-static-raw-exports.d.ts +37 -0
- package/dist/src/export/build-static-raw-exports.d.ts.map +1 -0
- package/dist/src/export/write-static-raw-exports.d.ts +12 -0
- package/dist/src/export/write-static-raw-exports.d.ts.map +1 -0
- package/dist/src/history/record-revision.d.ts +2 -0
- package/dist/src/history/record-revision.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/storage/translations.d.ts +6 -0
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/gemini-client.d.ts +7 -0
- package/dist/src/translate/gemini-client.d.ts.map +1 -1
- package/dist/src/translate/gemini-models.d.ts +8 -0
- package/dist/src/translate/gemini-models.d.ts.map +1 -0
- package/dist/src/translate/gemini-pricing.d.ts +11 -0
- package/dist/src/translate/gemini-pricing.d.ts.map +1 -0
- package/dist/src/translate/page-translator.d.ts +38 -1
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +1 -2
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/response-schema.d.ts +7 -0
- package/dist/src/translate/response-schema.d.ts.map +1 -0
- package/dist/src/translate/validate-translation.d.ts +14 -0
- package/dist/src/translate/validate-translation.d.ts.map +1 -0
- package/dist/src/translate/worklist.d.ts +3 -0
- package/dist/src/translate/worklist.d.ts.map +1 -1
- package/dist/src/validate/validate-assets.d.ts +20 -0
- package/dist/src/validate/validate-assets.d.ts.map +1 -0
- package/dist/src/validate/validate-project.d.ts +1 -1
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/studio/server.cjs +611 -194
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +611 -194
- package/dist/studio/server.js.map +1 -1
- package/package.json +16 -5
- package/bin/ensure-built.mjs +0 -31
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,22 @@ function unwrapSchema(schema) {
|
|
|
109
109
|
}
|
|
110
110
|
return current;
|
|
111
111
|
}
|
|
112
|
+
function peelOptionalWrappers(schema) {
|
|
113
|
+
let current = schema;
|
|
114
|
+
for (let i = 0; i < 8; i++) {
|
|
115
|
+
const type = current._def?.type;
|
|
116
|
+
if (type === "optional" || type === "nullable") {
|
|
117
|
+
current = current.unwrap();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (type === "default") {
|
|
121
|
+
current = current.removeDefault();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
return current;
|
|
127
|
+
}
|
|
112
128
|
|
|
113
129
|
// src/core/types.ts
|
|
114
130
|
function defineConfig(config) {
|
|
@@ -123,14 +139,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
123
139
|
function isRoutableType(type) {
|
|
124
140
|
return typeof type.path === "string" && type.path.length > 0;
|
|
125
141
|
}
|
|
126
|
-
function assertValidPathTemplate(
|
|
127
|
-
if (!
|
|
128
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
142
|
+
function assertValidPathTemplate(path11, typeId) {
|
|
143
|
+
if (!path11.startsWith("/")) {
|
|
144
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
|
|
129
145
|
}
|
|
130
|
-
const count = (
|
|
146
|
+
const count = (path11.match(/\{slug\}/g) ?? []).length;
|
|
131
147
|
if (count !== 1) {
|
|
132
148
|
throw new Error(
|
|
133
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
149
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
|
|
134
150
|
);
|
|
135
151
|
}
|
|
136
152
|
}
|
|
@@ -182,6 +198,7 @@ function resolveConfig(input, baseDir) {
|
|
|
182
198
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
183
199
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
184
200
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
201
|
+
const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
|
|
185
202
|
const seenIds = /* @__PURE__ */ new Set();
|
|
186
203
|
const types = raw.types.map((type) => {
|
|
187
204
|
if (seenIds.has(type.id)) {
|
|
@@ -202,6 +219,7 @@ function resolveConfig(input, baseDir) {
|
|
|
202
219
|
const config = {
|
|
203
220
|
rootDir: contentRoot,
|
|
204
221
|
storePath,
|
|
222
|
+
assetsPath,
|
|
205
223
|
locales: [...raw.locales],
|
|
206
224
|
defaultLocale,
|
|
207
225
|
localePresets: raw.localePresets,
|
|
@@ -254,10 +272,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
254
272
|
}
|
|
255
273
|
function extractByPaths(data, paths) {
|
|
256
274
|
const out = {};
|
|
257
|
-
for (const
|
|
258
|
-
const value = getAtPath(data,
|
|
275
|
+
for (const path11 of paths) {
|
|
276
|
+
const value = getAtPath(data, path11);
|
|
259
277
|
if (value !== void 0) {
|
|
260
|
-
setAtPath(out,
|
|
278
|
+
setAtPath(out, path11.filter((p) => p !== "*"), value);
|
|
261
279
|
}
|
|
262
280
|
}
|
|
263
281
|
return out;
|
|
@@ -275,13 +293,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
275
293
|
const translatable = pickTranslatable(localeData, schema);
|
|
276
294
|
return deepMerge(structural, translatable);
|
|
277
295
|
}
|
|
278
|
-
function getAtPath(obj,
|
|
296
|
+
function getAtPath(obj, path11) {
|
|
279
297
|
let current = obj;
|
|
280
|
-
for (const segment of
|
|
298
|
+
for (const segment of path11) {
|
|
281
299
|
if (segment === "*") {
|
|
282
300
|
if (!Array.isArray(current)) return void 0;
|
|
283
301
|
return current.map(
|
|
284
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
302
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
|
|
285
303
|
);
|
|
286
304
|
}
|
|
287
305
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -289,13 +307,13 @@ function getAtPath(obj, path9) {
|
|
|
289
307
|
}
|
|
290
308
|
return current;
|
|
291
309
|
}
|
|
292
|
-
function setAtPath(obj,
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
295
|
-
obj[
|
|
310
|
+
function setAtPath(obj, path11, value) {
|
|
311
|
+
if (path11.length === 0) return;
|
|
312
|
+
if (path11.length === 1) {
|
|
313
|
+
obj[path11[0]] = value;
|
|
296
314
|
return;
|
|
297
315
|
}
|
|
298
|
-
const [head, ...rest] =
|
|
316
|
+
const [head, ...rest] = path11;
|
|
299
317
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
300
318
|
obj[head] = {};
|
|
301
319
|
}
|
|
@@ -491,7 +509,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
491
509
|
redirectTo: enDoc.redirectTo
|
|
492
510
|
};
|
|
493
511
|
}
|
|
494
|
-
var SCHEMA_VERSION =
|
|
512
|
+
var SCHEMA_VERSION = 3;
|
|
495
513
|
var MIGRATIONS = [
|
|
496
514
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
497
515
|
key TEXT PRIMARY KEY,
|
|
@@ -548,17 +566,27 @@ function resolveStorePath(config) {
|
|
|
548
566
|
}
|
|
549
567
|
function openStore(config, mode = "readwrite") {
|
|
550
568
|
const storePath = resolveStorePath(config);
|
|
551
|
-
|
|
569
|
+
if (mode === "readwrite") {
|
|
570
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
|
|
571
|
+
}
|
|
552
572
|
const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
|
|
553
573
|
if (mode === "readwrite") {
|
|
554
574
|
migrate(db);
|
|
555
575
|
}
|
|
556
576
|
return db;
|
|
557
577
|
}
|
|
578
|
+
function addColumnIfMissing(db, table, column, ddlType) {
|
|
579
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
580
|
+
if (!columns.some((c) => c.name === column)) {
|
|
581
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
558
584
|
function migrate(db) {
|
|
559
585
|
for (const sql of MIGRATIONS) {
|
|
560
586
|
db.exec(sql);
|
|
561
587
|
}
|
|
588
|
+
addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
|
|
589
|
+
addColumnIfMissing(db, "revisions", "body", "TEXT");
|
|
562
590
|
db.prepare(
|
|
563
591
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
564
592
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -609,8 +637,9 @@ function bulkLoadTranslations(db) {
|
|
|
609
637
|
function appendRevision(db, input) {
|
|
610
638
|
const result = db.prepare(
|
|
611
639
|
`INSERT INTO revisions (
|
|
612
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
613
|
-
|
|
640
|
+
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
641
|
+
model, body_preview, frontmatter_json, body
|
|
642
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
614
643
|
).run(
|
|
615
644
|
input.contentType,
|
|
616
645
|
input.enSlug,
|
|
@@ -620,7 +649,9 @@ function appendRevision(db, input) {
|
|
|
620
649
|
input.bodyHash,
|
|
621
650
|
input.createdAt,
|
|
622
651
|
input.model ?? null,
|
|
623
|
-
input.bodyPreview ?? null
|
|
652
|
+
input.bodyPreview ?? null,
|
|
653
|
+
input.frontmatterJson ?? null,
|
|
654
|
+
input.body ?? null
|
|
624
655
|
);
|
|
625
656
|
return Number(result.lastInsertRowid);
|
|
626
657
|
}
|
|
@@ -1553,8 +1584,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1553
1584
|
// src/sitemap/join-base-url.ts
|
|
1554
1585
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1555
1586
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1556
|
-
const
|
|
1557
|
-
return `${origin}${
|
|
1587
|
+
const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1588
|
+
return `${origin}${path11}`;
|
|
1558
1589
|
}
|
|
1559
1590
|
|
|
1560
1591
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1698,34 +1729,6 @@ function loadConfigSync(options = {}) {
|
|
|
1698
1729
|
}
|
|
1699
1730
|
return resolveConfig(config, path3__default.default.dirname(configPath));
|
|
1700
1731
|
}
|
|
1701
|
-
function sha256(input) {
|
|
1702
|
-
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
1703
|
-
}
|
|
1704
|
-
function computePageEnHash(translatableFrontmatter, body) {
|
|
1705
|
-
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1706
|
-
return sha256(payload);
|
|
1707
|
-
}
|
|
1708
|
-
function computeBodyHash(body) {
|
|
1709
|
-
return sha256(body);
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
// src/history/record-revision.ts
|
|
1713
|
-
function recordRevision(config, input) {
|
|
1714
|
-
const db = openStore(config, "readwrite");
|
|
1715
|
-
const id = appendRevision(db, {
|
|
1716
|
-
contentType: input.contentType,
|
|
1717
|
-
enSlug: input.enSlug,
|
|
1718
|
-
locale: input.locale,
|
|
1719
|
-
revisionKind: input.revisionKind,
|
|
1720
|
-
enHash: input.enHash,
|
|
1721
|
-
bodyHash: computeBodyHash(input.body),
|
|
1722
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1723
|
-
model: input.model,
|
|
1724
|
-
bodyPreview: input.body.slice(0, 200)
|
|
1725
|
-
});
|
|
1726
|
-
db.close();
|
|
1727
|
-
return id;
|
|
1728
|
-
}
|
|
1729
1732
|
function getAtPath2(obj, fieldPath) {
|
|
1730
1733
|
let current = obj;
|
|
1731
1734
|
for (const segment of fieldPath) {
|
|
@@ -1805,6 +1808,72 @@ function validateRelations(project) {
|
|
|
1805
1808
|
}
|
|
1806
1809
|
return issues;
|
|
1807
1810
|
}
|
|
1811
|
+
var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
|
|
1812
|
+
var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
|
|
1813
|
+
var MARKDOWN_IMAGE_RE = new RegExp(
|
|
1814
|
+
String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
|
|
1815
|
+
"gi"
|
|
1816
|
+
);
|
|
1817
|
+
var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
|
|
1818
|
+
function isImageWebPath(value) {
|
|
1819
|
+
return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
|
|
1820
|
+
}
|
|
1821
|
+
function isSiteAssetPath(webPath) {
|
|
1822
|
+
const segment = webPath.split("/")[1];
|
|
1823
|
+
return Boolean(segment && !segment.includes("."));
|
|
1824
|
+
}
|
|
1825
|
+
function addImagePath(webPath, out) {
|
|
1826
|
+
if (isSiteAssetPath(webPath)) out.add(webPath);
|
|
1827
|
+
}
|
|
1828
|
+
function collectBodyImagePaths(body, out) {
|
|
1829
|
+
for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
|
|
1830
|
+
for (const match of body.matchAll(re)) {
|
|
1831
|
+
addImagePath(match[1], out);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function collectFrontmatterImagePaths(value, out) {
|
|
1836
|
+
if (typeof value === "string") {
|
|
1837
|
+
if (isImageWebPath(value)) addImagePath(value, out);
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (Array.isArray(value)) {
|
|
1841
|
+
for (const item of value) collectFrontmatterImagePaths(item, out);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (value && typeof value === "object") {
|
|
1845
|
+
for (const nested of Object.values(value)) {
|
|
1846
|
+
collectFrontmatterImagePaths(nested, out);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function collectImagePaths(frontmatter, body) {
|
|
1851
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1852
|
+
collectFrontmatterImagePaths(frontmatter, paths);
|
|
1853
|
+
collectBodyImagePaths(body, paths);
|
|
1854
|
+
return [...paths].sort();
|
|
1855
|
+
}
|
|
1856
|
+
function assetFilePath(assetsPath, webPath) {
|
|
1857
|
+
return path3__default.default.join(assetsPath, webPath.replace(/^\//, ""));
|
|
1858
|
+
}
|
|
1859
|
+
function validateDocumentAssets(config, input) {
|
|
1860
|
+
const assetsPath = config.assetsPath;
|
|
1861
|
+
if (!assetsPath) return [];
|
|
1862
|
+
const issues = [];
|
|
1863
|
+
for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
|
|
1864
|
+
const filePath = assetFilePath(assetsPath, webPath);
|
|
1865
|
+
if (fs2__default.default.existsSync(filePath)) continue;
|
|
1866
|
+
issues.push({
|
|
1867
|
+
level: "warning",
|
|
1868
|
+
contentType: input.contentType,
|
|
1869
|
+
enSlug: input.enSlug,
|
|
1870
|
+
locale: input.locale,
|
|
1871
|
+
field: "asset",
|
|
1872
|
+
message: `Missing image asset ${webPath} (expected ${filePath})`
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
return issues;
|
|
1876
|
+
}
|
|
1808
1877
|
|
|
1809
1878
|
// src/validate/validate-slug-suffix.ts
|
|
1810
1879
|
function validateTranslationSlugSuffixes(config, db) {
|
|
@@ -1867,8 +1936,6 @@ function validateProject(config) {
|
|
|
1867
1936
|
});
|
|
1868
1937
|
continue;
|
|
1869
1938
|
}
|
|
1870
|
-
const payload = getTranslatablePayload(enDoc, type);
|
|
1871
|
-
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
1872
1939
|
const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
|
|
1873
1940
|
locale: config.defaultLocale,
|
|
1874
1941
|
defaultLocale: config.defaultLocale,
|
|
@@ -1886,28 +1953,18 @@ function validateProject(config) {
|
|
|
1886
1953
|
message: issue.message
|
|
1887
1954
|
});
|
|
1888
1955
|
}
|
|
1956
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1957
|
+
contentType: type.id,
|
|
1958
|
+
enSlug,
|
|
1959
|
+
frontmatter: enDoc.frontmatter,
|
|
1960
|
+
body: enDoc.content
|
|
1961
|
+
})) {
|
|
1962
|
+
issues.push(issue);
|
|
1963
|
+
}
|
|
1889
1964
|
for (const locale of config.locales) {
|
|
1890
1965
|
if (locale === config.defaultLocale) continue;
|
|
1891
1966
|
const row = getTranslation(db, type.id, enSlug, locale);
|
|
1892
1967
|
if (!row) continue;
|
|
1893
|
-
if (row.en_hash !== currentEnHash) {
|
|
1894
|
-
issues.push({
|
|
1895
|
-
level: "warning",
|
|
1896
|
-
contentType: type.id,
|
|
1897
|
-
enSlug,
|
|
1898
|
-
locale,
|
|
1899
|
-
field: "en_hash",
|
|
1900
|
-
message: "Translation is stale (en_hash mismatch)"
|
|
1901
|
-
});
|
|
1902
|
-
recordRevision(config, {
|
|
1903
|
-
contentType: type.id,
|
|
1904
|
-
enSlug,
|
|
1905
|
-
locale,
|
|
1906
|
-
revisionKind: "en_edit_detected",
|
|
1907
|
-
enHash: currentEnHash,
|
|
1908
|
-
body: row.body
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
1968
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
1912
1969
|
for (const issue of validateLocaleBuiltinFields(localeFm)) {
|
|
1913
1970
|
issues.push({
|
|
@@ -1919,6 +1976,15 @@ function validateProject(config) {
|
|
|
1919
1976
|
message: issue.message
|
|
1920
1977
|
});
|
|
1921
1978
|
}
|
|
1979
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1980
|
+
contentType: type.id,
|
|
1981
|
+
enSlug,
|
|
1982
|
+
locale,
|
|
1983
|
+
frontmatter: localeFm,
|
|
1984
|
+
body: row.body
|
|
1985
|
+
})) {
|
|
1986
|
+
issues.push(issue);
|
|
1987
|
+
}
|
|
1922
1988
|
}
|
|
1923
1989
|
}
|
|
1924
1990
|
try {
|
|
@@ -1986,6 +2052,18 @@ function validateProject(config) {
|
|
|
1986
2052
|
}
|
|
1987
2053
|
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
1988
2054
|
}
|
|
2055
|
+
function sha256(input) {
|
|
2056
|
+
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
2057
|
+
}
|
|
2058
|
+
function computePageEnHash(translatableFrontmatter, body) {
|
|
2059
|
+
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2060
|
+
return sha256(payload);
|
|
2061
|
+
}
|
|
2062
|
+
function computeBodyHash(body) {
|
|
2063
|
+
return sha256(body);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/translate/worklist.ts
|
|
1989
2067
|
function listEnSlugs3(rootDir, contentDir) {
|
|
1990
2068
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1991
2069
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
@@ -2030,6 +2108,10 @@ function buildWorklist(config, options = {}) {
|
|
|
2030
2108
|
}
|
|
2031
2109
|
}
|
|
2032
2110
|
db.close();
|
|
2111
|
+
const strategy = options.strategy ?? "all";
|
|
2112
|
+
if (strategy === "missing-only") {
|
|
2113
|
+
return items.filter((item) => item.reason === "missing");
|
|
2114
|
+
}
|
|
2033
2115
|
return items;
|
|
2034
2116
|
}
|
|
2035
2117
|
function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
@@ -2039,7 +2121,50 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2039
2121
|
}
|
|
2040
2122
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2041
2123
|
}
|
|
2042
|
-
|
|
2124
|
+
|
|
2125
|
+
// src/history/record-revision.ts
|
|
2126
|
+
function recordRevision(config, input) {
|
|
2127
|
+
const db = openStore(config, "readwrite");
|
|
2128
|
+
const id = appendRevision(db, {
|
|
2129
|
+
contentType: input.contentType,
|
|
2130
|
+
enSlug: input.enSlug,
|
|
2131
|
+
locale: input.locale,
|
|
2132
|
+
revisionKind: input.revisionKind,
|
|
2133
|
+
enHash: input.enHash,
|
|
2134
|
+
bodyHash: computeBodyHash(input.body),
|
|
2135
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2136
|
+
model: input.model,
|
|
2137
|
+
bodyPreview: input.body.slice(0, 200),
|
|
2138
|
+
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
2139
|
+
body: input.body
|
|
2140
|
+
});
|
|
2141
|
+
db.close();
|
|
2142
|
+
return id;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/translate/gemini-models.ts
|
|
2146
|
+
var GEMINI_MODEL_IDS = {
|
|
2147
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
2148
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview"
|
|
2149
|
+
};
|
|
2150
|
+
var DEFAULT_GEMINI_MODEL = "gemini-3.1-pro";
|
|
2151
|
+
function stripModelsPrefix(model) {
|
|
2152
|
+
return model.replace(/^models\//, "");
|
|
2153
|
+
}
|
|
2154
|
+
function resolveGeminiModelId(model) {
|
|
2155
|
+
const name = stripModelsPrefix(model);
|
|
2156
|
+
return GEMINI_MODEL_IDS[name] ?? name;
|
|
2157
|
+
}
|
|
2158
|
+
function normalizeGeminiDisplayName(model) {
|
|
2159
|
+
const name = stripModelsPrefix(model);
|
|
2160
|
+
if (name in GEMINI_MODEL_IDS) return name;
|
|
2161
|
+
const alias = Object.entries(GEMINI_MODEL_IDS).find(([, apiId]) => apiId === name);
|
|
2162
|
+
if (alias) return alias[0];
|
|
2163
|
+
return name;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/translate/gemini-client.ts
|
|
2167
|
+
var DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
2043
2168
|
function extractJson(text) {
|
|
2044
2169
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
2045
2170
|
if (fenced?.[1]) return fenced[1].trim();
|
|
@@ -2053,13 +2178,17 @@ async function translatePageWithGemini(input) {
|
|
|
2053
2178
|
if (!apiKey) {
|
|
2054
2179
|
throw new Error("GEMINI_API_KEY is required for scribe translate");
|
|
2055
2180
|
}
|
|
2056
|
-
const
|
|
2181
|
+
const displayModel = normalizeGeminiDisplayName(
|
|
2182
|
+
input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL
|
|
2183
|
+
);
|
|
2184
|
+
const apiModel = resolveGeminiModelId(displayModel);
|
|
2057
2185
|
const ai = new genai.GoogleGenAI({ apiKey });
|
|
2058
2186
|
const response = await ai.models.generateContent({
|
|
2059
|
-
model,
|
|
2187
|
+
model: apiModel,
|
|
2060
2188
|
contents: input.prompt,
|
|
2061
2189
|
config: {
|
|
2062
|
-
responseMimeType: "application/json"
|
|
2190
|
+
responseMimeType: "application/json",
|
|
2191
|
+
...input.responseSchema ? { responseSchema: input.responseSchema } : {}
|
|
2063
2192
|
}
|
|
2064
2193
|
});
|
|
2065
2194
|
const raw = response.text ?? "";
|
|
@@ -2067,7 +2196,38 @@ async function translatePageWithGemini(input) {
|
|
|
2067
2196
|
if (!parsed.frontmatter || typeof parsed.body !== "string") {
|
|
2068
2197
|
throw new Error("Gemini response missing frontmatter/body");
|
|
2069
2198
|
}
|
|
2070
|
-
|
|
2199
|
+
const usageMetadata = response.usageMetadata;
|
|
2200
|
+
const usage = {
|
|
2201
|
+
inputTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
2202
|
+
outputTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
2203
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0
|
|
2204
|
+
};
|
|
2205
|
+
return { model: displayModel, raw, parsed, usage };
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/translate/gemini-pricing.ts
|
|
2209
|
+
var CONTEXT_TIER_TOKENS = 2e5;
|
|
2210
|
+
var GEMINI_PRICING_USD = {
|
|
2211
|
+
"gemini-3.1-pro": { input: [2, 4], output: [12, 18] }
|
|
2212
|
+
};
|
|
2213
|
+
function normalizeModelId(model) {
|
|
2214
|
+
return model.replace(/^models\//, "").toLowerCase();
|
|
2215
|
+
}
|
|
2216
|
+
function tierRate(tokens, tiers) {
|
|
2217
|
+
return tokens <= CONTEXT_TIER_TOKENS ? tiers[0] : tiers[1];
|
|
2218
|
+
}
|
|
2219
|
+
function resolveModelPricing(model) {
|
|
2220
|
+
const id = normalizeModelId(model);
|
|
2221
|
+
if (GEMINI_PRICING_USD[id]) return GEMINI_PRICING_USD[id];
|
|
2222
|
+
const match = Object.entries(GEMINI_PRICING_USD).find(([key]) => id.includes(key));
|
|
2223
|
+
return match?.[1];
|
|
2224
|
+
}
|
|
2225
|
+
function estimateTranslationCostUsd(model, inputTokens, outputTokens) {
|
|
2226
|
+
const pricing = resolveModelPricing(model);
|
|
2227
|
+
if (!pricing) return void 0;
|
|
2228
|
+
const inputRate = tierRate(inputTokens, pricing.input);
|
|
2229
|
+
const outputRate = tierRate(outputTokens, pricing.output);
|
|
2230
|
+
return inputTokens / 1e6 * inputRate + outputTokens / 1e6 * outputRate;
|
|
2071
2231
|
}
|
|
2072
2232
|
|
|
2073
2233
|
// src/translate/prompts/translation-prompt.ts
|
|
@@ -2103,6 +2263,7 @@ function buildPageTranslationPrompt(input) {
|
|
|
2103
2263
|
prompt,
|
|
2104
2264
|
"",
|
|
2105
2265
|
...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
|
|
2266
|
+
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2106
2267
|
"## Rules",
|
|
2107
2268
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2108
2269
|
"",
|
|
@@ -2110,10 +2271,6 @@ function buildPageTranslationPrompt(input) {
|
|
|
2110
2271
|
"Return ONLY valid JSON with keys:",
|
|
2111
2272
|
input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
|
|
2112
2273
|
"",
|
|
2113
|
-
"## EN metadata",
|
|
2114
|
-
`title: ${input.enTitle}`,
|
|
2115
|
-
`description: ${input.enDescription}`,
|
|
2116
|
-
"",
|
|
2117
2274
|
"## EN translatable frontmatter (JSON)",
|
|
2118
2275
|
JSON.stringify(input.translatableFrontmatter, null, 2),
|
|
2119
2276
|
"",
|
|
@@ -2122,6 +2279,71 @@ function buildPageTranslationPrompt(input) {
|
|
|
2122
2279
|
];
|
|
2123
2280
|
return lines.join("\n");
|
|
2124
2281
|
}
|
|
2282
|
+
function getObjectShape(schema) {
|
|
2283
|
+
const base = peelOptionalWrappers(schema);
|
|
2284
|
+
if (base instanceof Object && "shape" in base) {
|
|
2285
|
+
return base.shape;
|
|
2286
|
+
}
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
function getArraySchema(schema) {
|
|
2290
|
+
const base = peelOptionalWrappers(schema);
|
|
2291
|
+
if (base instanceof Object && "element" in base && base._def?.type === "array") {
|
|
2292
|
+
return base;
|
|
2293
|
+
}
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
function extractTranslatableFromStructural(schema) {
|
|
2297
|
+
const arraySchema = getArraySchema(schema);
|
|
2298
|
+
if (arraySchema) {
|
|
2299
|
+
const inner = buildTranslatableSubschema(arraySchema.element);
|
|
2300
|
+
if (inner) return zod.z.array(inner);
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
if (getObjectShape(schema)) {
|
|
2304
|
+
return buildTranslatableSubschema(schema);
|
|
2305
|
+
}
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
function buildTranslatableSubschema(schema) {
|
|
2309
|
+
const shape = getObjectShape(schema);
|
|
2310
|
+
if (!shape) return null;
|
|
2311
|
+
const out = {};
|
|
2312
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2313
|
+
const childSchema = child;
|
|
2314
|
+
const kind = getFieldKind(childSchema);
|
|
2315
|
+
if (kind === "translatable") {
|
|
2316
|
+
out[key] = peelOptionalWrappers(childSchema);
|
|
2317
|
+
} else if (kind === "structural") {
|
|
2318
|
+
const nested = extractTranslatableFromStructural(childSchema);
|
|
2319
|
+
if (nested) out[key] = nested;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
if (Object.keys(out).length === 0) return null;
|
|
2323
|
+
return zod.z.object(out);
|
|
2324
|
+
}
|
|
2325
|
+
function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
2326
|
+
try {
|
|
2327
|
+
const translatable = buildTranslatableSubschema(schema);
|
|
2328
|
+
if (!translatable) return null;
|
|
2329
|
+
const responseShape = {
|
|
2330
|
+
frontmatter: translatable,
|
|
2331
|
+
body: zod.z.string()
|
|
2332
|
+
};
|
|
2333
|
+
if (slugStrategy === "localized") {
|
|
2334
|
+
responseShape.slug = zod.z.string();
|
|
2335
|
+
}
|
|
2336
|
+
const jsonSchema = zod.z.toJSONSchema(zod.z.object(responseShape), {
|
|
2337
|
+
target: "draft-2020-12",
|
|
2338
|
+
unrepresentable: "any",
|
|
2339
|
+
io: "output"
|
|
2340
|
+
});
|
|
2341
|
+
delete jsonSchema.$schema;
|
|
2342
|
+
return jsonSchema;
|
|
2343
|
+
} catch {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2125
2347
|
|
|
2126
2348
|
// src/translate/resolve-translate-config.ts
|
|
2127
2349
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
@@ -2162,107 +2384,329 @@ function resolveTranslateConfig(config, type) {
|
|
|
2162
2384
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2163
2385
|
}
|
|
2164
2386
|
|
|
2387
|
+
// src/translate/validate-translation.ts
|
|
2388
|
+
function formatZodIssues(error) {
|
|
2389
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2390
|
+
}
|
|
2391
|
+
function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
|
|
2392
|
+
return pickTranslatable(rawFrontmatter, schema);
|
|
2393
|
+
}
|
|
2394
|
+
function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
|
|
2395
|
+
const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
|
|
2396
|
+
const merged = mergeStructuralOntoLocale(
|
|
2397
|
+
frontmatter,
|
|
2398
|
+
enDoc.frontmatter,
|
|
2399
|
+
typeSchema
|
|
2400
|
+
);
|
|
2401
|
+
const parsed = typeSchema.safeParse(merged);
|
|
2402
|
+
if (!parsed.success) {
|
|
2403
|
+
return { ok: false, error: formatZodIssues(parsed.error) };
|
|
2404
|
+
}
|
|
2405
|
+
return { ok: true, frontmatter };
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2165
2408
|
// src/translate/page-translator.ts
|
|
2409
|
+
function summarizeResults(results, durationMs) {
|
|
2410
|
+
return results.reduce(
|
|
2411
|
+
(totals, result) => {
|
|
2412
|
+
if (result.failed) totals.failed += 1;
|
|
2413
|
+
else if (result.skipped) totals.skipped += 1;
|
|
2414
|
+
else totals.translated += 1;
|
|
2415
|
+
totals.inputTokens += result.usage?.inputTokens ?? 0;
|
|
2416
|
+
totals.outputTokens += result.usage?.outputTokens ?? 0;
|
|
2417
|
+
totals.estimatedCostUsd += result.estimatedCostUsd ?? 0;
|
|
2418
|
+
return totals;
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
translated: 0,
|
|
2422
|
+
skipped: 0,
|
|
2423
|
+
failed: 0,
|
|
2424
|
+
inputTokens: 0,
|
|
2425
|
+
outputTokens: 0,
|
|
2426
|
+
estimatedCostUsd: 0,
|
|
2427
|
+
durationMs
|
|
2428
|
+
}
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
function formatTranslateError(error) {
|
|
2432
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2433
|
+
try {
|
|
2434
|
+
const parsed = JSON.parse(message);
|
|
2435
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
2436
|
+
} catch {
|
|
2437
|
+
}
|
|
2438
|
+
const embedded = message.match(/"message"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
2439
|
+
if (embedded?.[1]) {
|
|
2440
|
+
return embedded[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
2441
|
+
}
|
|
2442
|
+
return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
|
|
2443
|
+
}
|
|
2444
|
+
async function runPool(items, concurrency, worker) {
|
|
2445
|
+
if (items.length === 0) return;
|
|
2446
|
+
let nextIndex = 0;
|
|
2447
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
2448
|
+
while (nextIndex < items.length) {
|
|
2449
|
+
const index = nextIndex++;
|
|
2450
|
+
await worker(items[index], index);
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
await Promise.all(runners);
|
|
2454
|
+
}
|
|
2455
|
+
function resolveContextLabel(enDoc, enSlug) {
|
|
2456
|
+
const fm = enDoc.frontmatter;
|
|
2457
|
+
for (const key of ["title", "name", "h1"]) {
|
|
2458
|
+
const value = fm[key];
|
|
2459
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2460
|
+
}
|
|
2461
|
+
return enSlug;
|
|
2462
|
+
}
|
|
2166
2463
|
async function translatePage(config, item, options = {}) {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2464
|
+
const startedAt = Date.now();
|
|
2465
|
+
const base = {
|
|
2466
|
+
contentType: item.contentType,
|
|
2467
|
+
enSlug: item.enSlug,
|
|
2468
|
+
locale: item.locale
|
|
2469
|
+
};
|
|
2470
|
+
try {
|
|
2471
|
+
const type = config.types.find((t) => t.id === item.contentType);
|
|
2472
|
+
if (!type) throw new Error(`Unknown content type ${item.contentType}`);
|
|
2473
|
+
const enDoc = readEnDocument(config, type, item.enSlug);
|
|
2474
|
+
if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
|
|
2475
|
+
const payload = getTranslatablePayload(enDoc, type);
|
|
2476
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2477
|
+
const db = openStore(config, "readonly");
|
|
2478
|
+
const existing = getTranslation(db, type.id, item.enSlug, item.locale);
|
|
2479
|
+
db.close();
|
|
2480
|
+
if (!options.force && existing && existing.en_hash === currentEnHash) {
|
|
2481
|
+
return {
|
|
2482
|
+
...base,
|
|
2483
|
+
skipped: true,
|
|
2484
|
+
reason: "fresh",
|
|
2485
|
+
durationMs: Date.now() - startedAt
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
const resolvedTranslate = resolveTranslateConfig(config, type);
|
|
2489
|
+
const model = options.model ?? resolvedTranslate.model;
|
|
2490
|
+
const prompt = buildPageTranslationPrompt({
|
|
2491
|
+
resolved: resolvedTranslate,
|
|
2492
|
+
targetLocale: item.locale,
|
|
2493
|
+
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2494
|
+
translatableFrontmatter: payload.frontmatter,
|
|
2495
|
+
enBody: payload.body,
|
|
2496
|
+
slugStrategy: type.slugStrategy
|
|
2497
|
+
});
|
|
2498
|
+
if (options.dryRun) {
|
|
2499
|
+
return {
|
|
2500
|
+
...base,
|
|
2501
|
+
skipped: false,
|
|
2502
|
+
model,
|
|
2503
|
+
durationMs: Date.now() - startedAt
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2507
|
+
const result = await translatePageWithGemini({
|
|
2508
|
+
prompt,
|
|
2509
|
+
model,
|
|
2510
|
+
responseSchema: responseSchema ?? void 0
|
|
2511
|
+
});
|
|
2512
|
+
const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2513
|
+
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2514
|
+
if (!validated.ok) {
|
|
2515
|
+
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2516
|
+
}
|
|
2517
|
+
const writeDb = openStore(config, "readwrite");
|
|
2518
|
+
upsertTranslation(writeDb, {
|
|
2519
|
+
contentType: type.id,
|
|
2179
2520
|
enSlug: item.enSlug,
|
|
2180
2521
|
locale: item.locale,
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
translatableFrontmatter: payload.frontmatter,
|
|
2192
|
-
enBody: payload.body,
|
|
2193
|
-
slugStrategy: type.slugStrategy
|
|
2194
|
-
});
|
|
2195
|
-
if (options.dryRun) {
|
|
2196
|
-
return {
|
|
2197
|
-
contentType: item.contentType,
|
|
2522
|
+
slug,
|
|
2523
|
+
frontmatter: validated.frontmatter,
|
|
2524
|
+
body: result.parsed.body,
|
|
2525
|
+
enHash: currentEnHash,
|
|
2526
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2527
|
+
model: result.model
|
|
2528
|
+
});
|
|
2529
|
+
writeDb.close();
|
|
2530
|
+
recordRevision(config, {
|
|
2531
|
+
contentType: type.id,
|
|
2198
2532
|
enSlug: item.enSlug,
|
|
2199
2533
|
locale: item.locale,
|
|
2534
|
+
revisionKind: "translation",
|
|
2535
|
+
enHash: currentEnHash,
|
|
2536
|
+
body: result.parsed.body,
|
|
2537
|
+
frontmatter: validated.frontmatter,
|
|
2538
|
+
model: result.model
|
|
2539
|
+
});
|
|
2540
|
+
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2541
|
+
normalizeGeminiDisplayName(result.model),
|
|
2542
|
+
result.usage.inputTokens,
|
|
2543
|
+
result.usage.outputTokens
|
|
2544
|
+
);
|
|
2545
|
+
return {
|
|
2546
|
+
...base,
|
|
2200
2547
|
skipped: false,
|
|
2201
|
-
model:
|
|
2548
|
+
model: result.model,
|
|
2549
|
+
usage: result.usage,
|
|
2550
|
+
estimatedCostUsd,
|
|
2551
|
+
durationMs: Date.now() - startedAt
|
|
2552
|
+
};
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
return {
|
|
2555
|
+
...base,
|
|
2556
|
+
skipped: false,
|
|
2557
|
+
failed: true,
|
|
2558
|
+
error: formatTranslateError(error),
|
|
2559
|
+
durationMs: Date.now() - startedAt
|
|
2202
2560
|
};
|
|
2203
2561
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
model: result.model
|
|
2562
|
+
}
|
|
2563
|
+
function labelForItem(item) {
|
|
2564
|
+
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
2565
|
+
}
|
|
2566
|
+
async function translateWorklist(config, items, options = {}) {
|
|
2567
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2568
|
+
const startedAt = Date.now();
|
|
2569
|
+
const results = new Array(items.length);
|
|
2570
|
+
const active = /* @__PURE__ */ new Set();
|
|
2571
|
+
options.onProgress?.({
|
|
2572
|
+
type: "start",
|
|
2573
|
+
total: items.length,
|
|
2574
|
+
concurrency,
|
|
2575
|
+
dryRun: Boolean(options.dryRun),
|
|
2576
|
+
model: options.model
|
|
2220
2577
|
});
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
model: result.model
|
|
2578
|
+
await runPool(items, concurrency, async (item, index) => {
|
|
2579
|
+
const label = labelForItem(item);
|
|
2580
|
+
active.add(label);
|
|
2581
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
2582
|
+
const result = await translatePage(config, item, options);
|
|
2583
|
+
results[index] = result;
|
|
2584
|
+
active.delete(label);
|
|
2585
|
+
options.onProgress?.({ type: "item-done", result });
|
|
2230
2586
|
});
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
locale: item.locale,
|
|
2235
|
-
skipped: false,
|
|
2236
|
-
model: result.model
|
|
2237
|
-
};
|
|
2587
|
+
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
2588
|
+
options.onProgress?.({ type: "done", results, totals });
|
|
2589
|
+
return results;
|
|
2238
2590
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2591
|
+
function serializeMdx(frontmatter, content) {
|
|
2592
|
+
return matter__default.default.stringify(content, frontmatter);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/export/build-static-raw-exports.ts
|
|
2596
|
+
function normalizeExtension(ext) {
|
|
2597
|
+
return ext.startsWith(".") ? ext : `.${ext}`;
|
|
2598
|
+
}
|
|
2599
|
+
function exportDirSegment(pathTemplate) {
|
|
2600
|
+
const prefix = pathPrefix(pathTemplate);
|
|
2601
|
+
return prefix.replace(/^\/+|\/+$/g, "");
|
|
2602
|
+
}
|
|
2603
|
+
function getStaticExportRoots(project, options = {}) {
|
|
2604
|
+
const { config } = project;
|
|
2605
|
+
const locales = options.locales ?? config.locales;
|
|
2606
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2607
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2608
|
+
for (const type of project.listTypes()) {
|
|
2609
|
+
if (!isRoutableType(type.config)) continue;
|
|
2610
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2611
|
+
const segment = exportDirSegment(type.config.path);
|
|
2612
|
+
roots.add(`${segment}/`);
|
|
2613
|
+
for (const locale of locales) {
|
|
2614
|
+
if (locale === config.defaultLocale) continue;
|
|
2615
|
+
roots.add(`${locale}/${segment}/`);
|
|
2616
|
+
}
|
|
2243
2617
|
}
|
|
2244
|
-
return
|
|
2618
|
+
return [...roots].sort();
|
|
2619
|
+
}
|
|
2620
|
+
function buildStaticRawExports(project, options = {}) {
|
|
2621
|
+
const { config } = project;
|
|
2622
|
+
const extension = normalizeExtension(options.extension ?? ".mdx");
|
|
2623
|
+
const locales = options.locales ?? config.locales;
|
|
2624
|
+
const excludeRedirected = options.excludeRedirected ?? true;
|
|
2625
|
+
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2626
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2627
|
+
const out = [];
|
|
2628
|
+
for (const type of project.listTypes()) {
|
|
2629
|
+
if (!isRoutableType(type.config)) continue;
|
|
2630
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2631
|
+
const pathTemplate = type.config.path;
|
|
2632
|
+
const all = type.load();
|
|
2633
|
+
const enIdx = all.get(config.defaultLocale);
|
|
2634
|
+
if (!enIdx) continue;
|
|
2635
|
+
for (const locale of locales) {
|
|
2636
|
+
for (const enDoc of enIdx.bySlug.values()) {
|
|
2637
|
+
const resolved = type.resolve(enDoc.slug, locale);
|
|
2638
|
+
if (!resolved.document) continue;
|
|
2639
|
+
if (excludeRedirected && resolved.shouldRedirectTo) continue;
|
|
2640
|
+
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2641
|
+
const doc = resolved.document;
|
|
2642
|
+
const slugWithExt = `${doc.slug}${extension}`;
|
|
2643
|
+
const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
|
|
2644
|
+
out.push({
|
|
2645
|
+
relativePath: urlPath.slice(1),
|
|
2646
|
+
urlPath,
|
|
2647
|
+
locale,
|
|
2648
|
+
typeId: type.id,
|
|
2649
|
+
enSlug: doc.enSlug,
|
|
2650
|
+
source: serializeMdx(doc.frontmatter, doc.content)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return out;
|
|
2656
|
+
}
|
|
2657
|
+
function rmDirIfExists(dir) {
|
|
2658
|
+
if (fs2__default.default.existsSync(dir)) {
|
|
2659
|
+
fs2__default.default.rmSync(dir, { recursive: true, force: true });
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function writeStaticRawExports(project, options = {}) {
|
|
2663
|
+
const outDir = path3__default.default.resolve(options.outDir ?? "public");
|
|
2664
|
+
const typeFilter = options.types;
|
|
2665
|
+
for (const root of getStaticExportRoots(project, {
|
|
2666
|
+
types: typeFilter,
|
|
2667
|
+
locales: options.locales
|
|
2668
|
+
})) {
|
|
2669
|
+
rmDirIfExists(path3__default.default.join(outDir, root));
|
|
2670
|
+
}
|
|
2671
|
+
const exports = buildStaticRawExports(project, options);
|
|
2672
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2673
|
+
for (const item of exports) {
|
|
2674
|
+
const filePath = path3__default.default.join(outDir, item.relativePath);
|
|
2675
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(filePath), { recursive: true });
|
|
2676
|
+
fs2__default.default.writeFileSync(filePath, item.source, "utf8");
|
|
2677
|
+
const key = `${item.typeId}/${item.locale}`;
|
|
2678
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2679
|
+
}
|
|
2680
|
+
for (const [key, count] of [...counts.entries()].sort()) {
|
|
2681
|
+
console.log(` ${key}: ${count}`);
|
|
2682
|
+
}
|
|
2683
|
+
return { exports, written: exports.length };
|
|
2245
2684
|
}
|
|
2246
2685
|
|
|
2247
2686
|
exports.buildAllContentRedirects = buildAllContentRedirects;
|
|
2687
|
+
exports.buildStaticRawExports = buildStaticRawExports;
|
|
2248
2688
|
exports.buildWorklist = buildWorklist;
|
|
2249
2689
|
exports.createProject = createProject;
|
|
2250
2690
|
exports.createScribe = createScribe;
|
|
2251
2691
|
exports.defineConfig = defineConfig;
|
|
2252
2692
|
exports.defineContentType = defineContentType;
|
|
2693
|
+
exports.exportDirSegment = exportDirSegment;
|
|
2253
2694
|
exports.field = field;
|
|
2254
2695
|
exports.findConfigPath = findConfigPath;
|
|
2255
2696
|
exports.generateSitemap = generateSitemap;
|
|
2256
2697
|
exports.getFieldKind = getFieldKind;
|
|
2257
2698
|
exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
|
|
2258
2699
|
exports.getRelationTarget = getRelationTarget;
|
|
2700
|
+
exports.getStaticExportRoots = getStaticExportRoots;
|
|
2259
2701
|
exports.isResolvedConfig = isResolvedConfig;
|
|
2260
2702
|
exports.loadConfigSync = loadConfigSync;
|
|
2261
2703
|
exports.resolveConfig = resolveConfig;
|
|
2262
2704
|
exports.resolveLocalesFromPreset = resolveLocalesFromPreset;
|
|
2705
|
+
exports.serializeMdx = serializeMdx;
|
|
2263
2706
|
exports.translatePage = translatePage;
|
|
2264
2707
|
exports.translateWorklist = translateWorklist;
|
|
2265
2708
|
exports.unwrapSchema = unwrapSchema;
|
|
2266
2709
|
exports.validateProject = validateProject;
|
|
2710
|
+
exports.writeStaticRawExports = writeStaticRawExports;
|
|
2267
2711
|
//# sourceMappingURL=index.cjs.map
|
|
2268
2712
|
//# sourceMappingURL=index.cjs.map
|