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/index.js
CHANGED
|
@@ -498,7 +498,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
498
498
|
redirectTo: enDoc.redirectTo
|
|
499
499
|
};
|
|
500
500
|
}
|
|
501
|
-
var SCHEMA_VERSION =
|
|
501
|
+
var SCHEMA_VERSION = 4;
|
|
502
502
|
var MIGRATIONS = [
|
|
503
503
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
504
504
|
key TEXT PRIMARY KEY,
|
|
@@ -518,20 +518,6 @@ var MIGRATIONS = [
|
|
|
518
518
|
)`,
|
|
519
519
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
520
520
|
ON translations(content_type, locale)`,
|
|
521
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
522
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
523
|
-
content_type TEXT NOT NULL,
|
|
524
|
-
en_slug TEXT NOT NULL,
|
|
525
|
-
locale TEXT,
|
|
526
|
-
revision_kind TEXT NOT NULL,
|
|
527
|
-
en_hash TEXT NOT NULL,
|
|
528
|
-
body_hash TEXT NOT NULL,
|
|
529
|
-
created_at TEXT NOT NULL,
|
|
530
|
-
model TEXT,
|
|
531
|
-
body_preview TEXT
|
|
532
|
-
)`,
|
|
533
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
534
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
535
521
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
536
522
|
content_type TEXT NOT NULL,
|
|
537
523
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -548,7 +534,19 @@ var MIGRATIONS = [
|
|
|
548
534
|
locale_slug TEXT NOT NULL,
|
|
549
535
|
captured_at TEXT NOT NULL,
|
|
550
536
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
551
|
-
)
|
|
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)`
|
|
552
550
|
];
|
|
553
551
|
function resolveStorePath(config) {
|
|
554
552
|
return config.storePath;
|
|
@@ -570,12 +568,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
570
568
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
571
569
|
}
|
|
572
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
|
+
}
|
|
573
575
|
function migrate(db) {
|
|
576
|
+
const previousVersion = readSchemaVersion(db);
|
|
574
577
|
for (const sql of MIGRATIONS) {
|
|
575
578
|
db.exec(sql);
|
|
576
579
|
}
|
|
577
|
-
|
|
578
|
-
|
|
580
|
+
if (previousVersion < 4) {
|
|
581
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
582
|
+
}
|
|
583
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
579
584
|
db.prepare(
|
|
580
585
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
581
586
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -583,18 +588,38 @@ function migrate(db) {
|
|
|
583
588
|
}
|
|
584
589
|
|
|
585
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
|
+
}
|
|
586
610
|
function upsertTranslation(db, input) {
|
|
587
611
|
db.prepare(
|
|
588
612
|
`INSERT INTO translations (
|
|
589
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
590
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
613
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
614
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
591
615
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
592
616
|
slug = excluded.slug,
|
|
593
617
|
frontmatter_json = excluded.frontmatter_json,
|
|
594
618
|
body = excluded.body,
|
|
595
619
|
en_hash = excluded.en_hash,
|
|
596
620
|
translated_at = excluded.translated_at,
|
|
597
|
-
model = excluded.model
|
|
621
|
+
model = excluded.model,
|
|
622
|
+
snapshot_id = excluded.snapshot_id`
|
|
598
623
|
).run(
|
|
599
624
|
input.contentType,
|
|
600
625
|
input.enSlug,
|
|
@@ -604,7 +629,8 @@ function upsertTranslation(db, input) {
|
|
|
604
629
|
input.body,
|
|
605
630
|
input.enHash,
|
|
606
631
|
input.translatedAt,
|
|
607
|
-
input.model
|
|
632
|
+
input.model,
|
|
633
|
+
input.snapshotId
|
|
608
634
|
);
|
|
609
635
|
}
|
|
610
636
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -623,27 +649,6 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
|
623
649
|
function bulkLoadTranslations(db) {
|
|
624
650
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
625
651
|
}
|
|
626
|
-
function appendRevision(db, input) {
|
|
627
|
-
const result = db.prepare(
|
|
628
|
-
`INSERT INTO revisions (
|
|
629
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
630
|
-
model, body_preview, frontmatter_json, body
|
|
631
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
632
|
-
).run(
|
|
633
|
-
input.contentType,
|
|
634
|
-
input.enSlug,
|
|
635
|
-
input.locale,
|
|
636
|
-
input.revisionKind,
|
|
637
|
-
input.enHash,
|
|
638
|
-
input.bodyHash,
|
|
639
|
-
input.createdAt,
|
|
640
|
-
input.model ?? null,
|
|
641
|
-
input.bodyPreview ?? null,
|
|
642
|
-
input.frontmatterJson ?? null,
|
|
643
|
-
input.body ?? null
|
|
644
|
-
);
|
|
645
|
-
return Number(result.lastInsertRowid);
|
|
646
|
-
}
|
|
647
652
|
|
|
648
653
|
// src/loader/normalize-en.ts
|
|
649
654
|
function normalizeEnFrontmatter(data) {
|
|
@@ -1864,23 +1869,43 @@ function validateDocumentAssets(config, input) {
|
|
|
1864
1869
|
return issues;
|
|
1865
1870
|
}
|
|
1866
1871
|
|
|
1872
|
+
// src/core/localized-slug.ts
|
|
1873
|
+
function findLocaleSuffixInSlug(slug, localeCodes) {
|
|
1874
|
+
const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
|
|
1875
|
+
for (const code of sorted) {
|
|
1876
|
+
if (slug.endsWith(`-${code}`)) {
|
|
1877
|
+
return code;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return void 0;
|
|
1881
|
+
}
|
|
1882
|
+
function stripLocaleSuffixFromSlug(slug, localeCodes) {
|
|
1883
|
+
const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
|
|
1884
|
+
if (!matchedCode) {
|
|
1885
|
+
return { slug, stripped: false };
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
slug: slug.slice(0, -(matchedCode.length + 1)),
|
|
1889
|
+
stripped: true,
|
|
1890
|
+
matchedCode
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1867
1894
|
// src/validate/validate-slug-suffix.ts
|
|
1868
1895
|
function validateTranslationSlugSuffixes(config, db) {
|
|
1869
1896
|
const issues = [];
|
|
1870
1897
|
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
1871
1898
|
for (const row of bulkLoadTranslations(db)) {
|
|
1872
1899
|
if (row.locale === config.defaultLocale) continue;
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
break;
|
|
1883
|
-
}
|
|
1900
|
+
const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
|
|
1901
|
+
if (matchedCode) {
|
|
1902
|
+
issues.push({
|
|
1903
|
+
contentTypeId: row.content_type,
|
|
1904
|
+
enSlug: row.en_slug,
|
|
1905
|
+
locale: row.locale,
|
|
1906
|
+
slug: row.slug,
|
|
1907
|
+
message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
|
|
1908
|
+
});
|
|
1884
1909
|
}
|
|
1885
1910
|
}
|
|
1886
1911
|
return issues;
|
|
@@ -2028,7 +2053,7 @@ function validateProject(config) {
|
|
|
2028
2053
|
try {
|
|
2029
2054
|
for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
|
|
2030
2055
|
issues.push({
|
|
2031
|
-
level: "
|
|
2056
|
+
level: "warning",
|
|
2032
2057
|
contentType: issue.contentTypeId,
|
|
2033
2058
|
enSlug: issue.enSlug,
|
|
2034
2059
|
locale: issue.locale,
|
|
@@ -2048,9 +2073,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
2048
2073
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2049
2074
|
return sha256(payload);
|
|
2050
2075
|
}
|
|
2051
|
-
function computeBodyHash(body) {
|
|
2052
|
-
return sha256(body);
|
|
2053
|
-
}
|
|
2054
2076
|
|
|
2055
2077
|
// src/translate/worklist.ts
|
|
2056
2078
|
function listEnSlugs3(rootDir, contentDir) {
|
|
@@ -2111,23 +2133,18 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2111
2133
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2112
2134
|
}
|
|
2113
2135
|
|
|
2114
|
-
// src/history/record-
|
|
2115
|
-
function
|
|
2116
|
-
const
|
|
2117
|
-
const id =
|
|
2136
|
+
// src/history/record-snapshot.ts
|
|
2137
|
+
function recordEnSnapshot(config, input, db) {
|
|
2138
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
2139
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
2118
2140
|
contentType: input.contentType,
|
|
2119
2141
|
enSlug: input.enSlug,
|
|
2120
|
-
locale: input.locale,
|
|
2121
|
-
revisionKind: input.revisionKind,
|
|
2122
2142
|
enHash: input.enHash,
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
bodyPreview: input.body.slice(0, 200),
|
|
2127
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
2128
|
-
body: input.body
|
|
2143
|
+
frontmatter: input.frontmatter,
|
|
2144
|
+
body: input.body,
|
|
2145
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2129
2146
|
});
|
|
2130
|
-
db.close();
|
|
2147
|
+
if (!db) ownDb.close();
|
|
2131
2148
|
return id;
|
|
2132
2149
|
}
|
|
2133
2150
|
|
|
@@ -2255,6 +2272,9 @@ function buildPageTranslationPrompt(input) {
|
|
|
2255
2272
|
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2256
2273
|
"## Rules",
|
|
2257
2274
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2275
|
+
...input.slugStrategy === "localized" ? [
|
|
2276
|
+
`- 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.`
|
|
2277
|
+
] : [],
|
|
2258
2278
|
"",
|
|
2259
2279
|
"## Output format",
|
|
2260
2280
|
"Return ONLY valid JSON with keys:",
|
|
@@ -2338,9 +2358,11 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2338
2358
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2339
2359
|
if (slugStrategy === "localized") {
|
|
2340
2360
|
const rules = [
|
|
2341
|
-
"Provide a
|
|
2361
|
+
"Provide a URL slug in JSON field `slug` that is TRANSLATED into the target language.",
|
|
2362
|
+
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2342
2363
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2343
|
-
"For non-Latin
|
|
2364
|
+
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2365
|
+
"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."
|
|
2344
2366
|
];
|
|
2345
2367
|
if (preserveTerms?.length) {
|
|
2346
2368
|
rules.push(
|
|
@@ -2498,12 +2520,26 @@ async function translatePage(config, item, options = {}) {
|
|
|
2498
2520
|
model,
|
|
2499
2521
|
responseSchema: responseSchema ?? void 0
|
|
2500
2522
|
});
|
|
2501
|
-
const
|
|
2523
|
+
const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2524
|
+
const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
|
|
2525
|
+
const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
|
|
2526
|
+
const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
|
|
2502
2527
|
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2503
2528
|
if (!validated.ok) {
|
|
2504
2529
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2505
2530
|
}
|
|
2506
2531
|
const writeDb = openStore(config, "readwrite");
|
|
2532
|
+
const snapshotId = recordEnSnapshot(
|
|
2533
|
+
config,
|
|
2534
|
+
{
|
|
2535
|
+
contentType: type.id,
|
|
2536
|
+
enSlug: item.enSlug,
|
|
2537
|
+
enHash: currentEnHash,
|
|
2538
|
+
frontmatter: payload.frontmatter,
|
|
2539
|
+
body: payload.body
|
|
2540
|
+
},
|
|
2541
|
+
writeDb
|
|
2542
|
+
);
|
|
2507
2543
|
upsertTranslation(writeDb, {
|
|
2508
2544
|
contentType: type.id,
|
|
2509
2545
|
enSlug: item.enSlug,
|
|
@@ -2513,19 +2549,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2513
2549
|
body: result.parsed.body,
|
|
2514
2550
|
enHash: currentEnHash,
|
|
2515
2551
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2516
|
-
model: result.model
|
|
2552
|
+
model: result.model,
|
|
2553
|
+
snapshotId
|
|
2517
2554
|
});
|
|
2518
2555
|
writeDb.close();
|
|
2519
|
-
recordRevision(config, {
|
|
2520
|
-
contentType: type.id,
|
|
2521
|
-
enSlug: item.enSlug,
|
|
2522
|
-
locale: item.locale,
|
|
2523
|
-
revisionKind: "translation",
|
|
2524
|
-
enHash: currentEnHash,
|
|
2525
|
-
body: result.parsed.body,
|
|
2526
|
-
frontmatter: validated.frontmatter,
|
|
2527
|
-
model: result.model
|
|
2528
|
-
});
|
|
2529
2556
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2530
2557
|
normalizeGeminiDisplayName(result.model),
|
|
2531
2558
|
result.usage.inputTokens,
|
|
@@ -2537,7 +2564,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2537
2564
|
model: result.model,
|
|
2538
2565
|
usage: result.usage,
|
|
2539
2566
|
estimatedCostUsd,
|
|
2540
|
-
durationMs: Date.now() - startedAt
|
|
2567
|
+
durationMs: Date.now() - startedAt,
|
|
2568
|
+
slugAdjusted
|
|
2541
2569
|
};
|
|
2542
2570
|
} catch (error) {
|
|
2543
2571
|
return {
|