scribe-cms 0.0.5 → 0.0.7
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 +185 -150
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +184 -149
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/translate-progress.d.ts.map +1 -1
- package/dist/index.cjs +114 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +114 -86
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +23 -18
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +23 -18
- package/dist/runtime.js.map +1 -1
- package/dist/src/core/localized-slug.d.ts +8 -0
- package/dist/src/core/localized-slug.d.ts.map +1 -0
- 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 +5 -0
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/resolve-translate-config.d.ts.map +1 -1
- package/dist/src/validate/validate-slug-suffix.d.ts.map +1 -1
- 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 +2 -1
- package/dist/src/history/record-revision.d.ts +0 -14
- package/dist/src/history/record-revision.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -11,11 +11,11 @@ import { GoogleGenAI } from '@google/genai';
|
|
|
11
11
|
import dotenv from 'dotenv';
|
|
12
12
|
import { serve } from '@hono/node-server';
|
|
13
13
|
import { Hono } from 'hono';
|
|
14
|
+
import { CancelPromptError } from '@inquirer/core';
|
|
14
15
|
import { select, checkbox } from '@inquirer/prompts';
|
|
15
16
|
|
|
16
17
|
var __defProp = Object.defineProperty;
|
|
17
18
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
19
19
|
var __esm = (fn, res) => function __init() {
|
|
20
20
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
21
|
};
|
|
@@ -23,7 +23,6 @@ var __export = (target, all) => {
|
|
|
23
23
|
for (var name in all)
|
|
24
24
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
25
|
};
|
|
26
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
27
26
|
var init_esm_shims = __esm({
|
|
28
27
|
"../../node_modules/tsup/assets/esm_shims.js"() {
|
|
29
28
|
}
|
|
@@ -56,12 +55,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
56
55
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
57
56
|
}
|
|
58
57
|
}
|
|
58
|
+
function readSchemaVersion(db) {
|
|
59
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
60
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
61
|
+
}
|
|
59
62
|
function migrate(db) {
|
|
63
|
+
const previousVersion = readSchemaVersion(db);
|
|
60
64
|
for (const sql of MIGRATIONS) {
|
|
61
65
|
db.exec(sql);
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if (previousVersion < 4) {
|
|
68
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
69
|
+
}
|
|
70
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
65
71
|
db.prepare(
|
|
66
72
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
67
73
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -74,7 +80,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
74
80
|
var init_sqlite = __esm({
|
|
75
81
|
"src/storage/sqlite.ts"() {
|
|
76
82
|
init_esm_shims();
|
|
77
|
-
SCHEMA_VERSION =
|
|
83
|
+
SCHEMA_VERSION = 4;
|
|
78
84
|
MIGRATIONS = [
|
|
79
85
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
80
86
|
key TEXT PRIMARY KEY,
|
|
@@ -94,20 +100,6 @@ var init_sqlite = __esm({
|
|
|
94
100
|
)`,
|
|
95
101
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
96
102
|
ON translations(content_type, locale)`,
|
|
97
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
98
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
|
-
content_type TEXT NOT NULL,
|
|
100
|
-
en_slug TEXT NOT NULL,
|
|
101
|
-
locale TEXT,
|
|
102
|
-
revision_kind TEXT NOT NULL,
|
|
103
|
-
en_hash TEXT NOT NULL,
|
|
104
|
-
body_hash TEXT NOT NULL,
|
|
105
|
-
created_at TEXT NOT NULL,
|
|
106
|
-
model TEXT,
|
|
107
|
-
body_preview TEXT
|
|
108
|
-
)`,
|
|
109
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
110
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
111
103
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
112
104
|
content_type TEXT NOT NULL,
|
|
113
105
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -124,7 +116,19 @@ var init_sqlite = __esm({
|
|
|
124
116
|
locale_slug TEXT NOT NULL,
|
|
125
117
|
captured_at TEXT NOT NULL,
|
|
126
118
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
127
|
-
)
|
|
119
|
+
)`,
|
|
120
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
content_type TEXT NOT NULL,
|
|
123
|
+
en_slug TEXT NOT NULL,
|
|
124
|
+
en_hash TEXT NOT NULL,
|
|
125
|
+
frontmatter_json TEXT NOT NULL,
|
|
126
|
+
body TEXT NOT NULL,
|
|
127
|
+
created_at TEXT NOT NULL,
|
|
128
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
129
|
+
)`,
|
|
130
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
131
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
128
132
|
];
|
|
129
133
|
}
|
|
130
134
|
});
|
|
@@ -598,18 +602,51 @@ init_sqlite();
|
|
|
598
602
|
|
|
599
603
|
// src/storage/translations.ts
|
|
600
604
|
init_esm_shims();
|
|
605
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
606
|
+
db.prepare(
|
|
607
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
608
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
609
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
610
|
+
).run(
|
|
611
|
+
input.contentType,
|
|
612
|
+
input.enSlug,
|
|
613
|
+
input.enHash,
|
|
614
|
+
JSON.stringify(input.frontmatter),
|
|
615
|
+
input.body,
|
|
616
|
+
input.createdAt
|
|
617
|
+
);
|
|
618
|
+
const row = db.prepare(
|
|
619
|
+
`SELECT id FROM en_snapshots
|
|
620
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
621
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
622
|
+
return row.id;
|
|
623
|
+
}
|
|
624
|
+
function getEnSnapshot(db, snapshotId) {
|
|
625
|
+
return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
|
|
626
|
+
}
|
|
627
|
+
function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
|
|
628
|
+
return db.prepare(
|
|
629
|
+
`SELECT s.*, GROUP_CONCAT(t.locale) AS locales
|
|
630
|
+
FROM en_snapshots s
|
|
631
|
+
LEFT JOIN translations t ON t.snapshot_id = s.id
|
|
632
|
+
WHERE s.content_type = ? AND s.en_slug = ?
|
|
633
|
+
GROUP BY s.id
|
|
634
|
+
ORDER BY s.created_at DESC, s.id DESC`
|
|
635
|
+
).all(contentType, enSlug);
|
|
636
|
+
}
|
|
601
637
|
function upsertTranslation(db, input) {
|
|
602
638
|
db.prepare(
|
|
603
639
|
`INSERT INTO translations (
|
|
604
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
605
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
640
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
641
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
606
642
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
607
643
|
slug = excluded.slug,
|
|
608
644
|
frontmatter_json = excluded.frontmatter_json,
|
|
609
645
|
body = excluded.body,
|
|
610
646
|
en_hash = excluded.en_hash,
|
|
611
647
|
translated_at = excluded.translated_at,
|
|
612
|
-
model = excluded.model
|
|
648
|
+
model = excluded.model,
|
|
649
|
+
snapshot_id = excluded.snapshot_id`
|
|
613
650
|
).run(
|
|
614
651
|
input.contentType,
|
|
615
652
|
input.enSlug,
|
|
@@ -619,7 +656,8 @@ function upsertTranslation(db, input) {
|
|
|
619
656
|
input.body,
|
|
620
657
|
input.enHash,
|
|
621
658
|
input.translatedAt,
|
|
622
|
-
input.model
|
|
659
|
+
input.model,
|
|
660
|
+
input.snapshotId
|
|
623
661
|
);
|
|
624
662
|
}
|
|
625
663
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -641,41 +679,6 @@ function listTranslationsForLocale(db, contentType, locale) {
|
|
|
641
679
|
function bulkLoadTranslations(db) {
|
|
642
680
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
643
681
|
}
|
|
644
|
-
function appendRevision(db, input) {
|
|
645
|
-
const result = db.prepare(
|
|
646
|
-
`INSERT INTO revisions (
|
|
647
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
648
|
-
model, body_preview, frontmatter_json, body
|
|
649
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
650
|
-
).run(
|
|
651
|
-
input.contentType,
|
|
652
|
-
input.enSlug,
|
|
653
|
-
input.locale,
|
|
654
|
-
input.revisionKind,
|
|
655
|
-
input.enHash,
|
|
656
|
-
input.bodyHash,
|
|
657
|
-
input.createdAt,
|
|
658
|
-
input.model ?? null,
|
|
659
|
-
input.bodyPreview ?? null,
|
|
660
|
-
input.frontmatterJson ?? null,
|
|
661
|
-
input.body ?? null
|
|
662
|
-
);
|
|
663
|
-
return Number(result.lastInsertRowid);
|
|
664
|
-
}
|
|
665
|
-
function listRevisions(db, contentType, enSlug, locale) {
|
|
666
|
-
if (locale) {
|
|
667
|
-
return db.prepare(
|
|
668
|
-
`SELECT * FROM revisions
|
|
669
|
-
WHERE content_type = ? AND en_slug = ? AND locale = ?
|
|
670
|
-
ORDER BY created_at DESC, id DESC`
|
|
671
|
-
).all(contentType, enSlug, locale);
|
|
672
|
-
}
|
|
673
|
-
return db.prepare(
|
|
674
|
-
`SELECT * FROM revisions
|
|
675
|
-
WHERE content_type = ? AND en_slug = ?
|
|
676
|
-
ORDER BY created_at DESC, id DESC`
|
|
677
|
-
).all(contentType, enSlug);
|
|
678
|
-
}
|
|
679
682
|
|
|
680
683
|
// src/loader/normalize-en.ts
|
|
681
684
|
init_esm_shims();
|
|
@@ -1623,22 +1626,45 @@ function validateDocumentAssets(config, input) {
|
|
|
1623
1626
|
|
|
1624
1627
|
// src/validate/validate-slug-suffix.ts
|
|
1625
1628
|
init_esm_shims();
|
|
1629
|
+
|
|
1630
|
+
// src/core/localized-slug.ts
|
|
1631
|
+
init_esm_shims();
|
|
1632
|
+
function findLocaleSuffixInSlug(slug, localeCodes) {
|
|
1633
|
+
const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
|
|
1634
|
+
for (const code of sorted) {
|
|
1635
|
+
if (slug.endsWith(`-${code}`)) {
|
|
1636
|
+
return code;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return void 0;
|
|
1640
|
+
}
|
|
1641
|
+
function stripLocaleSuffixFromSlug(slug, localeCodes) {
|
|
1642
|
+
const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
|
|
1643
|
+
if (!matchedCode) {
|
|
1644
|
+
return { slug, stripped: false };
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
slug: slug.slice(0, -(matchedCode.length + 1)),
|
|
1648
|
+
stripped: true,
|
|
1649
|
+
matchedCode
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/validate/validate-slug-suffix.ts
|
|
1626
1654
|
function validateTranslationSlugSuffixes(config, db) {
|
|
1627
1655
|
const issues = [];
|
|
1628
1656
|
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
1629
1657
|
for (const row of bulkLoadTranslations(db)) {
|
|
1630
1658
|
if (row.locale === config.defaultLocale) continue;
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
break;
|
|
1641
|
-
}
|
|
1659
|
+
const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
|
|
1660
|
+
if (matchedCode) {
|
|
1661
|
+
issues.push({
|
|
1662
|
+
contentTypeId: row.content_type,
|
|
1663
|
+
enSlug: row.en_slug,
|
|
1664
|
+
locale: row.locale,
|
|
1665
|
+
slug: row.slug,
|
|
1666
|
+
message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
|
|
1667
|
+
});
|
|
1642
1668
|
}
|
|
1643
1669
|
}
|
|
1644
1670
|
return issues;
|
|
@@ -1786,7 +1812,7 @@ function validateProject(config) {
|
|
|
1786
1812
|
try {
|
|
1787
1813
|
for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
|
|
1788
1814
|
issues.push({
|
|
1789
|
-
level: "
|
|
1815
|
+
level: "warning",
|
|
1790
1816
|
contentType: issue.contentTypeId,
|
|
1791
1817
|
enSlug: issue.enSlug,
|
|
1792
1818
|
locale: issue.locale,
|
|
@@ -1812,9 +1838,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1812
1838
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1813
1839
|
return sha256(payload);
|
|
1814
1840
|
}
|
|
1815
|
-
function computeBodyHash(body) {
|
|
1816
|
-
return sha256(body);
|
|
1817
|
-
}
|
|
1818
1841
|
|
|
1819
1842
|
// src/translate/worklist.ts
|
|
1820
1843
|
init_sqlite();
|
|
@@ -1879,25 +1902,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
1879
1902
|
// src/translate/page-translator.ts
|
|
1880
1903
|
init_esm_shims();
|
|
1881
1904
|
|
|
1882
|
-
// src/history/record-
|
|
1905
|
+
// src/history/record-snapshot.ts
|
|
1883
1906
|
init_esm_shims();
|
|
1884
1907
|
init_sqlite();
|
|
1885
|
-
function
|
|
1886
|
-
const
|
|
1887
|
-
const id =
|
|
1908
|
+
function recordEnSnapshot(config, input, db) {
|
|
1909
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
1910
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
1888
1911
|
contentType: input.contentType,
|
|
1889
1912
|
enSlug: input.enSlug,
|
|
1890
|
-
locale: input.locale,
|
|
1891
|
-
revisionKind: input.revisionKind,
|
|
1892
1913
|
enHash: input.enHash,
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1897
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1898
|
-
body: input.body
|
|
1914
|
+
frontmatter: input.frontmatter,
|
|
1915
|
+
body: input.body,
|
|
1916
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1899
1917
|
});
|
|
1900
|
-
db.close();
|
|
1918
|
+
if (!db) ownDb.close();
|
|
1901
1919
|
return id;
|
|
1902
1920
|
}
|
|
1903
1921
|
|
|
@@ -2045,6 +2063,9 @@ function buildPageTranslationPrompt(input) {
|
|
|
2045
2063
|
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2046
2064
|
"## Rules",
|
|
2047
2065
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2066
|
+
...input.slugStrategy === "localized" ? [
|
|
2067
|
+
`- The slug MUST be written in ${localeName}, derived from the ${localeName} title and its meaning \u2014 never the English slug. Transliterate non-Latin ${localeName} into ASCII Latin.`
|
|
2068
|
+
] : [],
|
|
2048
2069
|
"",
|
|
2049
2070
|
"## Output format",
|
|
2050
2071
|
"Return ONLY valid JSON with keys:",
|
|
@@ -2132,9 +2153,11 @@ init_esm_shims();
|
|
|
2132
2153
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2133
2154
|
if (slugStrategy === "localized") {
|
|
2134
2155
|
const rules = [
|
|
2135
|
-
"Provide a
|
|
2156
|
+
"Provide a URL slug in JSON field `slug` that is TRANSLATED into the target language.",
|
|
2157
|
+
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2136
2158
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2137
|
-
"For non-Latin
|
|
2159
|
+
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2160
|
+
"Do NOT append locale codes to the slug (e.g. -fr, -he, -zh-cn). Locale routing is handled by the URL prefix, not the slug."
|
|
2138
2161
|
];
|
|
2139
2162
|
if (preserveTerms?.length) {
|
|
2140
2163
|
rules.push(
|
|
@@ -2293,12 +2316,26 @@ async function translatePage(config, item, options = {}) {
|
|
|
2293
2316
|
model,
|
|
2294
2317
|
responseSchema: responseSchema ?? void 0
|
|
2295
2318
|
});
|
|
2296
|
-
const
|
|
2319
|
+
const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2320
|
+
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
2321
|
+
const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
|
|
2322
|
+
const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
|
|
2297
2323
|
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2298
2324
|
if (!validated.ok) {
|
|
2299
2325
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2300
2326
|
}
|
|
2301
2327
|
const writeDb = openStore(config, "readwrite");
|
|
2328
|
+
const snapshotId = recordEnSnapshot(
|
|
2329
|
+
config,
|
|
2330
|
+
{
|
|
2331
|
+
contentType: type.id,
|
|
2332
|
+
enSlug: item.enSlug,
|
|
2333
|
+
enHash: currentEnHash,
|
|
2334
|
+
frontmatter: payload.frontmatter,
|
|
2335
|
+
body: payload.body
|
|
2336
|
+
},
|
|
2337
|
+
writeDb
|
|
2338
|
+
);
|
|
2302
2339
|
upsertTranslation(writeDb, {
|
|
2303
2340
|
contentType: type.id,
|
|
2304
2341
|
enSlug: item.enSlug,
|
|
@@ -2308,19 +2345,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2308
2345
|
body: result.parsed.body,
|
|
2309
2346
|
enHash: currentEnHash,
|
|
2310
2347
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2311
|
-
model: result.model
|
|
2348
|
+
model: result.model,
|
|
2349
|
+
snapshotId
|
|
2312
2350
|
});
|
|
2313
2351
|
writeDb.close();
|
|
2314
|
-
recordRevision(config, {
|
|
2315
|
-
contentType: type.id,
|
|
2316
|
-
enSlug: item.enSlug,
|
|
2317
|
-
locale: item.locale,
|
|
2318
|
-
revisionKind: "translation",
|
|
2319
|
-
enHash: currentEnHash,
|
|
2320
|
-
body: result.parsed.body,
|
|
2321
|
-
frontmatter: validated.frontmatter,
|
|
2322
|
-
model: result.model
|
|
2323
|
-
});
|
|
2324
2352
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2325
2353
|
normalizeGeminiDisplayName(result.model),
|
|
2326
2354
|
result.usage.inputTokens,
|
|
@@ -2332,7 +2360,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2332
2360
|
model: result.model,
|
|
2333
2361
|
usage: result.usage,
|
|
2334
2362
|
estimatedCostUsd,
|
|
2335
|
-
durationMs: Date.now() - startedAt
|
|
2363
|
+
durationMs: Date.now() - startedAt,
|
|
2364
|
+
slugAdjusted
|
|
2336
2365
|
};
|
|
2337
2366
|
} catch (error) {
|
|
2338
2367
|
return {
|
|
@@ -2559,38 +2588,31 @@ function renderFrontmatterTable(frontmatter, schema) {
|
|
|
2559
2588
|
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2560
2589
|
</table>`;
|
|
2561
2590
|
}
|
|
2562
|
-
function
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2572
|
-
).join("");
|
|
2573
|
-
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2574
|
-
<pre class="code">${escapeHtml(revision.body)}</pre>`;
|
|
2575
|
-
}
|
|
2576
|
-
return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
|
|
2577
|
-
}
|
|
2578
|
-
function renderRevisionTimeline(revisions) {
|
|
2579
|
-
if (revisions.length === 0) {
|
|
2580
|
-
return `<p class="dim">No history.</p>`;
|
|
2581
|
-
}
|
|
2582
|
-
const items = revisions.map(
|
|
2583
|
-
(row) => `<tr>
|
|
2584
|
-
<td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
|
|
2585
|
-
<td>${escapeHtml(row.revision_kind)}</td>
|
|
2586
|
-
<td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
|
|
2587
|
-
<td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
|
|
2588
|
-
<td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
|
|
2589
|
-
</tr>`
|
|
2591
|
+
function renderEnSnapshotPreview(snapshot) {
|
|
2592
|
+
let frontmatter = {};
|
|
2593
|
+
try {
|
|
2594
|
+
frontmatter = JSON.parse(snapshot.frontmatter_json);
|
|
2595
|
+
} catch {
|
|
2596
|
+
frontmatter = {};
|
|
2597
|
+
}
|
|
2598
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2599
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2590
2600
|
).join("");
|
|
2591
|
-
return `<table class="
|
|
2592
|
-
<
|
|
2593
|
-
|
|
2601
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2602
|
+
<pre class="code">${escapeHtml(snapshot.body)}</pre>`;
|
|
2603
|
+
}
|
|
2604
|
+
function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
|
|
2605
|
+
if (!snapshot) {
|
|
2606
|
+
return `<p class="dim">No EN snapshot linked to this translation.</p>`;
|
|
2607
|
+
}
|
|
2608
|
+
const staleNote = currentEnHash && currentEnHash !== snapshot.en_hash ? `<p class="dim">Current EN hash differs from snapshot (${escapeHtml(currentEnHash.slice(0, 12))} vs ${escapeHtml(snapshot.en_hash.slice(0, 12))}).</p>` : "";
|
|
2609
|
+
return `<dl class="meta">
|
|
2610
|
+
<dt>snapshot</dt><dd>#${snapshot.id}</dd>
|
|
2611
|
+
<dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
|
|
2612
|
+
<dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
|
|
2613
|
+
</dl>
|
|
2614
|
+
${staleNote}
|
|
2615
|
+
<details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
|
|
2594
2616
|
}
|
|
2595
2617
|
function renderLayout(title, body, project, options = {}) {
|
|
2596
2618
|
const typeLinks = project.listTypes().map((type) => {
|
|
@@ -2901,10 +2923,10 @@ async function startStudio(project, options = {}) {
|
|
|
2901
2923
|
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2902
2924
|
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2903
2925
|
</dl>`;
|
|
2904
|
-
const
|
|
2926
|
+
const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
|
|
2905
2927
|
historyPanel = `<div class="section">
|
|
2906
|
-
<div class="section-head">
|
|
2907
|
-
<div class="section-body">${
|
|
2928
|
+
<div class="section-head">EN snapshot</div>
|
|
2929
|
+
<div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
|
|
2908
2930
|
</div>`;
|
|
2909
2931
|
} else {
|
|
2910
2932
|
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
@@ -2965,16 +2987,6 @@ async function startStudio(project, options = {}) {
|
|
|
2965
2987
|
|
|
2966
2988
|
// cli/prompt-translate.ts
|
|
2967
2989
|
init_esm_shims();
|
|
2968
|
-
|
|
2969
|
-
// ../../node_modules/@inquirer/core/dist/lib/errors.js
|
|
2970
|
-
init_esm_shims();
|
|
2971
|
-
var CancelPromptError = class extends Error {
|
|
2972
|
-
constructor() {
|
|
2973
|
-
super(...arguments);
|
|
2974
|
-
__publicField(this, "name", "CancelPromptError");
|
|
2975
|
-
__publicField(this, "message", "Prompt was canceled");
|
|
2976
|
-
}
|
|
2977
|
-
};
|
|
2978
2990
|
function isInteractive() {
|
|
2979
2991
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2980
2992
|
}
|
|
@@ -3086,8 +3098,17 @@ function statusForResult(result, dryRun) {
|
|
|
3086
3098
|
if (dryRun) return yellow("would translate");
|
|
3087
3099
|
return green("translated");
|
|
3088
3100
|
}
|
|
3101
|
+
function slugAdjustedMessage(result) {
|
|
3102
|
+
if (!result.slugAdjusted) return void 0;
|
|
3103
|
+
const { from, to, matchedCode } = result.slugAdjusted;
|
|
3104
|
+
return yellow(
|
|
3105
|
+
`slug adjusted: "${from}" \u2192 "${to}" (stripped -${matchedCode})`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3089
3108
|
function detailForResult(result) {
|
|
3090
3109
|
const parts = [];
|
|
3110
|
+
const slugMsg = slugAdjustedMessage(result);
|
|
3111
|
+
if (slugMsg) parts.push(slugMsg);
|
|
3091
3112
|
if (result.durationMs !== void 0) parts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
3092
3113
|
if (result.usage) {
|
|
3093
3114
|
parts.push(`${formatTokenCount(result.usage.inputTokens)} in`);
|
|
@@ -3121,6 +3142,10 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
3121
3142
|
const status = statusForResult(event.result, dryRun);
|
|
3122
3143
|
const detail = detailForResult(event.result);
|
|
3123
3144
|
console.log(`${label}: ${status}${detail ? ` (${detail.replace(/\x1b\[[0-9;]*m/g, "")})` : ""}`);
|
|
3145
|
+
const slugMsg = slugAdjustedMessage(event.result);
|
|
3146
|
+
if (slugMsg) {
|
|
3147
|
+
console.log(`[warning] ${labelForResult(event.result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`);
|
|
3148
|
+
}
|
|
3124
3149
|
if (event.result.failed && event.result.error) {
|
|
3125
3150
|
console.error(event.result.error);
|
|
3126
3151
|
}
|
|
@@ -3224,6 +3249,14 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
3224
3249
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
3225
3250
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|
|
3226
3251
|
}
|
|
3252
|
+
for (const result of event.results.filter((entry) => entry.slugAdjusted)) {
|
|
3253
|
+
const slugMsg = slugAdjustedMessage(result);
|
|
3254
|
+
if (slugMsg) {
|
|
3255
|
+
process.stdout.write(
|
|
3256
|
+
"\x1B[2K" + yellow(`[warning] ${labelForResult(result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`) + "\n"
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3227
3260
|
break;
|
|
3228
3261
|
}
|
|
3229
3262
|
},
|
|
@@ -3369,7 +3402,7 @@ Commands:
|
|
|
3369
3402
|
validate Validate EN files and sqlite consistency
|
|
3370
3403
|
export-static Write raw MDX files for static hosting
|
|
3371
3404
|
translate Translate stale/missing locale pages
|
|
3372
|
-
history <type> <slug> Show
|
|
3405
|
+
history <type> <slug> Show EN snapshot timeline
|
|
3373
3406
|
studio Start read-only local studio
|
|
3374
3407
|
version Print scribe-cms version
|
|
3375
3408
|
|
|
@@ -3475,11 +3508,13 @@ Translate flags:
|
|
|
3475
3508
|
}
|
|
3476
3509
|
const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3477
3510
|
const db = openStore2(config, "readonly");
|
|
3478
|
-
const rows =
|
|
3511
|
+
const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
|
|
3479
3512
|
db.close();
|
|
3480
3513
|
for (const row of rows) {
|
|
3514
|
+
const locales = row.locales ?? "";
|
|
3515
|
+
if (locale && !locales.split(",").includes(locale)) continue;
|
|
3481
3516
|
console.log(
|
|
3482
|
-
`${row.created_at}
|
|
3517
|
+
`${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
|
|
3483
3518
|
);
|
|
3484
3519
|
}
|
|
3485
3520
|
break;
|