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.
@@ -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;AAuD7C,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,CAuIjC"}
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 = 3;
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
- addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
589
- addColumnIfMissing(db, "revisions", "body", "TEXT");
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
- for (const code of localeCodes) {
1885
- if (row.slug.endsWith(`-${code}`)) {
1886
- issues.push({
1887
- contentTypeId: row.content_type,
1888
- enSlug: row.en_slug,
1889
- locale: row.locale,
1890
- slug: row.slug,
1891
- message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
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: "error",
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-revision.ts
2126
- function recordRevision(config, input) {
2127
- const db = openStore(config, "readwrite");
2128
- const id = appendRevision(db, {
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
- bodyHash: computeBodyHash(input.body),
2135
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2136
- model: input.model,
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 localized URL slug in JSON field `slug`.",
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 locales, transliterate into Latin script."
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 slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
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 {