scribe-cms 0.0.4 → 0.0.6
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/dist/cli/index.cjs +272 -180
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +272 -181
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +188 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +188 -123
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +25 -18
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +25 -18
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +4 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/history/record-snapshot.d.ts +11 -0
- package/dist/src/history/record-snapshot.d.ts.map +1 -0
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/storage/translations.d.ts +14 -21
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/page-translator.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 +51 -64
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +51 -64
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/history/record-revision.d.ts +0 -14
- package/dist/src/history/record-revision.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -128,14 +128,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
128
128
|
function isRoutableType(type) {
|
|
129
129
|
return typeof type.path === "string" && type.path.length > 0;
|
|
130
130
|
}
|
|
131
|
-
function assertValidPathTemplate(
|
|
132
|
-
if (!
|
|
133
|
-
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}")`);
|
|
134
134
|
}
|
|
135
|
-
const count = (
|
|
135
|
+
const count = (path11.match(/\{slug\}/g) ?? []).length;
|
|
136
136
|
if (count !== 1) {
|
|
137
137
|
throw new Error(
|
|
138
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
138
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
@@ -187,6 +187,7 @@ function resolveConfig(input, baseDir) {
|
|
|
187
187
|
const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
188
188
|
const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
|
|
189
189
|
const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
190
|
+
const assetsPath = raw.assetsDir ? path3.resolve(projectRoot, raw.assetsDir) : void 0;
|
|
190
191
|
const seenIds = /* @__PURE__ */ new Set();
|
|
191
192
|
const types = raw.types.map((type) => {
|
|
192
193
|
if (seenIds.has(type.id)) {
|
|
@@ -207,6 +208,7 @@ function resolveConfig(input, baseDir) {
|
|
|
207
208
|
const config = {
|
|
208
209
|
rootDir: contentRoot,
|
|
209
210
|
storePath,
|
|
211
|
+
assetsPath,
|
|
210
212
|
locales: [...raw.locales],
|
|
211
213
|
defaultLocale,
|
|
212
214
|
localePresets: raw.localePresets,
|
|
@@ -259,10 +261,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
259
261
|
}
|
|
260
262
|
function extractByPaths(data, paths) {
|
|
261
263
|
const out = {};
|
|
262
|
-
for (const
|
|
263
|
-
const value = getAtPath(data,
|
|
264
|
+
for (const path11 of paths) {
|
|
265
|
+
const value = getAtPath(data, path11);
|
|
264
266
|
if (value !== void 0) {
|
|
265
|
-
setAtPath(out,
|
|
267
|
+
setAtPath(out, path11.filter((p) => p !== "*"), value);
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
return out;
|
|
@@ -280,13 +282,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
280
282
|
const translatable = pickTranslatable(localeData, schema);
|
|
281
283
|
return deepMerge(structural, translatable);
|
|
282
284
|
}
|
|
283
|
-
function getAtPath(obj,
|
|
285
|
+
function getAtPath(obj, path11) {
|
|
284
286
|
let current = obj;
|
|
285
|
-
for (const segment of
|
|
287
|
+
for (const segment of path11) {
|
|
286
288
|
if (segment === "*") {
|
|
287
289
|
if (!Array.isArray(current)) return void 0;
|
|
288
290
|
return current.map(
|
|
289
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
291
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
|
|
290
292
|
);
|
|
291
293
|
}
|
|
292
294
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -294,13 +296,13 @@ function getAtPath(obj, path10) {
|
|
|
294
296
|
}
|
|
295
297
|
return current;
|
|
296
298
|
}
|
|
297
|
-
function setAtPath(obj,
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
300
|
-
obj[
|
|
299
|
+
function setAtPath(obj, path11, value) {
|
|
300
|
+
if (path11.length === 0) return;
|
|
301
|
+
if (path11.length === 1) {
|
|
302
|
+
obj[path11[0]] = value;
|
|
301
303
|
return;
|
|
302
304
|
}
|
|
303
|
-
const [head, ...rest] =
|
|
305
|
+
const [head, ...rest] = path11;
|
|
304
306
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
305
307
|
obj[head] = {};
|
|
306
308
|
}
|
|
@@ -496,7 +498,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
496
498
|
redirectTo: enDoc.redirectTo
|
|
497
499
|
};
|
|
498
500
|
}
|
|
499
|
-
var SCHEMA_VERSION =
|
|
501
|
+
var SCHEMA_VERSION = 4;
|
|
500
502
|
var MIGRATIONS = [
|
|
501
503
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
502
504
|
key TEXT PRIMARY KEY,
|
|
@@ -516,20 +518,6 @@ var MIGRATIONS = [
|
|
|
516
518
|
)`,
|
|
517
519
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
518
520
|
ON translations(content_type, locale)`,
|
|
519
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
520
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
521
|
-
content_type TEXT NOT NULL,
|
|
522
|
-
en_slug TEXT NOT NULL,
|
|
523
|
-
locale TEXT,
|
|
524
|
-
revision_kind TEXT NOT NULL,
|
|
525
|
-
en_hash TEXT NOT NULL,
|
|
526
|
-
body_hash TEXT NOT NULL,
|
|
527
|
-
created_at TEXT NOT NULL,
|
|
528
|
-
model TEXT,
|
|
529
|
-
body_preview TEXT
|
|
530
|
-
)`,
|
|
531
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
532
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
533
521
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
534
522
|
content_type TEXT NOT NULL,
|
|
535
523
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -546,7 +534,19 @@ var MIGRATIONS = [
|
|
|
546
534
|
locale_slug TEXT NOT NULL,
|
|
547
535
|
captured_at TEXT NOT NULL,
|
|
548
536
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
549
|
-
)
|
|
537
|
+
)`,
|
|
538
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
539
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
540
|
+
content_type TEXT NOT NULL,
|
|
541
|
+
en_slug TEXT NOT NULL,
|
|
542
|
+
en_hash TEXT NOT NULL,
|
|
543
|
+
frontmatter_json TEXT NOT NULL,
|
|
544
|
+
body TEXT NOT NULL,
|
|
545
|
+
created_at TEXT NOT NULL,
|
|
546
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
547
|
+
)`,
|
|
548
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
549
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
550
550
|
];
|
|
551
551
|
function resolveStorePath(config) {
|
|
552
552
|
return config.storePath;
|
|
@@ -568,12 +568,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
568
568
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
|
+
function readSchemaVersion(db) {
|
|
572
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
573
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
574
|
+
}
|
|
571
575
|
function migrate(db) {
|
|
576
|
+
const previousVersion = readSchemaVersion(db);
|
|
572
577
|
for (const sql of MIGRATIONS) {
|
|
573
578
|
db.exec(sql);
|
|
574
579
|
}
|
|
575
|
-
|
|
576
|
-
|
|
580
|
+
if (previousVersion < 4) {
|
|
581
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
582
|
+
}
|
|
583
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
577
584
|
db.prepare(
|
|
578
585
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
579
586
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -581,18 +588,38 @@ function migrate(db) {
|
|
|
581
588
|
}
|
|
582
589
|
|
|
583
590
|
// src/storage/translations.ts
|
|
591
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
592
|
+
db.prepare(
|
|
593
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
594
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
595
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
596
|
+
).run(
|
|
597
|
+
input.contentType,
|
|
598
|
+
input.enSlug,
|
|
599
|
+
input.enHash,
|
|
600
|
+
JSON.stringify(input.frontmatter),
|
|
601
|
+
input.body,
|
|
602
|
+
input.createdAt
|
|
603
|
+
);
|
|
604
|
+
const row = db.prepare(
|
|
605
|
+
`SELECT id FROM en_snapshots
|
|
606
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
607
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
608
|
+
return row.id;
|
|
609
|
+
}
|
|
584
610
|
function upsertTranslation(db, input) {
|
|
585
611
|
db.prepare(
|
|
586
612
|
`INSERT INTO translations (
|
|
587
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
588
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
613
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
614
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
589
615
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
590
616
|
slug = excluded.slug,
|
|
591
617
|
frontmatter_json = excluded.frontmatter_json,
|
|
592
618
|
body = excluded.body,
|
|
593
619
|
en_hash = excluded.en_hash,
|
|
594
620
|
translated_at = excluded.translated_at,
|
|
595
|
-
model = excluded.model
|
|
621
|
+
model = excluded.model,
|
|
622
|
+
snapshot_id = excluded.snapshot_id`
|
|
596
623
|
).run(
|
|
597
624
|
input.contentType,
|
|
598
625
|
input.enSlug,
|
|
@@ -602,7 +629,8 @@ function upsertTranslation(db, input) {
|
|
|
602
629
|
input.body,
|
|
603
630
|
input.enHash,
|
|
604
631
|
input.translatedAt,
|
|
605
|
-
input.model
|
|
632
|
+
input.model,
|
|
633
|
+
input.snapshotId
|
|
606
634
|
);
|
|
607
635
|
}
|
|
608
636
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -621,27 +649,6 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
|
621
649
|
function bulkLoadTranslations(db) {
|
|
622
650
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
623
651
|
}
|
|
624
|
-
function appendRevision(db, input) {
|
|
625
|
-
const result = db.prepare(
|
|
626
|
-
`INSERT INTO revisions (
|
|
627
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
628
|
-
model, body_preview, frontmatter_json, body
|
|
629
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
630
|
-
).run(
|
|
631
|
-
input.contentType,
|
|
632
|
-
input.enSlug,
|
|
633
|
-
input.locale,
|
|
634
|
-
input.revisionKind,
|
|
635
|
-
input.enHash,
|
|
636
|
-
input.bodyHash,
|
|
637
|
-
input.createdAt,
|
|
638
|
-
input.model ?? null,
|
|
639
|
-
input.bodyPreview ?? null,
|
|
640
|
-
input.frontmatterJson ?? null,
|
|
641
|
-
input.body ?? null
|
|
642
|
-
);
|
|
643
|
-
return Number(result.lastInsertRowid);
|
|
644
|
-
}
|
|
645
652
|
|
|
646
653
|
// src/loader/normalize-en.ts
|
|
647
654
|
function normalizeEnFrontmatter(data) {
|
|
@@ -1571,8 +1578,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1571
1578
|
// src/sitemap/join-base-url.ts
|
|
1572
1579
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1573
1580
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1574
|
-
const
|
|
1575
|
-
return `${origin}${
|
|
1581
|
+
const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1582
|
+
return `${origin}${path11}`;
|
|
1576
1583
|
}
|
|
1577
1584
|
|
|
1578
1585
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1716,36 +1723,6 @@ function loadConfigSync(options = {}) {
|
|
|
1716
1723
|
}
|
|
1717
1724
|
return resolveConfig(config, path3.dirname(configPath));
|
|
1718
1725
|
}
|
|
1719
|
-
function sha256(input) {
|
|
1720
|
-
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
1721
|
-
}
|
|
1722
|
-
function computePageEnHash(translatableFrontmatter, body) {
|
|
1723
|
-
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1724
|
-
return sha256(payload);
|
|
1725
|
-
}
|
|
1726
|
-
function computeBodyHash(body) {
|
|
1727
|
-
return sha256(body);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// src/history/record-revision.ts
|
|
1731
|
-
function recordRevision(config, input) {
|
|
1732
|
-
const db = openStore(config, "readwrite");
|
|
1733
|
-
const id = appendRevision(db, {
|
|
1734
|
-
contentType: input.contentType,
|
|
1735
|
-
enSlug: input.enSlug,
|
|
1736
|
-
locale: input.locale,
|
|
1737
|
-
revisionKind: input.revisionKind,
|
|
1738
|
-
enHash: input.enHash,
|
|
1739
|
-
bodyHash: computeBodyHash(input.body),
|
|
1740
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1741
|
-
model: input.model,
|
|
1742
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1743
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1744
|
-
body: input.body
|
|
1745
|
-
});
|
|
1746
|
-
db.close();
|
|
1747
|
-
return id;
|
|
1748
|
-
}
|
|
1749
1726
|
function getAtPath2(obj, fieldPath) {
|
|
1750
1727
|
let current = obj;
|
|
1751
1728
|
for (const segment of fieldPath) {
|
|
@@ -1825,6 +1802,72 @@ function validateRelations(project) {
|
|
|
1825
1802
|
}
|
|
1826
1803
|
return issues;
|
|
1827
1804
|
}
|
|
1805
|
+
var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
|
|
1806
|
+
var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
|
|
1807
|
+
var MARKDOWN_IMAGE_RE = new RegExp(
|
|
1808
|
+
String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
|
|
1809
|
+
"gi"
|
|
1810
|
+
);
|
|
1811
|
+
var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
|
|
1812
|
+
function isImageWebPath(value) {
|
|
1813
|
+
return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
|
|
1814
|
+
}
|
|
1815
|
+
function isSiteAssetPath(webPath) {
|
|
1816
|
+
const segment = webPath.split("/")[1];
|
|
1817
|
+
return Boolean(segment && !segment.includes("."));
|
|
1818
|
+
}
|
|
1819
|
+
function addImagePath(webPath, out) {
|
|
1820
|
+
if (isSiteAssetPath(webPath)) out.add(webPath);
|
|
1821
|
+
}
|
|
1822
|
+
function collectBodyImagePaths(body, out) {
|
|
1823
|
+
for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
|
|
1824
|
+
for (const match of body.matchAll(re)) {
|
|
1825
|
+
addImagePath(match[1], out);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function collectFrontmatterImagePaths(value, out) {
|
|
1830
|
+
if (typeof value === "string") {
|
|
1831
|
+
if (isImageWebPath(value)) addImagePath(value, out);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (Array.isArray(value)) {
|
|
1835
|
+
for (const item of value) collectFrontmatterImagePaths(item, out);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (value && typeof value === "object") {
|
|
1839
|
+
for (const nested of Object.values(value)) {
|
|
1840
|
+
collectFrontmatterImagePaths(nested, out);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function collectImagePaths(frontmatter, body) {
|
|
1845
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1846
|
+
collectFrontmatterImagePaths(frontmatter, paths);
|
|
1847
|
+
collectBodyImagePaths(body, paths);
|
|
1848
|
+
return [...paths].sort();
|
|
1849
|
+
}
|
|
1850
|
+
function assetFilePath(assetsPath, webPath) {
|
|
1851
|
+
return path3.join(assetsPath, webPath.replace(/^\//, ""));
|
|
1852
|
+
}
|
|
1853
|
+
function validateDocumentAssets(config, input) {
|
|
1854
|
+
const assetsPath = config.assetsPath;
|
|
1855
|
+
if (!assetsPath) return [];
|
|
1856
|
+
const issues = [];
|
|
1857
|
+
for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
|
|
1858
|
+
const filePath = assetFilePath(assetsPath, webPath);
|
|
1859
|
+
if (fs2.existsSync(filePath)) continue;
|
|
1860
|
+
issues.push({
|
|
1861
|
+
level: "warning",
|
|
1862
|
+
contentType: input.contentType,
|
|
1863
|
+
enSlug: input.enSlug,
|
|
1864
|
+
locale: input.locale,
|
|
1865
|
+
field: "asset",
|
|
1866
|
+
message: `Missing image asset ${webPath} (expected ${filePath})`
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
return issues;
|
|
1870
|
+
}
|
|
1828
1871
|
|
|
1829
1872
|
// src/validate/validate-slug-suffix.ts
|
|
1830
1873
|
function validateTranslationSlugSuffixes(config, db) {
|
|
@@ -1887,8 +1930,6 @@ function validateProject(config) {
|
|
|
1887
1930
|
});
|
|
1888
1931
|
continue;
|
|
1889
1932
|
}
|
|
1890
|
-
const payload = getTranslatablePayload(enDoc, type);
|
|
1891
|
-
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
1892
1933
|
const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
|
|
1893
1934
|
locale: config.defaultLocale,
|
|
1894
1935
|
defaultLocale: config.defaultLocale,
|
|
@@ -1906,29 +1947,18 @@ function validateProject(config) {
|
|
|
1906
1947
|
message: issue.message
|
|
1907
1948
|
});
|
|
1908
1949
|
}
|
|
1950
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1951
|
+
contentType: type.id,
|
|
1952
|
+
enSlug,
|
|
1953
|
+
frontmatter: enDoc.frontmatter,
|
|
1954
|
+
body: enDoc.content
|
|
1955
|
+
})) {
|
|
1956
|
+
issues.push(issue);
|
|
1957
|
+
}
|
|
1909
1958
|
for (const locale of config.locales) {
|
|
1910
1959
|
if (locale === config.defaultLocale) continue;
|
|
1911
1960
|
const row = getTranslation(db, type.id, enSlug, locale);
|
|
1912
1961
|
if (!row) continue;
|
|
1913
|
-
if (row.en_hash !== currentEnHash) {
|
|
1914
|
-
issues.push({
|
|
1915
|
-
level: "warning",
|
|
1916
|
-
contentType: type.id,
|
|
1917
|
-
enSlug,
|
|
1918
|
-
locale,
|
|
1919
|
-
field: "en_hash",
|
|
1920
|
-
message: "Translation is stale (en_hash mismatch)"
|
|
1921
|
-
});
|
|
1922
|
-
recordRevision(config, {
|
|
1923
|
-
contentType: type.id,
|
|
1924
|
-
enSlug,
|
|
1925
|
-
locale,
|
|
1926
|
-
revisionKind: "en_edit_detected",
|
|
1927
|
-
enHash: currentEnHash,
|
|
1928
|
-
body: row.body,
|
|
1929
|
-
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
1962
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
1933
1963
|
for (const issue of validateLocaleBuiltinFields(localeFm)) {
|
|
1934
1964
|
issues.push({
|
|
@@ -1940,6 +1970,15 @@ function validateProject(config) {
|
|
|
1940
1970
|
message: issue.message
|
|
1941
1971
|
});
|
|
1942
1972
|
}
|
|
1973
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1974
|
+
contentType: type.id,
|
|
1975
|
+
enSlug,
|
|
1976
|
+
locale,
|
|
1977
|
+
frontmatter: localeFm,
|
|
1978
|
+
body: row.body
|
|
1979
|
+
})) {
|
|
1980
|
+
issues.push(issue);
|
|
1981
|
+
}
|
|
1943
1982
|
}
|
|
1944
1983
|
}
|
|
1945
1984
|
try {
|
|
@@ -2007,6 +2046,15 @@ function validateProject(config) {
|
|
|
2007
2046
|
}
|
|
2008
2047
|
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
2009
2048
|
}
|
|
2049
|
+
function sha256(input) {
|
|
2050
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
2051
|
+
}
|
|
2052
|
+
function computePageEnHash(translatableFrontmatter, body) {
|
|
2053
|
+
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2054
|
+
return sha256(payload);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/translate/worklist.ts
|
|
2010
2058
|
function listEnSlugs3(rootDir, contentDir) {
|
|
2011
2059
|
const dir = path3.join(rootDir, contentDir);
|
|
2012
2060
|
if (!fs2.existsSync(dir)) return [];
|
|
@@ -2065,6 +2113,21 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2065
2113
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2066
2114
|
}
|
|
2067
2115
|
|
|
2116
|
+
// src/history/record-snapshot.ts
|
|
2117
|
+
function recordEnSnapshot(config, input, db) {
|
|
2118
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
2119
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
2120
|
+
contentType: input.contentType,
|
|
2121
|
+
enSlug: input.enSlug,
|
|
2122
|
+
enHash: input.enHash,
|
|
2123
|
+
frontmatter: input.frontmatter,
|
|
2124
|
+
body: input.body,
|
|
2125
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2126
|
+
});
|
|
2127
|
+
if (!db) ownDb.close();
|
|
2128
|
+
return id;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2068
2131
|
// src/translate/gemini-models.ts
|
|
2069
2132
|
var GEMINI_MODEL_IDS = {
|
|
2070
2133
|
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
@@ -2438,6 +2501,17 @@ async function translatePage(config, item, options = {}) {
|
|
|
2438
2501
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2439
2502
|
}
|
|
2440
2503
|
const writeDb = openStore(config, "readwrite");
|
|
2504
|
+
const snapshotId = recordEnSnapshot(
|
|
2505
|
+
config,
|
|
2506
|
+
{
|
|
2507
|
+
contentType: type.id,
|
|
2508
|
+
enSlug: item.enSlug,
|
|
2509
|
+
enHash: currentEnHash,
|
|
2510
|
+
frontmatter: payload.frontmatter,
|
|
2511
|
+
body: payload.body
|
|
2512
|
+
},
|
|
2513
|
+
writeDb
|
|
2514
|
+
);
|
|
2441
2515
|
upsertTranslation(writeDb, {
|
|
2442
2516
|
contentType: type.id,
|
|
2443
2517
|
enSlug: item.enSlug,
|
|
@@ -2447,19 +2521,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2447
2521
|
body: result.parsed.body,
|
|
2448
2522
|
enHash: currentEnHash,
|
|
2449
2523
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2450
|
-
model: result.model
|
|
2524
|
+
model: result.model,
|
|
2525
|
+
snapshotId
|
|
2451
2526
|
});
|
|
2452
2527
|
writeDb.close();
|
|
2453
|
-
recordRevision(config, {
|
|
2454
|
-
contentType: type.id,
|
|
2455
|
-
enSlug: item.enSlug,
|
|
2456
|
-
locale: item.locale,
|
|
2457
|
-
revisionKind: "translation",
|
|
2458
|
-
enHash: currentEnHash,
|
|
2459
|
-
body: result.parsed.body,
|
|
2460
|
-
frontmatter: validated.frontmatter,
|
|
2461
|
-
model: result.model
|
|
2462
|
-
});
|
|
2463
2528
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2464
2529
|
normalizeGeminiDisplayName(result.model),
|
|
2465
2530
|
result.usage.inputTokens,
|