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.js
CHANGED
|
@@ -98,6 +98,22 @@ function unwrapSchema(schema) {
|
|
|
98
98
|
}
|
|
99
99
|
return current;
|
|
100
100
|
}
|
|
101
|
+
function peelOptionalWrappers(schema) {
|
|
102
|
+
let current = schema;
|
|
103
|
+
for (let i = 0; i < 8; i++) {
|
|
104
|
+
const type = current._def?.type;
|
|
105
|
+
if (type === "optional" || type === "nullable") {
|
|
106
|
+
current = current.unwrap();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (type === "default") {
|
|
110
|
+
current = current.removeDefault();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
101
117
|
|
|
102
118
|
// src/core/types.ts
|
|
103
119
|
function defineConfig(config) {
|
|
@@ -112,14 +128,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
112
128
|
function isRoutableType(type) {
|
|
113
129
|
return typeof type.path === "string" && type.path.length > 0;
|
|
114
130
|
}
|
|
115
|
-
function assertValidPathTemplate(
|
|
116
|
-
if (!
|
|
117
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
131
|
+
function assertValidPathTemplate(path11, typeId) {
|
|
132
|
+
if (!path11.startsWith("/")) {
|
|
133
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
|
|
118
134
|
}
|
|
119
|
-
const count = (
|
|
135
|
+
const count = (path11.match(/\{slug\}/g) ?? []).length;
|
|
120
136
|
if (count !== 1) {
|
|
121
137
|
throw new Error(
|
|
122
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
138
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
|
|
123
139
|
);
|
|
124
140
|
}
|
|
125
141
|
}
|
|
@@ -171,6 +187,7 @@ function resolveConfig(input, baseDir) {
|
|
|
171
187
|
const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
172
188
|
const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
|
|
173
189
|
const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
190
|
+
const assetsPath = raw.assetsDir ? path3.resolve(projectRoot, raw.assetsDir) : void 0;
|
|
174
191
|
const seenIds = /* @__PURE__ */ new Set();
|
|
175
192
|
const types = raw.types.map((type) => {
|
|
176
193
|
if (seenIds.has(type.id)) {
|
|
@@ -191,6 +208,7 @@ function resolveConfig(input, baseDir) {
|
|
|
191
208
|
const config = {
|
|
192
209
|
rootDir: contentRoot,
|
|
193
210
|
storePath,
|
|
211
|
+
assetsPath,
|
|
194
212
|
locales: [...raw.locales],
|
|
195
213
|
defaultLocale,
|
|
196
214
|
localePresets: raw.localePresets,
|
|
@@ -243,10 +261,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
243
261
|
}
|
|
244
262
|
function extractByPaths(data, paths) {
|
|
245
263
|
const out = {};
|
|
246
|
-
for (const
|
|
247
|
-
const value = getAtPath(data,
|
|
264
|
+
for (const path11 of paths) {
|
|
265
|
+
const value = getAtPath(data, path11);
|
|
248
266
|
if (value !== void 0) {
|
|
249
|
-
setAtPath(out,
|
|
267
|
+
setAtPath(out, path11.filter((p) => p !== "*"), value);
|
|
250
268
|
}
|
|
251
269
|
}
|
|
252
270
|
return out;
|
|
@@ -264,13 +282,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
264
282
|
const translatable = pickTranslatable(localeData, schema);
|
|
265
283
|
return deepMerge(structural, translatable);
|
|
266
284
|
}
|
|
267
|
-
function getAtPath(obj,
|
|
285
|
+
function getAtPath(obj, path11) {
|
|
268
286
|
let current = obj;
|
|
269
|
-
for (const segment of
|
|
287
|
+
for (const segment of path11) {
|
|
270
288
|
if (segment === "*") {
|
|
271
289
|
if (!Array.isArray(current)) return void 0;
|
|
272
290
|
return current.map(
|
|
273
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
291
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
|
|
274
292
|
);
|
|
275
293
|
}
|
|
276
294
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -278,13 +296,13 @@ function getAtPath(obj, path9) {
|
|
|
278
296
|
}
|
|
279
297
|
return current;
|
|
280
298
|
}
|
|
281
|
-
function setAtPath(obj,
|
|
282
|
-
if (
|
|
283
|
-
if (
|
|
284
|
-
obj[
|
|
299
|
+
function setAtPath(obj, path11, value) {
|
|
300
|
+
if (path11.length === 0) return;
|
|
301
|
+
if (path11.length === 1) {
|
|
302
|
+
obj[path11[0]] = value;
|
|
285
303
|
return;
|
|
286
304
|
}
|
|
287
|
-
const [head, ...rest] =
|
|
305
|
+
const [head, ...rest] = path11;
|
|
288
306
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
289
307
|
obj[head] = {};
|
|
290
308
|
}
|
|
@@ -480,7 +498,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
480
498
|
redirectTo: enDoc.redirectTo
|
|
481
499
|
};
|
|
482
500
|
}
|
|
483
|
-
var SCHEMA_VERSION =
|
|
501
|
+
var SCHEMA_VERSION = 3;
|
|
484
502
|
var MIGRATIONS = [
|
|
485
503
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
486
504
|
key TEXT PRIMARY KEY,
|
|
@@ -537,17 +555,27 @@ function resolveStorePath(config) {
|
|
|
537
555
|
}
|
|
538
556
|
function openStore(config, mode = "readwrite") {
|
|
539
557
|
const storePath = resolveStorePath(config);
|
|
540
|
-
|
|
558
|
+
if (mode === "readwrite") {
|
|
559
|
+
fs2.mkdirSync(path3.dirname(storePath), { recursive: true });
|
|
560
|
+
}
|
|
541
561
|
const db = new Database(storePath, { readonly: mode === "readonly" });
|
|
542
562
|
if (mode === "readwrite") {
|
|
543
563
|
migrate(db);
|
|
544
564
|
}
|
|
545
565
|
return db;
|
|
546
566
|
}
|
|
567
|
+
function addColumnIfMissing(db, table, column, ddlType) {
|
|
568
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
569
|
+
if (!columns.some((c) => c.name === column)) {
|
|
570
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
547
573
|
function migrate(db) {
|
|
548
574
|
for (const sql of MIGRATIONS) {
|
|
549
575
|
db.exec(sql);
|
|
550
576
|
}
|
|
577
|
+
addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
|
|
578
|
+
addColumnIfMissing(db, "revisions", "body", "TEXT");
|
|
551
579
|
db.prepare(
|
|
552
580
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
553
581
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -598,8 +626,9 @@ function bulkLoadTranslations(db) {
|
|
|
598
626
|
function appendRevision(db, input) {
|
|
599
627
|
const result = db.prepare(
|
|
600
628
|
`INSERT INTO revisions (
|
|
601
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
602
|
-
|
|
629
|
+
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
630
|
+
model, body_preview, frontmatter_json, body
|
|
631
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
603
632
|
).run(
|
|
604
633
|
input.contentType,
|
|
605
634
|
input.enSlug,
|
|
@@ -609,7 +638,9 @@ function appendRevision(db, input) {
|
|
|
609
638
|
input.bodyHash,
|
|
610
639
|
input.createdAt,
|
|
611
640
|
input.model ?? null,
|
|
612
|
-
input.bodyPreview ?? null
|
|
641
|
+
input.bodyPreview ?? null,
|
|
642
|
+
input.frontmatterJson ?? null,
|
|
643
|
+
input.body ?? null
|
|
613
644
|
);
|
|
614
645
|
return Number(result.lastInsertRowid);
|
|
615
646
|
}
|
|
@@ -1542,8 +1573,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1542
1573
|
// src/sitemap/join-base-url.ts
|
|
1543
1574
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1544
1575
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1545
|
-
const
|
|
1546
|
-
return `${origin}${
|
|
1576
|
+
const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1577
|
+
return `${origin}${path11}`;
|
|
1547
1578
|
}
|
|
1548
1579
|
|
|
1549
1580
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1687,34 +1718,6 @@ function loadConfigSync(options = {}) {
|
|
|
1687
1718
|
}
|
|
1688
1719
|
return resolveConfig(config, path3.dirname(configPath));
|
|
1689
1720
|
}
|
|
1690
|
-
function sha256(input) {
|
|
1691
|
-
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
1692
|
-
}
|
|
1693
|
-
function computePageEnHash(translatableFrontmatter, body) {
|
|
1694
|
-
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1695
|
-
return sha256(payload);
|
|
1696
|
-
}
|
|
1697
|
-
function computeBodyHash(body) {
|
|
1698
|
-
return sha256(body);
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// src/history/record-revision.ts
|
|
1702
|
-
function recordRevision(config, input) {
|
|
1703
|
-
const db = openStore(config, "readwrite");
|
|
1704
|
-
const id = appendRevision(db, {
|
|
1705
|
-
contentType: input.contentType,
|
|
1706
|
-
enSlug: input.enSlug,
|
|
1707
|
-
locale: input.locale,
|
|
1708
|
-
revisionKind: input.revisionKind,
|
|
1709
|
-
enHash: input.enHash,
|
|
1710
|
-
bodyHash: computeBodyHash(input.body),
|
|
1711
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1712
|
-
model: input.model,
|
|
1713
|
-
bodyPreview: input.body.slice(0, 200)
|
|
1714
|
-
});
|
|
1715
|
-
db.close();
|
|
1716
|
-
return id;
|
|
1717
|
-
}
|
|
1718
1721
|
function getAtPath2(obj, fieldPath) {
|
|
1719
1722
|
let current = obj;
|
|
1720
1723
|
for (const segment of fieldPath) {
|
|
@@ -1794,6 +1797,72 @@ function validateRelations(project) {
|
|
|
1794
1797
|
}
|
|
1795
1798
|
return issues;
|
|
1796
1799
|
}
|
|
1800
|
+
var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
|
|
1801
|
+
var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
|
|
1802
|
+
var MARKDOWN_IMAGE_RE = new RegExp(
|
|
1803
|
+
String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
|
|
1804
|
+
"gi"
|
|
1805
|
+
);
|
|
1806
|
+
var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
|
|
1807
|
+
function isImageWebPath(value) {
|
|
1808
|
+
return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
|
|
1809
|
+
}
|
|
1810
|
+
function isSiteAssetPath(webPath) {
|
|
1811
|
+
const segment = webPath.split("/")[1];
|
|
1812
|
+
return Boolean(segment && !segment.includes("."));
|
|
1813
|
+
}
|
|
1814
|
+
function addImagePath(webPath, out) {
|
|
1815
|
+
if (isSiteAssetPath(webPath)) out.add(webPath);
|
|
1816
|
+
}
|
|
1817
|
+
function collectBodyImagePaths(body, out) {
|
|
1818
|
+
for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
|
|
1819
|
+
for (const match of body.matchAll(re)) {
|
|
1820
|
+
addImagePath(match[1], out);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function collectFrontmatterImagePaths(value, out) {
|
|
1825
|
+
if (typeof value === "string") {
|
|
1826
|
+
if (isImageWebPath(value)) addImagePath(value, out);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (Array.isArray(value)) {
|
|
1830
|
+
for (const item of value) collectFrontmatterImagePaths(item, out);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (value && typeof value === "object") {
|
|
1834
|
+
for (const nested of Object.values(value)) {
|
|
1835
|
+
collectFrontmatterImagePaths(nested, out);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function collectImagePaths(frontmatter, body) {
|
|
1840
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1841
|
+
collectFrontmatterImagePaths(frontmatter, paths);
|
|
1842
|
+
collectBodyImagePaths(body, paths);
|
|
1843
|
+
return [...paths].sort();
|
|
1844
|
+
}
|
|
1845
|
+
function assetFilePath(assetsPath, webPath) {
|
|
1846
|
+
return path3.join(assetsPath, webPath.replace(/^\//, ""));
|
|
1847
|
+
}
|
|
1848
|
+
function validateDocumentAssets(config, input) {
|
|
1849
|
+
const assetsPath = config.assetsPath;
|
|
1850
|
+
if (!assetsPath) return [];
|
|
1851
|
+
const issues = [];
|
|
1852
|
+
for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
|
|
1853
|
+
const filePath = assetFilePath(assetsPath, webPath);
|
|
1854
|
+
if (fs2.existsSync(filePath)) continue;
|
|
1855
|
+
issues.push({
|
|
1856
|
+
level: "warning",
|
|
1857
|
+
contentType: input.contentType,
|
|
1858
|
+
enSlug: input.enSlug,
|
|
1859
|
+
locale: input.locale,
|
|
1860
|
+
field: "asset",
|
|
1861
|
+
message: `Missing image asset ${webPath} (expected ${filePath})`
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
return issues;
|
|
1865
|
+
}
|
|
1797
1866
|
|
|
1798
1867
|
// src/validate/validate-slug-suffix.ts
|
|
1799
1868
|
function validateTranslationSlugSuffixes(config, db) {
|
|
@@ -1856,8 +1925,6 @@ function validateProject(config) {
|
|
|
1856
1925
|
});
|
|
1857
1926
|
continue;
|
|
1858
1927
|
}
|
|
1859
|
-
const payload = getTranslatablePayload(enDoc, type);
|
|
1860
|
-
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
1861
1928
|
const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
|
|
1862
1929
|
locale: config.defaultLocale,
|
|
1863
1930
|
defaultLocale: config.defaultLocale,
|
|
@@ -1875,28 +1942,18 @@ function validateProject(config) {
|
|
|
1875
1942
|
message: issue.message
|
|
1876
1943
|
});
|
|
1877
1944
|
}
|
|
1945
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1946
|
+
contentType: type.id,
|
|
1947
|
+
enSlug,
|
|
1948
|
+
frontmatter: enDoc.frontmatter,
|
|
1949
|
+
body: enDoc.content
|
|
1950
|
+
})) {
|
|
1951
|
+
issues.push(issue);
|
|
1952
|
+
}
|
|
1878
1953
|
for (const locale of config.locales) {
|
|
1879
1954
|
if (locale === config.defaultLocale) continue;
|
|
1880
1955
|
const row = getTranslation(db, type.id, enSlug, locale);
|
|
1881
1956
|
if (!row) continue;
|
|
1882
|
-
if (row.en_hash !== currentEnHash) {
|
|
1883
|
-
issues.push({
|
|
1884
|
-
level: "warning",
|
|
1885
|
-
contentType: type.id,
|
|
1886
|
-
enSlug,
|
|
1887
|
-
locale,
|
|
1888
|
-
field: "en_hash",
|
|
1889
|
-
message: "Translation is stale (en_hash mismatch)"
|
|
1890
|
-
});
|
|
1891
|
-
recordRevision(config, {
|
|
1892
|
-
contentType: type.id,
|
|
1893
|
-
enSlug,
|
|
1894
|
-
locale,
|
|
1895
|
-
revisionKind: "en_edit_detected",
|
|
1896
|
-
enHash: currentEnHash,
|
|
1897
|
-
body: row.body
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
1957
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
1901
1958
|
for (const issue of validateLocaleBuiltinFields(localeFm)) {
|
|
1902
1959
|
issues.push({
|
|
@@ -1908,6 +1965,15 @@ function validateProject(config) {
|
|
|
1908
1965
|
message: issue.message
|
|
1909
1966
|
});
|
|
1910
1967
|
}
|
|
1968
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1969
|
+
contentType: type.id,
|
|
1970
|
+
enSlug,
|
|
1971
|
+
locale,
|
|
1972
|
+
frontmatter: localeFm,
|
|
1973
|
+
body: row.body
|
|
1974
|
+
})) {
|
|
1975
|
+
issues.push(issue);
|
|
1976
|
+
}
|
|
1911
1977
|
}
|
|
1912
1978
|
}
|
|
1913
1979
|
try {
|
|
@@ -1975,6 +2041,18 @@ function validateProject(config) {
|
|
|
1975
2041
|
}
|
|
1976
2042
|
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
1977
2043
|
}
|
|
2044
|
+
function sha256(input) {
|
|
2045
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
2046
|
+
}
|
|
2047
|
+
function computePageEnHash(translatableFrontmatter, body) {
|
|
2048
|
+
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2049
|
+
return sha256(payload);
|
|
2050
|
+
}
|
|
2051
|
+
function computeBodyHash(body) {
|
|
2052
|
+
return sha256(body);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// src/translate/worklist.ts
|
|
1978
2056
|
function listEnSlugs3(rootDir, contentDir) {
|
|
1979
2057
|
const dir = path3.join(rootDir, contentDir);
|
|
1980
2058
|
if (!fs2.existsSync(dir)) return [];
|
|
@@ -2019,6 +2097,10 @@ function buildWorklist(config, options = {}) {
|
|
|
2019
2097
|
}
|
|
2020
2098
|
}
|
|
2021
2099
|
db.close();
|
|
2100
|
+
const strategy = options.strategy ?? "all";
|
|
2101
|
+
if (strategy === "missing-only") {
|
|
2102
|
+
return items.filter((item) => item.reason === "missing");
|
|
2103
|
+
}
|
|
2022
2104
|
return items;
|
|
2023
2105
|
}
|
|
2024
2106
|
function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
@@ -2028,7 +2110,50 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2028
2110
|
}
|
|
2029
2111
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2030
2112
|
}
|
|
2031
|
-
|
|
2113
|
+
|
|
2114
|
+
// src/history/record-revision.ts
|
|
2115
|
+
function recordRevision(config, input) {
|
|
2116
|
+
const db = openStore(config, "readwrite");
|
|
2117
|
+
const id = appendRevision(db, {
|
|
2118
|
+
contentType: input.contentType,
|
|
2119
|
+
enSlug: input.enSlug,
|
|
2120
|
+
locale: input.locale,
|
|
2121
|
+
revisionKind: input.revisionKind,
|
|
2122
|
+
enHash: input.enHash,
|
|
2123
|
+
bodyHash: computeBodyHash(input.body),
|
|
2124
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2125
|
+
model: input.model,
|
|
2126
|
+
bodyPreview: input.body.slice(0, 200),
|
|
2127
|
+
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
2128
|
+
body: input.body
|
|
2129
|
+
});
|
|
2130
|
+
db.close();
|
|
2131
|
+
return id;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/translate/gemini-models.ts
|
|
2135
|
+
var GEMINI_MODEL_IDS = {
|
|
2136
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
2137
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview"
|
|
2138
|
+
};
|
|
2139
|
+
var DEFAULT_GEMINI_MODEL = "gemini-3.1-pro";
|
|
2140
|
+
function stripModelsPrefix(model) {
|
|
2141
|
+
return model.replace(/^models\//, "");
|
|
2142
|
+
}
|
|
2143
|
+
function resolveGeminiModelId(model) {
|
|
2144
|
+
const name = stripModelsPrefix(model);
|
|
2145
|
+
return GEMINI_MODEL_IDS[name] ?? name;
|
|
2146
|
+
}
|
|
2147
|
+
function normalizeGeminiDisplayName(model) {
|
|
2148
|
+
const name = stripModelsPrefix(model);
|
|
2149
|
+
if (name in GEMINI_MODEL_IDS) return name;
|
|
2150
|
+
const alias = Object.entries(GEMINI_MODEL_IDS).find(([, apiId]) => apiId === name);
|
|
2151
|
+
if (alias) return alias[0];
|
|
2152
|
+
return name;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/translate/gemini-client.ts
|
|
2156
|
+
var DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
2032
2157
|
function extractJson(text) {
|
|
2033
2158
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
2034
2159
|
if (fenced?.[1]) return fenced[1].trim();
|
|
@@ -2042,13 +2167,17 @@ async function translatePageWithGemini(input) {
|
|
|
2042
2167
|
if (!apiKey) {
|
|
2043
2168
|
throw new Error("GEMINI_API_KEY is required for scribe translate");
|
|
2044
2169
|
}
|
|
2045
|
-
const
|
|
2170
|
+
const displayModel = normalizeGeminiDisplayName(
|
|
2171
|
+
input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL
|
|
2172
|
+
);
|
|
2173
|
+
const apiModel = resolveGeminiModelId(displayModel);
|
|
2046
2174
|
const ai = new GoogleGenAI({ apiKey });
|
|
2047
2175
|
const response = await ai.models.generateContent({
|
|
2048
|
-
model,
|
|
2176
|
+
model: apiModel,
|
|
2049
2177
|
contents: input.prompt,
|
|
2050
2178
|
config: {
|
|
2051
|
-
responseMimeType: "application/json"
|
|
2179
|
+
responseMimeType: "application/json",
|
|
2180
|
+
...input.responseSchema ? { responseSchema: input.responseSchema } : {}
|
|
2052
2181
|
}
|
|
2053
2182
|
});
|
|
2054
2183
|
const raw = response.text ?? "";
|
|
@@ -2056,7 +2185,38 @@ async function translatePageWithGemini(input) {
|
|
|
2056
2185
|
if (!parsed.frontmatter || typeof parsed.body !== "string") {
|
|
2057
2186
|
throw new Error("Gemini response missing frontmatter/body");
|
|
2058
2187
|
}
|
|
2059
|
-
|
|
2188
|
+
const usageMetadata = response.usageMetadata;
|
|
2189
|
+
const usage = {
|
|
2190
|
+
inputTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
2191
|
+
outputTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
2192
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0
|
|
2193
|
+
};
|
|
2194
|
+
return { model: displayModel, raw, parsed, usage };
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// src/translate/gemini-pricing.ts
|
|
2198
|
+
var CONTEXT_TIER_TOKENS = 2e5;
|
|
2199
|
+
var GEMINI_PRICING_USD = {
|
|
2200
|
+
"gemini-3.1-pro": { input: [2, 4], output: [12, 18] }
|
|
2201
|
+
};
|
|
2202
|
+
function normalizeModelId(model) {
|
|
2203
|
+
return model.replace(/^models\//, "").toLowerCase();
|
|
2204
|
+
}
|
|
2205
|
+
function tierRate(tokens, tiers) {
|
|
2206
|
+
return tokens <= CONTEXT_TIER_TOKENS ? tiers[0] : tiers[1];
|
|
2207
|
+
}
|
|
2208
|
+
function resolveModelPricing(model) {
|
|
2209
|
+
const id = normalizeModelId(model);
|
|
2210
|
+
if (GEMINI_PRICING_USD[id]) return GEMINI_PRICING_USD[id];
|
|
2211
|
+
const match = Object.entries(GEMINI_PRICING_USD).find(([key]) => id.includes(key));
|
|
2212
|
+
return match?.[1];
|
|
2213
|
+
}
|
|
2214
|
+
function estimateTranslationCostUsd(model, inputTokens, outputTokens) {
|
|
2215
|
+
const pricing = resolveModelPricing(model);
|
|
2216
|
+
if (!pricing) return void 0;
|
|
2217
|
+
const inputRate = tierRate(inputTokens, pricing.input);
|
|
2218
|
+
const outputRate = tierRate(outputTokens, pricing.output);
|
|
2219
|
+
return inputTokens / 1e6 * inputRate + outputTokens / 1e6 * outputRate;
|
|
2060
2220
|
}
|
|
2061
2221
|
|
|
2062
2222
|
// src/translate/prompts/translation-prompt.ts
|
|
@@ -2092,6 +2252,7 @@ function buildPageTranslationPrompt(input) {
|
|
|
2092
2252
|
prompt,
|
|
2093
2253
|
"",
|
|
2094
2254
|
...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
|
|
2255
|
+
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2095
2256
|
"## Rules",
|
|
2096
2257
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2097
2258
|
"",
|
|
@@ -2099,10 +2260,6 @@ function buildPageTranslationPrompt(input) {
|
|
|
2099
2260
|
"Return ONLY valid JSON with keys:",
|
|
2100
2261
|
input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
|
|
2101
2262
|
"",
|
|
2102
|
-
"## EN metadata",
|
|
2103
|
-
`title: ${input.enTitle}`,
|
|
2104
|
-
`description: ${input.enDescription}`,
|
|
2105
|
-
"",
|
|
2106
2263
|
"## EN translatable frontmatter (JSON)",
|
|
2107
2264
|
JSON.stringify(input.translatableFrontmatter, null, 2),
|
|
2108
2265
|
"",
|
|
@@ -2111,6 +2268,71 @@ function buildPageTranslationPrompt(input) {
|
|
|
2111
2268
|
];
|
|
2112
2269
|
return lines.join("\n");
|
|
2113
2270
|
}
|
|
2271
|
+
function getObjectShape(schema) {
|
|
2272
|
+
const base = peelOptionalWrappers(schema);
|
|
2273
|
+
if (base instanceof Object && "shape" in base) {
|
|
2274
|
+
return base.shape;
|
|
2275
|
+
}
|
|
2276
|
+
return null;
|
|
2277
|
+
}
|
|
2278
|
+
function getArraySchema(schema) {
|
|
2279
|
+
const base = peelOptionalWrappers(schema);
|
|
2280
|
+
if (base instanceof Object && "element" in base && base._def?.type === "array") {
|
|
2281
|
+
return base;
|
|
2282
|
+
}
|
|
2283
|
+
return null;
|
|
2284
|
+
}
|
|
2285
|
+
function extractTranslatableFromStructural(schema) {
|
|
2286
|
+
const arraySchema = getArraySchema(schema);
|
|
2287
|
+
if (arraySchema) {
|
|
2288
|
+
const inner = buildTranslatableSubschema(arraySchema.element);
|
|
2289
|
+
if (inner) return z.array(inner);
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2292
|
+
if (getObjectShape(schema)) {
|
|
2293
|
+
return buildTranslatableSubschema(schema);
|
|
2294
|
+
}
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
function buildTranslatableSubschema(schema) {
|
|
2298
|
+
const shape = getObjectShape(schema);
|
|
2299
|
+
if (!shape) return null;
|
|
2300
|
+
const out = {};
|
|
2301
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2302
|
+
const childSchema = child;
|
|
2303
|
+
const kind = getFieldKind(childSchema);
|
|
2304
|
+
if (kind === "translatable") {
|
|
2305
|
+
out[key] = peelOptionalWrappers(childSchema);
|
|
2306
|
+
} else if (kind === "structural") {
|
|
2307
|
+
const nested = extractTranslatableFromStructural(childSchema);
|
|
2308
|
+
if (nested) out[key] = nested;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (Object.keys(out).length === 0) return null;
|
|
2312
|
+
return z.object(out);
|
|
2313
|
+
}
|
|
2314
|
+
function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
2315
|
+
try {
|
|
2316
|
+
const translatable = buildTranslatableSubschema(schema);
|
|
2317
|
+
if (!translatable) return null;
|
|
2318
|
+
const responseShape = {
|
|
2319
|
+
frontmatter: translatable,
|
|
2320
|
+
body: z.string()
|
|
2321
|
+
};
|
|
2322
|
+
if (slugStrategy === "localized") {
|
|
2323
|
+
responseShape.slug = z.string();
|
|
2324
|
+
}
|
|
2325
|
+
const jsonSchema = z.toJSONSchema(z.object(responseShape), {
|
|
2326
|
+
target: "draft-2020-12",
|
|
2327
|
+
unrepresentable: "any",
|
|
2328
|
+
io: "output"
|
|
2329
|
+
});
|
|
2330
|
+
delete jsonSchema.$schema;
|
|
2331
|
+
return jsonSchema;
|
|
2332
|
+
} catch {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2114
2336
|
|
|
2115
2337
|
// src/translate/resolve-translate-config.ts
|
|
2116
2338
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
@@ -2151,88 +2373,305 @@ function resolveTranslateConfig(config, type) {
|
|
|
2151
2373
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2152
2374
|
}
|
|
2153
2375
|
|
|
2376
|
+
// src/translate/validate-translation.ts
|
|
2377
|
+
function formatZodIssues(error) {
|
|
2378
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2379
|
+
}
|
|
2380
|
+
function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
|
|
2381
|
+
return pickTranslatable(rawFrontmatter, schema);
|
|
2382
|
+
}
|
|
2383
|
+
function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
|
|
2384
|
+
const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
|
|
2385
|
+
const merged = mergeStructuralOntoLocale(
|
|
2386
|
+
frontmatter,
|
|
2387
|
+
enDoc.frontmatter,
|
|
2388
|
+
typeSchema
|
|
2389
|
+
);
|
|
2390
|
+
const parsed = typeSchema.safeParse(merged);
|
|
2391
|
+
if (!parsed.success) {
|
|
2392
|
+
return { ok: false, error: formatZodIssues(parsed.error) };
|
|
2393
|
+
}
|
|
2394
|
+
return { ok: true, frontmatter };
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2154
2397
|
// src/translate/page-translator.ts
|
|
2398
|
+
function summarizeResults(results, durationMs) {
|
|
2399
|
+
return results.reduce(
|
|
2400
|
+
(totals, result) => {
|
|
2401
|
+
if (result.failed) totals.failed += 1;
|
|
2402
|
+
else if (result.skipped) totals.skipped += 1;
|
|
2403
|
+
else totals.translated += 1;
|
|
2404
|
+
totals.inputTokens += result.usage?.inputTokens ?? 0;
|
|
2405
|
+
totals.outputTokens += result.usage?.outputTokens ?? 0;
|
|
2406
|
+
totals.estimatedCostUsd += result.estimatedCostUsd ?? 0;
|
|
2407
|
+
return totals;
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
translated: 0,
|
|
2411
|
+
skipped: 0,
|
|
2412
|
+
failed: 0,
|
|
2413
|
+
inputTokens: 0,
|
|
2414
|
+
outputTokens: 0,
|
|
2415
|
+
estimatedCostUsd: 0,
|
|
2416
|
+
durationMs
|
|
2417
|
+
}
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
function formatTranslateError(error) {
|
|
2421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2422
|
+
try {
|
|
2423
|
+
const parsed = JSON.parse(message);
|
|
2424
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
const embedded = message.match(/"message"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
2428
|
+
if (embedded?.[1]) {
|
|
2429
|
+
return embedded[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
2430
|
+
}
|
|
2431
|
+
return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
|
|
2432
|
+
}
|
|
2433
|
+
async function runPool(items, concurrency, worker) {
|
|
2434
|
+
if (items.length === 0) return;
|
|
2435
|
+
let nextIndex = 0;
|
|
2436
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
2437
|
+
while (nextIndex < items.length) {
|
|
2438
|
+
const index = nextIndex++;
|
|
2439
|
+
await worker(items[index], index);
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
await Promise.all(runners);
|
|
2443
|
+
}
|
|
2444
|
+
function resolveContextLabel(enDoc, enSlug) {
|
|
2445
|
+
const fm = enDoc.frontmatter;
|
|
2446
|
+
for (const key of ["title", "name", "h1"]) {
|
|
2447
|
+
const value = fm[key];
|
|
2448
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2449
|
+
}
|
|
2450
|
+
return enSlug;
|
|
2451
|
+
}
|
|
2155
2452
|
async function translatePage(config, item, options = {}) {
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2453
|
+
const startedAt = Date.now();
|
|
2454
|
+
const base = {
|
|
2455
|
+
contentType: item.contentType,
|
|
2456
|
+
enSlug: item.enSlug,
|
|
2457
|
+
locale: item.locale
|
|
2458
|
+
};
|
|
2459
|
+
try {
|
|
2460
|
+
const type = config.types.find((t) => t.id === item.contentType);
|
|
2461
|
+
if (!type) throw new Error(`Unknown content type ${item.contentType}`);
|
|
2462
|
+
const enDoc = readEnDocument(config, type, item.enSlug);
|
|
2463
|
+
if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
|
|
2464
|
+
const payload = getTranslatablePayload(enDoc, type);
|
|
2465
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2466
|
+
const db = openStore(config, "readonly");
|
|
2467
|
+
const existing = getTranslation(db, type.id, item.enSlug, item.locale);
|
|
2468
|
+
db.close();
|
|
2469
|
+
if (!options.force && existing && existing.en_hash === currentEnHash) {
|
|
2470
|
+
return {
|
|
2471
|
+
...base,
|
|
2472
|
+
skipped: true,
|
|
2473
|
+
reason: "fresh",
|
|
2474
|
+
durationMs: Date.now() - startedAt
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
const resolvedTranslate = resolveTranslateConfig(config, type);
|
|
2478
|
+
const model = options.model ?? resolvedTranslate.model;
|
|
2479
|
+
const prompt = buildPageTranslationPrompt({
|
|
2480
|
+
resolved: resolvedTranslate,
|
|
2481
|
+
targetLocale: item.locale,
|
|
2482
|
+
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2483
|
+
translatableFrontmatter: payload.frontmatter,
|
|
2484
|
+
enBody: payload.body,
|
|
2485
|
+
slugStrategy: type.slugStrategy
|
|
2486
|
+
});
|
|
2487
|
+
if (options.dryRun) {
|
|
2488
|
+
return {
|
|
2489
|
+
...base,
|
|
2490
|
+
skipped: false,
|
|
2491
|
+
model,
|
|
2492
|
+
durationMs: Date.now() - startedAt
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2496
|
+
const result = await translatePageWithGemini({
|
|
2497
|
+
prompt,
|
|
2498
|
+
model,
|
|
2499
|
+
responseSchema: responseSchema ?? void 0
|
|
2500
|
+
});
|
|
2501
|
+
const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2502
|
+
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2503
|
+
if (!validated.ok) {
|
|
2504
|
+
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2505
|
+
}
|
|
2506
|
+
const writeDb = openStore(config, "readwrite");
|
|
2507
|
+
upsertTranslation(writeDb, {
|
|
2508
|
+
contentType: type.id,
|
|
2168
2509
|
enSlug: item.enSlug,
|
|
2169
2510
|
locale: item.locale,
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
translatableFrontmatter: payload.frontmatter,
|
|
2181
|
-
enBody: payload.body,
|
|
2182
|
-
slugStrategy: type.slugStrategy
|
|
2183
|
-
});
|
|
2184
|
-
if (options.dryRun) {
|
|
2185
|
-
return {
|
|
2186
|
-
contentType: item.contentType,
|
|
2511
|
+
slug,
|
|
2512
|
+
frontmatter: validated.frontmatter,
|
|
2513
|
+
body: result.parsed.body,
|
|
2514
|
+
enHash: currentEnHash,
|
|
2515
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2516
|
+
model: result.model
|
|
2517
|
+
});
|
|
2518
|
+
writeDb.close();
|
|
2519
|
+
recordRevision(config, {
|
|
2520
|
+
contentType: type.id,
|
|
2187
2521
|
enSlug: item.enSlug,
|
|
2188
2522
|
locale: item.locale,
|
|
2523
|
+
revisionKind: "translation",
|
|
2524
|
+
enHash: currentEnHash,
|
|
2525
|
+
body: result.parsed.body,
|
|
2526
|
+
frontmatter: validated.frontmatter,
|
|
2527
|
+
model: result.model
|
|
2528
|
+
});
|
|
2529
|
+
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2530
|
+
normalizeGeminiDisplayName(result.model),
|
|
2531
|
+
result.usage.inputTokens,
|
|
2532
|
+
result.usage.outputTokens
|
|
2533
|
+
);
|
|
2534
|
+
return {
|
|
2535
|
+
...base,
|
|
2189
2536
|
skipped: false,
|
|
2190
|
-
model:
|
|
2537
|
+
model: result.model,
|
|
2538
|
+
usage: result.usage,
|
|
2539
|
+
estimatedCostUsd,
|
|
2540
|
+
durationMs: Date.now() - startedAt
|
|
2541
|
+
};
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
return {
|
|
2544
|
+
...base,
|
|
2545
|
+
skipped: false,
|
|
2546
|
+
failed: true,
|
|
2547
|
+
error: formatTranslateError(error),
|
|
2548
|
+
durationMs: Date.now() - startedAt
|
|
2191
2549
|
};
|
|
2192
2550
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
model: result.model
|
|
2551
|
+
}
|
|
2552
|
+
function labelForItem(item) {
|
|
2553
|
+
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
2554
|
+
}
|
|
2555
|
+
async function translateWorklist(config, items, options = {}) {
|
|
2556
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2557
|
+
const startedAt = Date.now();
|
|
2558
|
+
const results = new Array(items.length);
|
|
2559
|
+
const active = /* @__PURE__ */ new Set();
|
|
2560
|
+
options.onProgress?.({
|
|
2561
|
+
type: "start",
|
|
2562
|
+
total: items.length,
|
|
2563
|
+
concurrency,
|
|
2564
|
+
dryRun: Boolean(options.dryRun),
|
|
2565
|
+
model: options.model
|
|
2209
2566
|
});
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
model: result.model
|
|
2567
|
+
await runPool(items, concurrency, async (item, index) => {
|
|
2568
|
+
const label = labelForItem(item);
|
|
2569
|
+
active.add(label);
|
|
2570
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
2571
|
+
const result = await translatePage(config, item, options);
|
|
2572
|
+
results[index] = result;
|
|
2573
|
+
active.delete(label);
|
|
2574
|
+
options.onProgress?.({ type: "item-done", result });
|
|
2219
2575
|
});
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
locale: item.locale,
|
|
2224
|
-
skipped: false,
|
|
2225
|
-
model: result.model
|
|
2226
|
-
};
|
|
2576
|
+
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
2577
|
+
options.onProgress?.({ type: "done", results, totals });
|
|
2578
|
+
return results;
|
|
2227
2579
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2580
|
+
function serializeMdx(frontmatter, content) {
|
|
2581
|
+
return matter.stringify(content, frontmatter);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/export/build-static-raw-exports.ts
|
|
2585
|
+
function normalizeExtension(ext) {
|
|
2586
|
+
return ext.startsWith(".") ? ext : `.${ext}`;
|
|
2587
|
+
}
|
|
2588
|
+
function exportDirSegment(pathTemplate) {
|
|
2589
|
+
const prefix = pathPrefix(pathTemplate);
|
|
2590
|
+
return prefix.replace(/^\/+|\/+$/g, "");
|
|
2591
|
+
}
|
|
2592
|
+
function getStaticExportRoots(project, options = {}) {
|
|
2593
|
+
const { config } = project;
|
|
2594
|
+
const locales = options.locales ?? config.locales;
|
|
2595
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2596
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2597
|
+
for (const type of project.listTypes()) {
|
|
2598
|
+
if (!isRoutableType(type.config)) continue;
|
|
2599
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2600
|
+
const segment = exportDirSegment(type.config.path);
|
|
2601
|
+
roots.add(`${segment}/`);
|
|
2602
|
+
for (const locale of locales) {
|
|
2603
|
+
if (locale === config.defaultLocale) continue;
|
|
2604
|
+
roots.add(`${locale}/${segment}/`);
|
|
2605
|
+
}
|
|
2232
2606
|
}
|
|
2233
|
-
return
|
|
2607
|
+
return [...roots].sort();
|
|
2608
|
+
}
|
|
2609
|
+
function buildStaticRawExports(project, options = {}) {
|
|
2610
|
+
const { config } = project;
|
|
2611
|
+
const extension = normalizeExtension(options.extension ?? ".mdx");
|
|
2612
|
+
const locales = options.locales ?? config.locales;
|
|
2613
|
+
const excludeRedirected = options.excludeRedirected ?? true;
|
|
2614
|
+
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2615
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2616
|
+
const out = [];
|
|
2617
|
+
for (const type of project.listTypes()) {
|
|
2618
|
+
if (!isRoutableType(type.config)) continue;
|
|
2619
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2620
|
+
const pathTemplate = type.config.path;
|
|
2621
|
+
const all = type.load();
|
|
2622
|
+
const enIdx = all.get(config.defaultLocale);
|
|
2623
|
+
if (!enIdx) continue;
|
|
2624
|
+
for (const locale of locales) {
|
|
2625
|
+
for (const enDoc of enIdx.bySlug.values()) {
|
|
2626
|
+
const resolved = type.resolve(enDoc.slug, locale);
|
|
2627
|
+
if (!resolved.document) continue;
|
|
2628
|
+
if (excludeRedirected && resolved.shouldRedirectTo) continue;
|
|
2629
|
+
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2630
|
+
const doc = resolved.document;
|
|
2631
|
+
const slugWithExt = `${doc.slug}${extension}`;
|
|
2632
|
+
const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
|
|
2633
|
+
out.push({
|
|
2634
|
+
relativePath: urlPath.slice(1),
|
|
2635
|
+
urlPath,
|
|
2636
|
+
locale,
|
|
2637
|
+
typeId: type.id,
|
|
2638
|
+
enSlug: doc.enSlug,
|
|
2639
|
+
source: serializeMdx(doc.frontmatter, doc.content)
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return out;
|
|
2645
|
+
}
|
|
2646
|
+
function rmDirIfExists(dir) {
|
|
2647
|
+
if (fs2.existsSync(dir)) {
|
|
2648
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
function writeStaticRawExports(project, options = {}) {
|
|
2652
|
+
const outDir = path3.resolve(options.outDir ?? "public");
|
|
2653
|
+
const typeFilter = options.types;
|
|
2654
|
+
for (const root of getStaticExportRoots(project, {
|
|
2655
|
+
types: typeFilter,
|
|
2656
|
+
locales: options.locales
|
|
2657
|
+
})) {
|
|
2658
|
+
rmDirIfExists(path3.join(outDir, root));
|
|
2659
|
+
}
|
|
2660
|
+
const exports = buildStaticRawExports(project, options);
|
|
2661
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2662
|
+
for (const item of exports) {
|
|
2663
|
+
const filePath = path3.join(outDir, item.relativePath);
|
|
2664
|
+
fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
2665
|
+
fs2.writeFileSync(filePath, item.source, "utf8");
|
|
2666
|
+
const key = `${item.typeId}/${item.locale}`;
|
|
2667
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2668
|
+
}
|
|
2669
|
+
for (const [key, count] of [...counts.entries()].sort()) {
|
|
2670
|
+
console.log(` ${key}: ${count}`);
|
|
2671
|
+
}
|
|
2672
|
+
return { exports, written: exports.length };
|
|
2234
2673
|
}
|
|
2235
2674
|
|
|
2236
|
-
export { buildAllContentRedirects, buildWorklist, createProject, createScribe, defineConfig, defineContentType, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, isResolvedConfig, loadConfigSync, resolveConfig, resolveLocalesFromPreset, translatePage, translateWorklist, unwrapSchema, validateProject };
|
|
2675
|
+
export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
|
|
2237
2676
|
//# sourceMappingURL=index.js.map
|
|
2238
2677
|
//# sourceMappingURL=index.js.map
|