scribe-cms 0.0.6 → 0.0.8

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
@@ -1187,6 +1187,9 @@ function createProject(config) {
1187
1187
  getType: getRuntime,
1188
1188
  listTypes() {
1189
1189
  return Array.from(runtimes.values());
1190
+ },
1191
+ listRoutableTypes() {
1192
+ return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
1190
1193
  }
1191
1194
  };
1192
1195
  }
@@ -1682,6 +1685,7 @@ function createScribe(input) {
1682
1685
  project,
1683
1686
  getType: project.getType,
1684
1687
  listTypes: project.listTypes,
1688
+ listRoutableTypes: project.listRoutableTypes,
1685
1689
  sitemap(options) {
1686
1690
  return generateSitemap(project, options);
1687
1691
  }
@@ -1880,23 +1884,43 @@ function validateDocumentAssets(config, input) {
1880
1884
  return issues;
1881
1885
  }
1882
1886
 
1887
+ // src/core/localized-slug.ts
1888
+ function findLocaleSuffixInSlug(slug, localeCodes) {
1889
+ const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
1890
+ for (const code of sorted) {
1891
+ if (slug.endsWith(`-${code}`)) {
1892
+ return code;
1893
+ }
1894
+ }
1895
+ return void 0;
1896
+ }
1897
+ function stripLocaleSuffixFromSlug(slug, localeCodes) {
1898
+ const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
1899
+ if (!matchedCode) {
1900
+ return { slug, stripped: false };
1901
+ }
1902
+ return {
1903
+ slug: slug.slice(0, -(matchedCode.length + 1)),
1904
+ stripped: true,
1905
+ matchedCode
1906
+ };
1907
+ }
1908
+
1883
1909
  // src/validate/validate-slug-suffix.ts
1884
1910
  function validateTranslationSlugSuffixes(config, db) {
1885
1911
  const issues = [];
1886
1912
  const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
1887
1913
  for (const row of bulkLoadTranslations(db)) {
1888
1914
  if (row.locale === config.defaultLocale) continue;
1889
- for (const code of localeCodes) {
1890
- if (row.slug.endsWith(`-${code}`)) {
1891
- issues.push({
1892
- contentTypeId: row.content_type,
1893
- enSlug: row.en_slug,
1894
- locale: row.locale,
1895
- slug: row.slug,
1896
- message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
1897
- });
1898
- break;
1899
- }
1915
+ const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
1916
+ if (matchedCode) {
1917
+ issues.push({
1918
+ contentTypeId: row.content_type,
1919
+ enSlug: row.en_slug,
1920
+ locale: row.locale,
1921
+ slug: row.slug,
1922
+ message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
1923
+ });
1900
1924
  }
1901
1925
  }
1902
1926
  return issues;
@@ -2044,7 +2068,7 @@ function validateProject(config) {
2044
2068
  try {
2045
2069
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
2046
2070
  issues.push({
2047
- level: "error",
2071
+ level: "warning",
2048
2072
  contentType: issue.contentTypeId,
2049
2073
  enSlug: issue.enSlug,
2050
2074
  locale: issue.locale,
@@ -2263,6 +2287,9 @@ function buildPageTranslationPrompt(input) {
2263
2287
  ...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
2264
2288
  "## Rules",
2265
2289
  ...input.resolved.rules.map((rule) => `- ${rule}`),
2290
+ ...input.slugStrategy === "localized" ? [
2291
+ `- 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.`
2292
+ ] : [],
2266
2293
  "",
2267
2294
  "## Output format",
2268
2295
  "Return ONLY valid JSON with keys:",
@@ -2346,9 +2373,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2346
2373
  function slugStrategyRules(slugStrategy, preserveTerms) {
2347
2374
  if (slugStrategy === "localized") {
2348
2375
  const rules = [
2349
- "Provide a localized URL slug in JSON field `slug`.",
2376
+ "Provide a URL slug in JSON field `slug` that is Localized into the target language.",
2377
+ "Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
2350
2378
  "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2351
- "For non-Latin locales, transliterate into Latin script."
2379
+ "For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
2380
+ "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.",
2381
+ 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".'
2352
2382
  ];
2353
2383
  if (preserveTerms?.length) {
2354
2384
  rules.push(
@@ -2381,6 +2411,95 @@ function resolveTranslateConfig(config, type) {
2381
2411
  return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
2382
2412
  }
2383
2413
 
2414
+ // src/translate/sanitize-mdx-jsx.ts
2415
+ function sanitizeMdxJsxAttributeQuotes(body) {
2416
+ let adjusted = false;
2417
+ let out = "";
2418
+ let i = 0;
2419
+ while (i < body.length) {
2420
+ if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
2421
+ out += body[i];
2422
+ i += 1;
2423
+ continue;
2424
+ }
2425
+ const tagStart = i;
2426
+ i += 1;
2427
+ while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
2428
+ const tagName = body.slice(tagStart + 1, i);
2429
+ let tagOut = `<${tagName}`;
2430
+ let tagAdjusted = false;
2431
+ while (i < body.length && body[i] !== ">" && body[i] !== "/") {
2432
+ while (i < body.length && /\s/.test(body[i] ?? "")) {
2433
+ tagOut += body[i];
2434
+ i += 1;
2435
+ }
2436
+ if (i >= body.length || body[i] === ">" || body[i] === "/") break;
2437
+ const attrStart = i;
2438
+ while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
2439
+ body.slice(attrStart, i);
2440
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2441
+ if (body[i] !== "=") {
2442
+ tagOut += body.slice(attrStart, i);
2443
+ continue;
2444
+ }
2445
+ tagOut += body.slice(attrStart, i);
2446
+ tagOut += "=";
2447
+ i += 1;
2448
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2449
+ const quote = body[i];
2450
+ if (quote !== '"' && quote !== "'") {
2451
+ continue;
2452
+ }
2453
+ if (quote === "'") {
2454
+ const valStart2 = i + 1;
2455
+ i += 1;
2456
+ while (i < body.length) {
2457
+ if (body[i] === "\\") {
2458
+ i += 2;
2459
+ continue;
2460
+ }
2461
+ if (body[i] === "'") break;
2462
+ i += 1;
2463
+ }
2464
+ tagOut += body.slice(valStart2 - 1, i + 1);
2465
+ i += 1;
2466
+ continue;
2467
+ }
2468
+ const valStart = i + 1;
2469
+ i += 1;
2470
+ let closeIdx = -1;
2471
+ for (let scan = valStart; scan < body.length; scan += 1) {
2472
+ if (body[scan] !== '"') continue;
2473
+ let j = scan + 1;
2474
+ while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
2475
+ if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
2476
+ closeIdx = scan;
2477
+ break;
2478
+ }
2479
+ }
2480
+ if (closeIdx === -1) {
2481
+ tagOut += body.slice(valStart - 1, i);
2482
+ break;
2483
+ }
2484
+ const value = body.slice(valStart, closeIdx);
2485
+ const hasInternalQuote = value.includes('"');
2486
+ if (hasInternalQuote) {
2487
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2488
+ tagOut += `'${escaped}'`;
2489
+ tagAdjusted = true;
2490
+ } else {
2491
+ tagOut += `"${value}"`;
2492
+ }
2493
+ i = closeIdx + 1;
2494
+ }
2495
+ if (tagAdjusted) adjusted = true;
2496
+ tagOut += body[i] ?? "";
2497
+ out += tagOut;
2498
+ i += 1;
2499
+ }
2500
+ return { body: out, adjusted };
2501
+ }
2502
+
2384
2503
  // src/translate/validate-translation.ts
2385
2504
  function formatZodIssues(error) {
2386
2505
  return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
@@ -2506,11 +2625,17 @@ async function translatePage(config, item, options = {}) {
2506
2625
  model,
2507
2626
  responseSchema: responseSchema ?? void 0
2508
2627
  });
2509
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2628
+ const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2629
+ const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
2630
+ const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
2631
+ const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
2510
2632
  const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
2511
2633
  if (!validated.ok) {
2512
2634
  throw new Error(`Translation validation failed: ${validated.error}`);
2513
2635
  }
2636
+ const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
2637
+ result.parsed.body
2638
+ );
2514
2639
  const writeDb = openStore(config, "readwrite");
2515
2640
  const snapshotId = recordEnSnapshot(
2516
2641
  config,
@@ -2529,7 +2654,7 @@ async function translatePage(config, item, options = {}) {
2529
2654
  locale: item.locale,
2530
2655
  slug,
2531
2656
  frontmatter: validated.frontmatter,
2532
- body: result.parsed.body,
2657
+ body: translatedBody,
2533
2658
  enHash: currentEnHash,
2534
2659
  translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2535
2660
  model: result.model,
@@ -2547,7 +2672,9 @@ async function translatePage(config, item, options = {}) {
2547
2672
  model: result.model,
2548
2673
  usage: result.usage,
2549
2674
  estimatedCostUsd,
2550
- durationMs: Date.now() - startedAt
2675
+ durationMs: Date.now() - startedAt,
2676
+ slugAdjusted,
2677
+ mdxAdjusted: mdxAdjusted || void 0
2551
2678
  };
2552
2679
  } catch (error) {
2553
2680
  return {
@@ -2698,6 +2825,7 @@ exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
2698
2825
  exports.getRelationTarget = getRelationTarget;
2699
2826
  exports.getStaticExportRoots = getStaticExportRoots;
2700
2827
  exports.isResolvedConfig = isResolvedConfig;
2828
+ exports.isRoutableType = isRoutableType;
2701
2829
  exports.loadConfigSync = loadConfigSync;
2702
2830
  exports.resolveConfig = resolveConfig;
2703
2831
  exports.resolveLocalesFromPreset = resolveLocalesFromPreset;