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.
package/dist/index.js CHANGED
@@ -1176,6 +1176,9 @@ function createProject(config) {
1176
1176
  getType: getRuntime,
1177
1177
  listTypes() {
1178
1178
  return Array.from(runtimes.values());
1179
+ },
1180
+ listRoutableTypes() {
1181
+ return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
1179
1182
  }
1180
1183
  };
1181
1184
  }
@@ -1671,6 +1674,7 @@ function createScribe(input) {
1671
1674
  project,
1672
1675
  getType: project.getType,
1673
1676
  listTypes: project.listTypes,
1677
+ listRoutableTypes: project.listRoutableTypes,
1674
1678
  sitemap(options) {
1675
1679
  return generateSitemap(project, options);
1676
1680
  }
@@ -1869,23 +1873,43 @@ function validateDocumentAssets(config, input) {
1869
1873
  return issues;
1870
1874
  }
1871
1875
 
1876
+ // src/core/localized-slug.ts
1877
+ function findLocaleSuffixInSlug(slug, localeCodes) {
1878
+ const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
1879
+ for (const code of sorted) {
1880
+ if (slug.endsWith(`-${code}`)) {
1881
+ return code;
1882
+ }
1883
+ }
1884
+ return void 0;
1885
+ }
1886
+ function stripLocaleSuffixFromSlug(slug, localeCodes) {
1887
+ const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
1888
+ if (!matchedCode) {
1889
+ return { slug, stripped: false };
1890
+ }
1891
+ return {
1892
+ slug: slug.slice(0, -(matchedCode.length + 1)),
1893
+ stripped: true,
1894
+ matchedCode
1895
+ };
1896
+ }
1897
+
1872
1898
  // src/validate/validate-slug-suffix.ts
1873
1899
  function validateTranslationSlugSuffixes(config, db) {
1874
1900
  const issues = [];
1875
1901
  const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
1876
1902
  for (const row of bulkLoadTranslations(db)) {
1877
1903
  if (row.locale === config.defaultLocale) continue;
1878
- for (const code of localeCodes) {
1879
- if (row.slug.endsWith(`-${code}`)) {
1880
- issues.push({
1881
- contentTypeId: row.content_type,
1882
- enSlug: row.en_slug,
1883
- locale: row.locale,
1884
- slug: row.slug,
1885
- message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
1886
- });
1887
- break;
1888
- }
1904
+ const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
1905
+ if (matchedCode) {
1906
+ issues.push({
1907
+ contentTypeId: row.content_type,
1908
+ enSlug: row.en_slug,
1909
+ locale: row.locale,
1910
+ slug: row.slug,
1911
+ message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
1912
+ });
1889
1913
  }
1890
1914
  }
1891
1915
  return issues;
@@ -2033,7 +2057,7 @@ function validateProject(config) {
2033
2057
  try {
2034
2058
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
2035
2059
  issues.push({
2036
- level: "error",
2060
+ level: "warning",
2037
2061
  contentType: issue.contentTypeId,
2038
2062
  enSlug: issue.enSlug,
2039
2063
  locale: issue.locale,
@@ -2252,6 +2276,9 @@ function buildPageTranslationPrompt(input) {
2252
2276
  ...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
2253
2277
  "## Rules",
2254
2278
  ...input.resolved.rules.map((rule) => `- ${rule}`),
2279
+ ...input.slugStrategy === "localized" ? [
2280
+ `- 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.`
2281
+ ] : [],
2255
2282
  "",
2256
2283
  "## Output format",
2257
2284
  "Return ONLY valid JSON with keys:",
@@ -2335,9 +2362,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2335
2362
  function slugStrategyRules(slugStrategy, preserveTerms) {
2336
2363
  if (slugStrategy === "localized") {
2337
2364
  const rules = [
2338
- "Provide a localized URL slug in JSON field `slug`.",
2365
+ "Provide a URL slug in JSON field `slug` that is Localized into the target language.",
2366
+ "Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
2339
2367
  "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2340
- "For non-Latin locales, transliterate into Latin script."
2368
+ "For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
2369
+ "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.",
2370
+ '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 \\".'
2341
2371
  ];
2342
2372
  if (preserveTerms?.length) {
2343
2373
  rules.push(
@@ -2370,6 +2400,95 @@ function resolveTranslateConfig(config, type) {
2370
2400
  return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
2371
2401
  }
2372
2402
 
2403
+ // src/translate/sanitize-mdx-jsx.ts
2404
+ function sanitizeMdxJsxAttributeQuotes(body) {
2405
+ let adjusted = false;
2406
+ let out = "";
2407
+ let i = 0;
2408
+ while (i < body.length) {
2409
+ if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
2410
+ out += body[i];
2411
+ i += 1;
2412
+ continue;
2413
+ }
2414
+ const tagStart = i;
2415
+ i += 1;
2416
+ while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
2417
+ const tagName = body.slice(tagStart + 1, i);
2418
+ let tagOut = `<${tagName}`;
2419
+ let tagAdjusted = false;
2420
+ while (i < body.length && body[i] !== ">" && body[i] !== "/") {
2421
+ while (i < body.length && /\s/.test(body[i] ?? "")) {
2422
+ tagOut += body[i];
2423
+ i += 1;
2424
+ }
2425
+ if (i >= body.length || body[i] === ">" || body[i] === "/") break;
2426
+ const attrStart = i;
2427
+ while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
2428
+ body.slice(attrStart, i);
2429
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2430
+ if (body[i] !== "=") {
2431
+ tagOut += body.slice(attrStart, i);
2432
+ continue;
2433
+ }
2434
+ tagOut += body.slice(attrStart, i);
2435
+ tagOut += "=";
2436
+ i += 1;
2437
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2438
+ const quote = body[i];
2439
+ if (quote !== '"' && quote !== "'") {
2440
+ continue;
2441
+ }
2442
+ if (quote === "'") {
2443
+ const valStart2 = i + 1;
2444
+ i += 1;
2445
+ while (i < body.length) {
2446
+ if (body[i] === "\\") {
2447
+ i += 2;
2448
+ continue;
2449
+ }
2450
+ if (body[i] === "'") break;
2451
+ i += 1;
2452
+ }
2453
+ tagOut += body.slice(valStart2 - 1, i + 1);
2454
+ i += 1;
2455
+ continue;
2456
+ }
2457
+ const valStart = i + 1;
2458
+ i += 1;
2459
+ let closeIdx = -1;
2460
+ for (let scan = valStart; scan < body.length; scan += 1) {
2461
+ if (body[scan] !== '"') continue;
2462
+ let j = scan + 1;
2463
+ while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
2464
+ if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
2465
+ closeIdx = scan;
2466
+ break;
2467
+ }
2468
+ }
2469
+ if (closeIdx === -1) {
2470
+ tagOut += body.slice(valStart - 1, i);
2471
+ break;
2472
+ }
2473
+ const value = body.slice(valStart, closeIdx);
2474
+ const hasInternalQuote = value.includes('"');
2475
+ if (hasInternalQuote) {
2476
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2477
+ tagOut += `'${escaped}'`;
2478
+ tagAdjusted = true;
2479
+ } else {
2480
+ tagOut += `"${value}"`;
2481
+ }
2482
+ i = closeIdx + 1;
2483
+ }
2484
+ if (tagAdjusted) adjusted = true;
2485
+ tagOut += body[i] ?? "";
2486
+ out += tagOut;
2487
+ i += 1;
2488
+ }
2489
+ return { body: out, adjusted };
2490
+ }
2491
+
2373
2492
  // src/translate/validate-translation.ts
2374
2493
  function formatZodIssues(error) {
2375
2494
  return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
@@ -2495,11 +2614,17 @@ async function translatePage(config, item, options = {}) {
2495
2614
  model,
2496
2615
  responseSchema: responseSchema ?? void 0
2497
2616
  });
2498
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2617
+ const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2618
+ const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
2619
+ const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
2620
+ const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
2499
2621
  const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
2500
2622
  if (!validated.ok) {
2501
2623
  throw new Error(`Translation validation failed: ${validated.error}`);
2502
2624
  }
2625
+ const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
2626
+ result.parsed.body
2627
+ );
2503
2628
  const writeDb = openStore(config, "readwrite");
2504
2629
  const snapshotId = recordEnSnapshot(
2505
2630
  config,
@@ -2518,7 +2643,7 @@ async function translatePage(config, item, options = {}) {
2518
2643
  locale: item.locale,
2519
2644
  slug,
2520
2645
  frontmatter: validated.frontmatter,
2521
- body: result.parsed.body,
2646
+ body: translatedBody,
2522
2647
  enHash: currentEnHash,
2523
2648
  translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2524
2649
  model: result.model,
@@ -2536,7 +2661,9 @@ async function translatePage(config, item, options = {}) {
2536
2661
  model: result.model,
2537
2662
  usage: result.usage,
2538
2663
  estimatedCostUsd,
2539
- durationMs: Date.now() - startedAt
2664
+ durationMs: Date.now() - startedAt,
2665
+ slugAdjusted,
2666
+ mdxAdjusted: mdxAdjusted || void 0
2540
2667
  };
2541
2668
  } catch (error) {
2542
2669
  return {
@@ -2671,6 +2798,6 @@ function writeStaticRawExports(project, options = {}) {
2671
2798
  return { exports, written: exports.length };
2672
2799
  }
2673
2800
 
2674
- export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
2801
+ export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, isRoutableType, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
2675
2802
  //# sourceMappingURL=index.js.map
2676
2803
  //# sourceMappingURL=index.js.map