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.cjs
CHANGED
|
@@ -12,6 +12,7 @@ var genai = require('@google/genai');
|
|
|
12
12
|
var dotenv = require('dotenv');
|
|
13
13
|
var nodeServer = require('@hono/node-server');
|
|
14
14
|
var hono = require('hono');
|
|
15
|
+
var core = require('@inquirer/core');
|
|
15
16
|
var prompts = require('@inquirer/prompts');
|
|
16
17
|
var url = require('url');
|
|
17
18
|
|
|
@@ -25,7 +26,6 @@ var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
|
|
|
25
26
|
|
|
26
27
|
var __defProp = Object.defineProperty;
|
|
27
28
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
28
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
29
29
|
var __esm = (fn, res) => function __init() {
|
|
30
30
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
31
31
|
};
|
|
@@ -33,7 +33,6 @@ var __export = (target, all) => {
|
|
|
33
33
|
for (var name in all)
|
|
34
34
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
35
35
|
};
|
|
36
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
37
36
|
|
|
38
37
|
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
39
38
|
var getImportMetaUrl, importMetaUrl;
|
|
@@ -71,12 +70,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
71
70
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
72
71
|
}
|
|
73
72
|
}
|
|
73
|
+
function readSchemaVersion(db) {
|
|
74
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
75
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
76
|
+
}
|
|
74
77
|
function migrate(db) {
|
|
78
|
+
const previousVersion = readSchemaVersion(db);
|
|
75
79
|
for (const sql of MIGRATIONS) {
|
|
76
80
|
db.exec(sql);
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
if (previousVersion < 4) {
|
|
83
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
84
|
+
}
|
|
85
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
80
86
|
db.prepare(
|
|
81
87
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
82
88
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -89,7 +95,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
89
95
|
var init_sqlite = __esm({
|
|
90
96
|
"src/storage/sqlite.ts"() {
|
|
91
97
|
init_cjs_shims();
|
|
92
|
-
SCHEMA_VERSION =
|
|
98
|
+
SCHEMA_VERSION = 4;
|
|
93
99
|
MIGRATIONS = [
|
|
94
100
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
95
101
|
key TEXT PRIMARY KEY,
|
|
@@ -109,20 +115,6 @@ var init_sqlite = __esm({
|
|
|
109
115
|
)`,
|
|
110
116
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
111
117
|
ON translations(content_type, locale)`,
|
|
112
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
113
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
-
content_type TEXT NOT NULL,
|
|
115
|
-
en_slug TEXT NOT NULL,
|
|
116
|
-
locale TEXT,
|
|
117
|
-
revision_kind TEXT NOT NULL,
|
|
118
|
-
en_hash TEXT NOT NULL,
|
|
119
|
-
body_hash TEXT NOT NULL,
|
|
120
|
-
created_at TEXT NOT NULL,
|
|
121
|
-
model TEXT,
|
|
122
|
-
body_preview TEXT
|
|
123
|
-
)`,
|
|
124
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
125
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
126
118
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
127
119
|
content_type TEXT NOT NULL,
|
|
128
120
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -139,7 +131,19 @@ var init_sqlite = __esm({
|
|
|
139
131
|
locale_slug TEXT NOT NULL,
|
|
140
132
|
captured_at TEXT NOT NULL,
|
|
141
133
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
142
|
-
)
|
|
134
|
+
)`,
|
|
135
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
136
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
137
|
+
content_type TEXT NOT NULL,
|
|
138
|
+
en_slug TEXT NOT NULL,
|
|
139
|
+
en_hash TEXT NOT NULL,
|
|
140
|
+
frontmatter_json TEXT NOT NULL,
|
|
141
|
+
body TEXT NOT NULL,
|
|
142
|
+
created_at TEXT NOT NULL,
|
|
143
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
144
|
+
)`,
|
|
145
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
146
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
143
147
|
];
|
|
144
148
|
}
|
|
145
149
|
});
|
|
@@ -613,18 +617,51 @@ init_sqlite();
|
|
|
613
617
|
|
|
614
618
|
// src/storage/translations.ts
|
|
615
619
|
init_cjs_shims();
|
|
620
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
621
|
+
db.prepare(
|
|
622
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
623
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
624
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
625
|
+
).run(
|
|
626
|
+
input.contentType,
|
|
627
|
+
input.enSlug,
|
|
628
|
+
input.enHash,
|
|
629
|
+
JSON.stringify(input.frontmatter),
|
|
630
|
+
input.body,
|
|
631
|
+
input.createdAt
|
|
632
|
+
);
|
|
633
|
+
const row = db.prepare(
|
|
634
|
+
`SELECT id FROM en_snapshots
|
|
635
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
636
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
637
|
+
return row.id;
|
|
638
|
+
}
|
|
639
|
+
function getEnSnapshot(db, snapshotId) {
|
|
640
|
+
return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
|
|
641
|
+
}
|
|
642
|
+
function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
|
|
643
|
+
return db.prepare(
|
|
644
|
+
`SELECT s.*, GROUP_CONCAT(t.locale) AS locales
|
|
645
|
+
FROM en_snapshots s
|
|
646
|
+
LEFT JOIN translations t ON t.snapshot_id = s.id
|
|
647
|
+
WHERE s.content_type = ? AND s.en_slug = ?
|
|
648
|
+
GROUP BY s.id
|
|
649
|
+
ORDER BY s.created_at DESC, s.id DESC`
|
|
650
|
+
).all(contentType, enSlug);
|
|
651
|
+
}
|
|
616
652
|
function upsertTranslation(db, input) {
|
|
617
653
|
db.prepare(
|
|
618
654
|
`INSERT INTO translations (
|
|
619
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
620
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
655
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
656
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
621
657
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
622
658
|
slug = excluded.slug,
|
|
623
659
|
frontmatter_json = excluded.frontmatter_json,
|
|
624
660
|
body = excluded.body,
|
|
625
661
|
en_hash = excluded.en_hash,
|
|
626
662
|
translated_at = excluded.translated_at,
|
|
627
|
-
model = excluded.model
|
|
663
|
+
model = excluded.model,
|
|
664
|
+
snapshot_id = excluded.snapshot_id`
|
|
628
665
|
).run(
|
|
629
666
|
input.contentType,
|
|
630
667
|
input.enSlug,
|
|
@@ -634,7 +671,8 @@ function upsertTranslation(db, input) {
|
|
|
634
671
|
input.body,
|
|
635
672
|
input.enHash,
|
|
636
673
|
input.translatedAt,
|
|
637
|
-
input.model
|
|
674
|
+
input.model,
|
|
675
|
+
input.snapshotId
|
|
638
676
|
);
|
|
639
677
|
}
|
|
640
678
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -656,41 +694,6 @@ function listTranslationsForLocale(db, contentType, locale) {
|
|
|
656
694
|
function bulkLoadTranslations(db) {
|
|
657
695
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
658
696
|
}
|
|
659
|
-
function appendRevision(db, input) {
|
|
660
|
-
const result = db.prepare(
|
|
661
|
-
`INSERT INTO revisions (
|
|
662
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
663
|
-
model, body_preview, frontmatter_json, body
|
|
664
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
665
|
-
).run(
|
|
666
|
-
input.contentType,
|
|
667
|
-
input.enSlug,
|
|
668
|
-
input.locale,
|
|
669
|
-
input.revisionKind,
|
|
670
|
-
input.enHash,
|
|
671
|
-
input.bodyHash,
|
|
672
|
-
input.createdAt,
|
|
673
|
-
input.model ?? null,
|
|
674
|
-
input.bodyPreview ?? null,
|
|
675
|
-
input.frontmatterJson ?? null,
|
|
676
|
-
input.body ?? null
|
|
677
|
-
);
|
|
678
|
-
return Number(result.lastInsertRowid);
|
|
679
|
-
}
|
|
680
|
-
function listRevisions(db, contentType, enSlug, locale) {
|
|
681
|
-
if (locale) {
|
|
682
|
-
return db.prepare(
|
|
683
|
-
`SELECT * FROM revisions
|
|
684
|
-
WHERE content_type = ? AND en_slug = ? AND locale = ?
|
|
685
|
-
ORDER BY created_at DESC, id DESC`
|
|
686
|
-
).all(contentType, enSlug, locale);
|
|
687
|
-
}
|
|
688
|
-
return db.prepare(
|
|
689
|
-
`SELECT * FROM revisions
|
|
690
|
-
WHERE content_type = ? AND en_slug = ?
|
|
691
|
-
ORDER BY created_at DESC, id DESC`
|
|
692
|
-
).all(contentType, enSlug);
|
|
693
|
-
}
|
|
694
697
|
|
|
695
698
|
// src/loader/normalize-en.ts
|
|
696
699
|
init_cjs_shims();
|
|
@@ -1638,22 +1641,45 @@ function validateDocumentAssets(config, input) {
|
|
|
1638
1641
|
|
|
1639
1642
|
// src/validate/validate-slug-suffix.ts
|
|
1640
1643
|
init_cjs_shims();
|
|
1644
|
+
|
|
1645
|
+
// src/core/localized-slug.ts
|
|
1646
|
+
init_cjs_shims();
|
|
1647
|
+
function findLocaleSuffixInSlug(slug, localeCodes) {
|
|
1648
|
+
const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
|
|
1649
|
+
for (const code of sorted) {
|
|
1650
|
+
if (slug.endsWith(`-${code}`)) {
|
|
1651
|
+
return code;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return void 0;
|
|
1655
|
+
}
|
|
1656
|
+
function stripLocaleSuffixFromSlug(slug, localeCodes) {
|
|
1657
|
+
const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
|
|
1658
|
+
if (!matchedCode) {
|
|
1659
|
+
return { slug, stripped: false };
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
slug: slug.slice(0, -(matchedCode.length + 1)),
|
|
1663
|
+
stripped: true,
|
|
1664
|
+
matchedCode
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/validate/validate-slug-suffix.ts
|
|
1641
1669
|
function validateTranslationSlugSuffixes(config, db) {
|
|
1642
1670
|
const issues = [];
|
|
1643
1671
|
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
1644
1672
|
for (const row of bulkLoadTranslations(db)) {
|
|
1645
1673
|
if (row.locale === config.defaultLocale) continue;
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
break;
|
|
1656
|
-
}
|
|
1674
|
+
const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
|
|
1675
|
+
if (matchedCode) {
|
|
1676
|
+
issues.push({
|
|
1677
|
+
contentTypeId: row.content_type,
|
|
1678
|
+
enSlug: row.en_slug,
|
|
1679
|
+
locale: row.locale,
|
|
1680
|
+
slug: row.slug,
|
|
1681
|
+
message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
|
|
1682
|
+
});
|
|
1657
1683
|
}
|
|
1658
1684
|
}
|
|
1659
1685
|
return issues;
|
|
@@ -1801,7 +1827,7 @@ function validateProject(config) {
|
|
|
1801
1827
|
try {
|
|
1802
1828
|
for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
|
|
1803
1829
|
issues.push({
|
|
1804
|
-
level: "
|
|
1830
|
+
level: "warning",
|
|
1805
1831
|
contentType: issue.contentTypeId,
|
|
1806
1832
|
enSlug: issue.enSlug,
|
|
1807
1833
|
locale: issue.locale,
|
|
@@ -1827,9 +1853,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1827
1853
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1828
1854
|
return sha256(payload);
|
|
1829
1855
|
}
|
|
1830
|
-
function computeBodyHash(body) {
|
|
1831
|
-
return sha256(body);
|
|
1832
|
-
}
|
|
1833
1856
|
|
|
1834
1857
|
// src/translate/worklist.ts
|
|
1835
1858
|
init_sqlite();
|
|
@@ -1894,25 +1917,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
1894
1917
|
// src/translate/page-translator.ts
|
|
1895
1918
|
init_cjs_shims();
|
|
1896
1919
|
|
|
1897
|
-
// src/history/record-
|
|
1920
|
+
// src/history/record-snapshot.ts
|
|
1898
1921
|
init_cjs_shims();
|
|
1899
1922
|
init_sqlite();
|
|
1900
|
-
function
|
|
1901
|
-
const
|
|
1902
|
-
const id =
|
|
1923
|
+
function recordEnSnapshot(config, input, db) {
|
|
1924
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
1925
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
1903
1926
|
contentType: input.contentType,
|
|
1904
1927
|
enSlug: input.enSlug,
|
|
1905
|
-
locale: input.locale,
|
|
1906
|
-
revisionKind: input.revisionKind,
|
|
1907
1928
|
enHash: input.enHash,
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1912
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1913
|
-
body: input.body
|
|
1929
|
+
frontmatter: input.frontmatter,
|
|
1930
|
+
body: input.body,
|
|
1931
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1914
1932
|
});
|
|
1915
|
-
db.close();
|
|
1933
|
+
if (!db) ownDb.close();
|
|
1916
1934
|
return id;
|
|
1917
1935
|
}
|
|
1918
1936
|
|
|
@@ -2060,6 +2078,9 @@ function buildPageTranslationPrompt(input) {
|
|
|
2060
2078
|
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2061
2079
|
"## Rules",
|
|
2062
2080
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2081
|
+
...input.slugStrategy === "localized" ? [
|
|
2082
|
+
`- 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.`
|
|
2083
|
+
] : [],
|
|
2063
2084
|
"",
|
|
2064
2085
|
"## Output format",
|
|
2065
2086
|
"Return ONLY valid JSON with keys:",
|
|
@@ -2147,9 +2168,11 @@ init_cjs_shims();
|
|
|
2147
2168
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2148
2169
|
if (slugStrategy === "localized") {
|
|
2149
2170
|
const rules = [
|
|
2150
|
-
"Provide a
|
|
2171
|
+
"Provide a URL slug in JSON field `slug` that is TRANSLATED into the target language.",
|
|
2172
|
+
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2151
2173
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2152
|
-
"For non-Latin
|
|
2174
|
+
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2175
|
+
"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."
|
|
2153
2176
|
];
|
|
2154
2177
|
if (preserveTerms?.length) {
|
|
2155
2178
|
rules.push(
|
|
@@ -2308,12 +2331,26 @@ async function translatePage(config, item, options = {}) {
|
|
|
2308
2331
|
model,
|
|
2309
2332
|
responseSchema: responseSchema ?? void 0
|
|
2310
2333
|
});
|
|
2311
|
-
const
|
|
2334
|
+
const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2335
|
+
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
2336
|
+
const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
|
|
2337
|
+
const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
|
|
2312
2338
|
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2313
2339
|
if (!validated.ok) {
|
|
2314
2340
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2315
2341
|
}
|
|
2316
2342
|
const writeDb = openStore(config, "readwrite");
|
|
2343
|
+
const snapshotId = recordEnSnapshot(
|
|
2344
|
+
config,
|
|
2345
|
+
{
|
|
2346
|
+
contentType: type.id,
|
|
2347
|
+
enSlug: item.enSlug,
|
|
2348
|
+
enHash: currentEnHash,
|
|
2349
|
+
frontmatter: payload.frontmatter,
|
|
2350
|
+
body: payload.body
|
|
2351
|
+
},
|
|
2352
|
+
writeDb
|
|
2353
|
+
);
|
|
2317
2354
|
upsertTranslation(writeDb, {
|
|
2318
2355
|
contentType: type.id,
|
|
2319
2356
|
enSlug: item.enSlug,
|
|
@@ -2323,19 +2360,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2323
2360
|
body: result.parsed.body,
|
|
2324
2361
|
enHash: currentEnHash,
|
|
2325
2362
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2326
|
-
model: result.model
|
|
2363
|
+
model: result.model,
|
|
2364
|
+
snapshotId
|
|
2327
2365
|
});
|
|
2328
2366
|
writeDb.close();
|
|
2329
|
-
recordRevision(config, {
|
|
2330
|
-
contentType: type.id,
|
|
2331
|
-
enSlug: item.enSlug,
|
|
2332
|
-
locale: item.locale,
|
|
2333
|
-
revisionKind: "translation",
|
|
2334
|
-
enHash: currentEnHash,
|
|
2335
|
-
body: result.parsed.body,
|
|
2336
|
-
frontmatter: validated.frontmatter,
|
|
2337
|
-
model: result.model
|
|
2338
|
-
});
|
|
2339
2367
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2340
2368
|
normalizeGeminiDisplayName(result.model),
|
|
2341
2369
|
result.usage.inputTokens,
|
|
@@ -2347,7 +2375,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2347
2375
|
model: result.model,
|
|
2348
2376
|
usage: result.usage,
|
|
2349
2377
|
estimatedCostUsd,
|
|
2350
|
-
durationMs: Date.now() - startedAt
|
|
2378
|
+
durationMs: Date.now() - startedAt,
|
|
2379
|
+
slugAdjusted
|
|
2351
2380
|
};
|
|
2352
2381
|
} catch (error) {
|
|
2353
2382
|
return {
|
|
@@ -2574,38 +2603,31 @@ function renderFrontmatterTable(frontmatter, schema) {
|
|
|
2574
2603
|
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2575
2604
|
</table>`;
|
|
2576
2605
|
}
|
|
2577
|
-
function
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2587
|
-
).join("");
|
|
2588
|
-
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2589
|
-
<pre class="code">${escapeHtml(revision.body)}</pre>`;
|
|
2590
|
-
}
|
|
2591
|
-
return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
|
|
2592
|
-
}
|
|
2593
|
-
function renderRevisionTimeline(revisions) {
|
|
2594
|
-
if (revisions.length === 0) {
|
|
2595
|
-
return `<p class="dim">No history.</p>`;
|
|
2596
|
-
}
|
|
2597
|
-
const items = revisions.map(
|
|
2598
|
-
(row) => `<tr>
|
|
2599
|
-
<td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
|
|
2600
|
-
<td>${escapeHtml(row.revision_kind)}</td>
|
|
2601
|
-
<td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
|
|
2602
|
-
<td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
|
|
2603
|
-
<td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
|
|
2604
|
-
</tr>`
|
|
2606
|
+
function renderEnSnapshotPreview(snapshot) {
|
|
2607
|
+
let frontmatter = {};
|
|
2608
|
+
try {
|
|
2609
|
+
frontmatter = JSON.parse(snapshot.frontmatter_json);
|
|
2610
|
+
} catch {
|
|
2611
|
+
frontmatter = {};
|
|
2612
|
+
}
|
|
2613
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2614
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2605
2615
|
).join("");
|
|
2606
|
-
return `<table class="
|
|
2607
|
-
<
|
|
2608
|
-
|
|
2616
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2617
|
+
<pre class="code">${escapeHtml(snapshot.body)}</pre>`;
|
|
2618
|
+
}
|
|
2619
|
+
function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
|
|
2620
|
+
if (!snapshot) {
|
|
2621
|
+
return `<p class="dim">No EN snapshot linked to this translation.</p>`;
|
|
2622
|
+
}
|
|
2623
|
+
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>` : "";
|
|
2624
|
+
return `<dl class="meta">
|
|
2625
|
+
<dt>snapshot</dt><dd>#${snapshot.id}</dd>
|
|
2626
|
+
<dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
|
|
2627
|
+
<dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
|
|
2628
|
+
</dl>
|
|
2629
|
+
${staleNote}
|
|
2630
|
+
<details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
|
|
2609
2631
|
}
|
|
2610
2632
|
function renderLayout(title, body, project, options = {}) {
|
|
2611
2633
|
const typeLinks = project.listTypes().map((type) => {
|
|
@@ -2916,10 +2938,10 @@ async function startStudio(project, options = {}) {
|
|
|
2916
2938
|
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2917
2939
|
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2918
2940
|
</dl>`;
|
|
2919
|
-
const
|
|
2941
|
+
const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
|
|
2920
2942
|
historyPanel = `<div class="section">
|
|
2921
|
-
<div class="section-head">
|
|
2922
|
-
<div class="section-body">${
|
|
2943
|
+
<div class="section-head">EN snapshot</div>
|
|
2944
|
+
<div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
|
|
2923
2945
|
</div>`;
|
|
2924
2946
|
} else {
|
|
2925
2947
|
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
@@ -2980,16 +3002,6 @@ async function startStudio(project, options = {}) {
|
|
|
2980
3002
|
|
|
2981
3003
|
// cli/prompt-translate.ts
|
|
2982
3004
|
init_cjs_shims();
|
|
2983
|
-
|
|
2984
|
-
// ../../node_modules/@inquirer/core/dist/lib/errors.js
|
|
2985
|
-
init_cjs_shims();
|
|
2986
|
-
var CancelPromptError = class extends Error {
|
|
2987
|
-
constructor() {
|
|
2988
|
-
super(...arguments);
|
|
2989
|
-
__publicField(this, "name", "CancelPromptError");
|
|
2990
|
-
__publicField(this, "message", "Prompt was canceled");
|
|
2991
|
-
}
|
|
2992
|
-
};
|
|
2993
3005
|
function isInteractive() {
|
|
2994
3006
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2995
3007
|
}
|
|
@@ -3000,7 +3012,7 @@ async function runPrompt(prompt) {
|
|
|
3000
3012
|
try {
|
|
3001
3013
|
return await prompt;
|
|
3002
3014
|
} catch (error) {
|
|
3003
|
-
if (error instanceof CancelPromptError) process.exit(0);
|
|
3015
|
+
if (error instanceof core.CancelPromptError) process.exit(0);
|
|
3004
3016
|
throw error;
|
|
3005
3017
|
}
|
|
3006
3018
|
}
|
|
@@ -3101,8 +3113,17 @@ function statusForResult(result, dryRun) {
|
|
|
3101
3113
|
if (dryRun) return yellow("would translate");
|
|
3102
3114
|
return green("translated");
|
|
3103
3115
|
}
|
|
3116
|
+
function slugAdjustedMessage(result) {
|
|
3117
|
+
if (!result.slugAdjusted) return void 0;
|
|
3118
|
+
const { from, to, matchedCode } = result.slugAdjusted;
|
|
3119
|
+
return yellow(
|
|
3120
|
+
`slug adjusted: "${from}" \u2192 "${to}" (stripped -${matchedCode})`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3104
3123
|
function detailForResult(result) {
|
|
3105
3124
|
const parts = [];
|
|
3125
|
+
const slugMsg = slugAdjustedMessage(result);
|
|
3126
|
+
if (slugMsg) parts.push(slugMsg);
|
|
3106
3127
|
if (result.durationMs !== void 0) parts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
3107
3128
|
if (result.usage) {
|
|
3108
3129
|
parts.push(`${formatTokenCount(result.usage.inputTokens)} in`);
|
|
@@ -3136,6 +3157,10 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
3136
3157
|
const status = statusForResult(event.result, dryRun);
|
|
3137
3158
|
const detail = detailForResult(event.result);
|
|
3138
3159
|
console.log(`${label}: ${status}${detail ? ` (${detail.replace(/\x1b\[[0-9;]*m/g, "")})` : ""}`);
|
|
3160
|
+
const slugMsg = slugAdjustedMessage(event.result);
|
|
3161
|
+
if (slugMsg) {
|
|
3162
|
+
console.log(`[warning] ${labelForResult(event.result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`);
|
|
3163
|
+
}
|
|
3139
3164
|
if (event.result.failed && event.result.error) {
|
|
3140
3165
|
console.error(event.result.error);
|
|
3141
3166
|
}
|
|
@@ -3239,6 +3264,14 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
3239
3264
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
3240
3265
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|
|
3241
3266
|
}
|
|
3267
|
+
for (const result of event.results.filter((entry) => entry.slugAdjusted)) {
|
|
3268
|
+
const slugMsg = slugAdjustedMessage(result);
|
|
3269
|
+
if (slugMsg) {
|
|
3270
|
+
process.stdout.write(
|
|
3271
|
+
"\x1B[2K" + yellow(`[warning] ${labelForResult(result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`) + "\n"
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3242
3275
|
break;
|
|
3243
3276
|
}
|
|
3244
3277
|
},
|
|
@@ -3384,7 +3417,7 @@ Commands:
|
|
|
3384
3417
|
validate Validate EN files and sqlite consistency
|
|
3385
3418
|
export-static Write raw MDX files for static hosting
|
|
3386
3419
|
translate Translate stale/missing locale pages
|
|
3387
|
-
history <type> <slug> Show
|
|
3420
|
+
history <type> <slug> Show EN snapshot timeline
|
|
3388
3421
|
studio Start read-only local studio
|
|
3389
3422
|
version Print scribe-cms version
|
|
3390
3423
|
|
|
@@ -3490,11 +3523,13 @@ Translate flags:
|
|
|
3490
3523
|
}
|
|
3491
3524
|
const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3492
3525
|
const db = openStore2(config, "readonly");
|
|
3493
|
-
const rows =
|
|
3526
|
+
const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
|
|
3494
3527
|
db.close();
|
|
3495
3528
|
for (const row of rows) {
|
|
3529
|
+
const locales = row.locales ?? "";
|
|
3530
|
+
if (locale && !locales.split(",").includes(locale)) continue;
|
|
3496
3531
|
console.log(
|
|
3497
|
-
`${row.created_at}
|
|
3532
|
+
`${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
|
|
3498
3533
|
);
|
|
3499
3534
|
}
|
|
3500
3535
|
break;
|