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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate-progress.d.ts","sourceRoot":"","sources":["../../cli/translate-progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"translate-progress.d.ts","sourceRoot":"","sources":["../../cli/translate-progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAiE7C,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACjD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,+BAA+B,CAAC,OAAO,GAAE;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,yBAAyB,CAmJjC"}
|
package/dist/index.cjs
CHANGED
|
@@ -509,7 +509,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
509
509
|
redirectTo: enDoc.redirectTo
|
|
510
510
|
};
|
|
511
511
|
}
|
|
512
|
-
var SCHEMA_VERSION =
|
|
512
|
+
var SCHEMA_VERSION = 4;
|
|
513
513
|
var MIGRATIONS = [
|
|
514
514
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
515
515
|
key TEXT PRIMARY KEY,
|
|
@@ -529,20 +529,6 @@ var MIGRATIONS = [
|
|
|
529
529
|
)`,
|
|
530
530
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
531
531
|
ON translations(content_type, locale)`,
|
|
532
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
533
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
534
|
-
content_type TEXT NOT NULL,
|
|
535
|
-
en_slug TEXT NOT NULL,
|
|
536
|
-
locale TEXT,
|
|
537
|
-
revision_kind TEXT NOT NULL,
|
|
538
|
-
en_hash TEXT NOT NULL,
|
|
539
|
-
body_hash TEXT NOT NULL,
|
|
540
|
-
created_at TEXT NOT NULL,
|
|
541
|
-
model TEXT,
|
|
542
|
-
body_preview TEXT
|
|
543
|
-
)`,
|
|
544
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
545
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
546
532
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
547
533
|
content_type TEXT NOT NULL,
|
|
548
534
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -559,7 +545,19 @@ var MIGRATIONS = [
|
|
|
559
545
|
locale_slug TEXT NOT NULL,
|
|
560
546
|
captured_at TEXT NOT NULL,
|
|
561
547
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
562
|
-
)
|
|
548
|
+
)`,
|
|
549
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
550
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
551
|
+
content_type TEXT NOT NULL,
|
|
552
|
+
en_slug TEXT NOT NULL,
|
|
553
|
+
en_hash TEXT NOT NULL,
|
|
554
|
+
frontmatter_json TEXT NOT NULL,
|
|
555
|
+
body TEXT NOT NULL,
|
|
556
|
+
created_at TEXT NOT NULL,
|
|
557
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
558
|
+
)`,
|
|
559
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
560
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
563
561
|
];
|
|
564
562
|
function resolveStorePath(config) {
|
|
565
563
|
return config.storePath;
|
|
@@ -581,12 +579,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
581
579
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
582
580
|
}
|
|
583
581
|
}
|
|
582
|
+
function readSchemaVersion(db) {
|
|
583
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
584
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
585
|
+
}
|
|
584
586
|
function migrate(db) {
|
|
587
|
+
const previousVersion = readSchemaVersion(db);
|
|
585
588
|
for (const sql of MIGRATIONS) {
|
|
586
589
|
db.exec(sql);
|
|
587
590
|
}
|
|
588
|
-
|
|
589
|
-
|
|
591
|
+
if (previousVersion < 4) {
|
|
592
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
593
|
+
}
|
|
594
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
590
595
|
db.prepare(
|
|
591
596
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
592
597
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -594,18 +599,38 @@ function migrate(db) {
|
|
|
594
599
|
}
|
|
595
600
|
|
|
596
601
|
// src/storage/translations.ts
|
|
602
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
603
|
+
db.prepare(
|
|
604
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
605
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
606
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
607
|
+
).run(
|
|
608
|
+
input.contentType,
|
|
609
|
+
input.enSlug,
|
|
610
|
+
input.enHash,
|
|
611
|
+
JSON.stringify(input.frontmatter),
|
|
612
|
+
input.body,
|
|
613
|
+
input.createdAt
|
|
614
|
+
);
|
|
615
|
+
const row = db.prepare(
|
|
616
|
+
`SELECT id FROM en_snapshots
|
|
617
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
618
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
619
|
+
return row.id;
|
|
620
|
+
}
|
|
597
621
|
function upsertTranslation(db, input) {
|
|
598
622
|
db.prepare(
|
|
599
623
|
`INSERT INTO translations (
|
|
600
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
601
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
624
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
625
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
602
626
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
603
627
|
slug = excluded.slug,
|
|
604
628
|
frontmatter_json = excluded.frontmatter_json,
|
|
605
629
|
body = excluded.body,
|
|
606
630
|
en_hash = excluded.en_hash,
|
|
607
631
|
translated_at = excluded.translated_at,
|
|
608
|
-
model = excluded.model
|
|
632
|
+
model = excluded.model,
|
|
633
|
+
snapshot_id = excluded.snapshot_id`
|
|
609
634
|
).run(
|
|
610
635
|
input.contentType,
|
|
611
636
|
input.enSlug,
|
|
@@ -615,7 +640,8 @@ function upsertTranslation(db, input) {
|
|
|
615
640
|
input.body,
|
|
616
641
|
input.enHash,
|
|
617
642
|
input.translatedAt,
|
|
618
|
-
input.model
|
|
643
|
+
input.model,
|
|
644
|
+
input.snapshotId
|
|
619
645
|
);
|
|
620
646
|
}
|
|
621
647
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -634,27 +660,6 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
|
634
660
|
function bulkLoadTranslations(db) {
|
|
635
661
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
636
662
|
}
|
|
637
|
-
function appendRevision(db, input) {
|
|
638
|
-
const result = db.prepare(
|
|
639
|
-
`INSERT INTO revisions (
|
|
640
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
641
|
-
model, body_preview, frontmatter_json, body
|
|
642
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
643
|
-
).run(
|
|
644
|
-
input.contentType,
|
|
645
|
-
input.enSlug,
|
|
646
|
-
input.locale,
|
|
647
|
-
input.revisionKind,
|
|
648
|
-
input.enHash,
|
|
649
|
-
input.bodyHash,
|
|
650
|
-
input.createdAt,
|
|
651
|
-
input.model ?? null,
|
|
652
|
-
input.bodyPreview ?? null,
|
|
653
|
-
input.frontmatterJson ?? null,
|
|
654
|
-
input.body ?? null
|
|
655
|
-
);
|
|
656
|
-
return Number(result.lastInsertRowid);
|
|
657
|
-
}
|
|
658
663
|
|
|
659
664
|
// src/loader/normalize-en.ts
|
|
660
665
|
function normalizeEnFrontmatter(data) {
|
|
@@ -1875,23 +1880,43 @@ function validateDocumentAssets(config, input) {
|
|
|
1875
1880
|
return issues;
|
|
1876
1881
|
}
|
|
1877
1882
|
|
|
1883
|
+
// src/core/localized-slug.ts
|
|
1884
|
+
function findLocaleSuffixInSlug(slug, localeCodes) {
|
|
1885
|
+
const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
|
|
1886
|
+
for (const code of sorted) {
|
|
1887
|
+
if (slug.endsWith(`-${code}`)) {
|
|
1888
|
+
return code;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return void 0;
|
|
1892
|
+
}
|
|
1893
|
+
function stripLocaleSuffixFromSlug(slug, localeCodes) {
|
|
1894
|
+
const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
|
|
1895
|
+
if (!matchedCode) {
|
|
1896
|
+
return { slug, stripped: false };
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
slug: slug.slice(0, -(matchedCode.length + 1)),
|
|
1900
|
+
stripped: true,
|
|
1901
|
+
matchedCode
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1878
1905
|
// src/validate/validate-slug-suffix.ts
|
|
1879
1906
|
function validateTranslationSlugSuffixes(config, db) {
|
|
1880
1907
|
const issues = [];
|
|
1881
1908
|
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
1882
1909
|
for (const row of bulkLoadTranslations(db)) {
|
|
1883
1910
|
if (row.locale === config.defaultLocale) continue;
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
break;
|
|
1894
|
-
}
|
|
1911
|
+
const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
|
|
1912
|
+
if (matchedCode) {
|
|
1913
|
+
issues.push({
|
|
1914
|
+
contentTypeId: row.content_type,
|
|
1915
|
+
enSlug: row.en_slug,
|
|
1916
|
+
locale: row.locale,
|
|
1917
|
+
slug: row.slug,
|
|
1918
|
+
message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
|
|
1919
|
+
});
|
|
1895
1920
|
}
|
|
1896
1921
|
}
|
|
1897
1922
|
return issues;
|
|
@@ -2039,7 +2064,7 @@ function validateProject(config) {
|
|
|
2039
2064
|
try {
|
|
2040
2065
|
for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
|
|
2041
2066
|
issues.push({
|
|
2042
|
-
level: "
|
|
2067
|
+
level: "warning",
|
|
2043
2068
|
contentType: issue.contentTypeId,
|
|
2044
2069
|
enSlug: issue.enSlug,
|
|
2045
2070
|
locale: issue.locale,
|
|
@@ -2059,9 +2084,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
2059
2084
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2060
2085
|
return sha256(payload);
|
|
2061
2086
|
}
|
|
2062
|
-
function computeBodyHash(body) {
|
|
2063
|
-
return sha256(body);
|
|
2064
|
-
}
|
|
2065
2087
|
|
|
2066
2088
|
// src/translate/worklist.ts
|
|
2067
2089
|
function listEnSlugs3(rootDir, contentDir) {
|
|
@@ -2122,23 +2144,18 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2122
2144
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2123
2145
|
}
|
|
2124
2146
|
|
|
2125
|
-
// src/history/record-
|
|
2126
|
-
function
|
|
2127
|
-
const
|
|
2128
|
-
const id =
|
|
2147
|
+
// src/history/record-snapshot.ts
|
|
2148
|
+
function recordEnSnapshot(config, input, db) {
|
|
2149
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
2150
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
2129
2151
|
contentType: input.contentType,
|
|
2130
2152
|
enSlug: input.enSlug,
|
|
2131
|
-
locale: input.locale,
|
|
2132
|
-
revisionKind: input.revisionKind,
|
|
2133
2153
|
enHash: input.enHash,
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
bodyPreview: input.body.slice(0, 200),
|
|
2138
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
2139
|
-
body: input.body
|
|
2154
|
+
frontmatter: input.frontmatter,
|
|
2155
|
+
body: input.body,
|
|
2156
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2140
2157
|
});
|
|
2141
|
-
db.close();
|
|
2158
|
+
if (!db) ownDb.close();
|
|
2142
2159
|
return id;
|
|
2143
2160
|
}
|
|
2144
2161
|
|
|
@@ -2266,6 +2283,9 @@ function buildPageTranslationPrompt(input) {
|
|
|
2266
2283
|
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2267
2284
|
"## Rules",
|
|
2268
2285
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2286
|
+
...input.slugStrategy === "localized" ? [
|
|
2287
|
+
`- 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.`
|
|
2288
|
+
] : [],
|
|
2269
2289
|
"",
|
|
2270
2290
|
"## Output format",
|
|
2271
2291
|
"Return ONLY valid JSON with keys:",
|
|
@@ -2349,9 +2369,11 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2349
2369
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2350
2370
|
if (slugStrategy === "localized") {
|
|
2351
2371
|
const rules = [
|
|
2352
|
-
"Provide a
|
|
2372
|
+
"Provide a URL slug in JSON field `slug` that is TRANSLATED into the target language.",
|
|
2373
|
+
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2353
2374
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2354
|
-
"For non-Latin
|
|
2375
|
+
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2376
|
+
"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."
|
|
2355
2377
|
];
|
|
2356
2378
|
if (preserveTerms?.length) {
|
|
2357
2379
|
rules.push(
|
|
@@ -2509,12 +2531,26 @@ async function translatePage(config, item, options = {}) {
|
|
|
2509
2531
|
model,
|
|
2510
2532
|
responseSchema: responseSchema ?? void 0
|
|
2511
2533
|
});
|
|
2512
|
-
const
|
|
2534
|
+
const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2535
|
+
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
2536
|
+
const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
|
|
2537
|
+
const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
|
|
2513
2538
|
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2514
2539
|
if (!validated.ok) {
|
|
2515
2540
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2516
2541
|
}
|
|
2517
2542
|
const writeDb = openStore(config, "readwrite");
|
|
2543
|
+
const snapshotId = recordEnSnapshot(
|
|
2544
|
+
config,
|
|
2545
|
+
{
|
|
2546
|
+
contentType: type.id,
|
|
2547
|
+
enSlug: item.enSlug,
|
|
2548
|
+
enHash: currentEnHash,
|
|
2549
|
+
frontmatter: payload.frontmatter,
|
|
2550
|
+
body: payload.body
|
|
2551
|
+
},
|
|
2552
|
+
writeDb
|
|
2553
|
+
);
|
|
2518
2554
|
upsertTranslation(writeDb, {
|
|
2519
2555
|
contentType: type.id,
|
|
2520
2556
|
enSlug: item.enSlug,
|
|
@@ -2524,19 +2560,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2524
2560
|
body: result.parsed.body,
|
|
2525
2561
|
enHash: currentEnHash,
|
|
2526
2562
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2527
|
-
model: result.model
|
|
2563
|
+
model: result.model,
|
|
2564
|
+
snapshotId
|
|
2528
2565
|
});
|
|
2529
2566
|
writeDb.close();
|
|
2530
|
-
recordRevision(config, {
|
|
2531
|
-
contentType: type.id,
|
|
2532
|
-
enSlug: item.enSlug,
|
|
2533
|
-
locale: item.locale,
|
|
2534
|
-
revisionKind: "translation",
|
|
2535
|
-
enHash: currentEnHash,
|
|
2536
|
-
body: result.parsed.body,
|
|
2537
|
-
frontmatter: validated.frontmatter,
|
|
2538
|
-
model: result.model
|
|
2539
|
-
});
|
|
2540
2567
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2541
2568
|
normalizeGeminiDisplayName(result.model),
|
|
2542
2569
|
result.usage.inputTokens,
|
|
@@ -2548,7 +2575,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2548
2575
|
model: result.model,
|
|
2549
2576
|
usage: result.usage,
|
|
2550
2577
|
estimatedCostUsd,
|
|
2551
|
-
durationMs: Date.now() - startedAt
|
|
2578
|
+
durationMs: Date.now() - startedAt,
|
|
2579
|
+
slugAdjusted
|
|
2552
2580
|
};
|
|
2553
2581
|
} catch (error) {
|
|
2554
2582
|
return {
|