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/cli/index.js CHANGED
@@ -11,11 +11,11 @@ import { GoogleGenAI } from '@google/genai';
11
11
  import dotenv from 'dotenv';
12
12
  import { serve } from '@hono/node-server';
13
13
  import { Hono } from 'hono';
14
+ import { CancelPromptError } from '@inquirer/core';
14
15
  import { select, checkbox } from '@inquirer/prompts';
15
16
 
16
17
  var __defProp = Object.defineProperty;
17
18
  var __getOwnPropNames = Object.getOwnPropertyNames;
18
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
19
19
  var __esm = (fn, res) => function __init() {
20
20
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
21
  };
@@ -23,7 +23,6 @@ var __export = (target, all) => {
23
23
  for (var name in all)
24
24
  __defProp(target, name, { get: all[name], enumerable: true });
25
25
  };
26
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
27
26
  var init_esm_shims = __esm({
28
27
  "../../node_modules/tsup/assets/esm_shims.js"() {
29
28
  }
@@ -1209,6 +1208,9 @@ function createProject(config) {
1209
1208
  getType: getRuntime,
1210
1209
  listTypes() {
1211
1210
  return Array.from(runtimes.values());
1211
+ },
1212
+ listRoutableTypes() {
1213
+ return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
1212
1214
  }
1213
1215
  };
1214
1216
  }
@@ -1627,22 +1629,45 @@ function validateDocumentAssets(config, input) {
1627
1629
 
1628
1630
  // src/validate/validate-slug-suffix.ts
1629
1631
  init_esm_shims();
1632
+
1633
+ // src/core/localized-slug.ts
1634
+ init_esm_shims();
1635
+ function findLocaleSuffixInSlug(slug, localeCodes) {
1636
+ const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
1637
+ for (const code of sorted) {
1638
+ if (slug.endsWith(`-${code}`)) {
1639
+ return code;
1640
+ }
1641
+ }
1642
+ return void 0;
1643
+ }
1644
+ function stripLocaleSuffixFromSlug(slug, localeCodes) {
1645
+ const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
1646
+ if (!matchedCode) {
1647
+ return { slug, stripped: false };
1648
+ }
1649
+ return {
1650
+ slug: slug.slice(0, -(matchedCode.length + 1)),
1651
+ stripped: true,
1652
+ matchedCode
1653
+ };
1654
+ }
1655
+
1656
+ // src/validate/validate-slug-suffix.ts
1630
1657
  function validateTranslationSlugSuffixes(config, db) {
1631
1658
  const issues = [];
1632
1659
  const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
1633
1660
  for (const row of bulkLoadTranslations(db)) {
1634
1661
  if (row.locale === config.defaultLocale) continue;
1635
- for (const code of localeCodes) {
1636
- if (row.slug.endsWith(`-${code}`)) {
1637
- issues.push({
1638
- contentTypeId: row.content_type,
1639
- enSlug: row.en_slug,
1640
- locale: row.locale,
1641
- slug: row.slug,
1642
- message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
1643
- });
1644
- break;
1645
- }
1662
+ const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
1663
+ if (matchedCode) {
1664
+ issues.push({
1665
+ contentTypeId: row.content_type,
1666
+ enSlug: row.en_slug,
1667
+ locale: row.locale,
1668
+ slug: row.slug,
1669
+ message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
1670
+ });
1646
1671
  }
1647
1672
  }
1648
1673
  return issues;
@@ -1790,7 +1815,7 @@ function validateProject(config) {
1790
1815
  try {
1791
1816
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
1792
1817
  issues.push({
1793
- level: "error",
1818
+ level: "warning",
1794
1819
  contentType: issue.contentTypeId,
1795
1820
  enSlug: issue.enSlug,
1796
1821
  locale: issue.locale,
@@ -2041,6 +2066,9 @@ function buildPageTranslationPrompt(input) {
2041
2066
  ...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
2042
2067
  "## Rules",
2043
2068
  ...input.resolved.rules.map((rule) => `- ${rule}`),
2069
+ ...input.slugStrategy === "localized" ? [
2070
+ `- 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.`
2071
+ ] : [],
2044
2072
  "",
2045
2073
  "## Output format",
2046
2074
  "Return ONLY valid JSON with keys:",
@@ -2128,9 +2156,12 @@ init_esm_shims();
2128
2156
  function slugStrategyRules(slugStrategy, preserveTerms) {
2129
2157
  if (slugStrategy === "localized") {
2130
2158
  const rules = [
2131
- "Provide a localized URL slug in JSON field `slug`.",
2159
+ "Provide a URL slug in JSON field `slug` that is Localized into the target language.",
2160
+ "Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
2132
2161
  "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2133
- "For non-Latin locales, transliterate into Latin script."
2162
+ "For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
2163
+ "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.",
2164
+ '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 \\".'
2134
2165
  ];
2135
2166
  if (preserveTerms?.length) {
2136
2167
  rules.push(
@@ -2163,6 +2194,96 @@ function resolveTranslateConfig(config, type) {
2163
2194
  return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
2164
2195
  }
2165
2196
 
2197
+ // src/translate/sanitize-mdx-jsx.ts
2198
+ init_esm_shims();
2199
+ function sanitizeMdxJsxAttributeQuotes(body) {
2200
+ let adjusted = false;
2201
+ let out = "";
2202
+ let i = 0;
2203
+ while (i < body.length) {
2204
+ if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
2205
+ out += body[i];
2206
+ i += 1;
2207
+ continue;
2208
+ }
2209
+ const tagStart = i;
2210
+ i += 1;
2211
+ while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
2212
+ const tagName = body.slice(tagStart + 1, i);
2213
+ let tagOut = `<${tagName}`;
2214
+ let tagAdjusted = false;
2215
+ while (i < body.length && body[i] !== ">" && body[i] !== "/") {
2216
+ while (i < body.length && /\s/.test(body[i] ?? "")) {
2217
+ tagOut += body[i];
2218
+ i += 1;
2219
+ }
2220
+ if (i >= body.length || body[i] === ">" || body[i] === "/") break;
2221
+ const attrStart = i;
2222
+ while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
2223
+ body.slice(attrStart, i);
2224
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2225
+ if (body[i] !== "=") {
2226
+ tagOut += body.slice(attrStart, i);
2227
+ continue;
2228
+ }
2229
+ tagOut += body.slice(attrStart, i);
2230
+ tagOut += "=";
2231
+ i += 1;
2232
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2233
+ const quote = body[i];
2234
+ if (quote !== '"' && quote !== "'") {
2235
+ continue;
2236
+ }
2237
+ if (quote === "'") {
2238
+ const valStart2 = i + 1;
2239
+ i += 1;
2240
+ while (i < body.length) {
2241
+ if (body[i] === "\\") {
2242
+ i += 2;
2243
+ continue;
2244
+ }
2245
+ if (body[i] === "'") break;
2246
+ i += 1;
2247
+ }
2248
+ tagOut += body.slice(valStart2 - 1, i + 1);
2249
+ i += 1;
2250
+ continue;
2251
+ }
2252
+ const valStart = i + 1;
2253
+ i += 1;
2254
+ let closeIdx = -1;
2255
+ for (let scan = valStart; scan < body.length; scan += 1) {
2256
+ if (body[scan] !== '"') continue;
2257
+ let j = scan + 1;
2258
+ while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
2259
+ if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
2260
+ closeIdx = scan;
2261
+ break;
2262
+ }
2263
+ }
2264
+ if (closeIdx === -1) {
2265
+ tagOut += body.slice(valStart - 1, i);
2266
+ break;
2267
+ }
2268
+ const value = body.slice(valStart, closeIdx);
2269
+ const hasInternalQuote = value.includes('"');
2270
+ if (hasInternalQuote) {
2271
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2272
+ tagOut += `'${escaped}'`;
2273
+ tagAdjusted = true;
2274
+ } else {
2275
+ tagOut += `"${value}"`;
2276
+ }
2277
+ i = closeIdx + 1;
2278
+ }
2279
+ if (tagAdjusted) adjusted = true;
2280
+ tagOut += body[i] ?? "";
2281
+ out += tagOut;
2282
+ i += 1;
2283
+ }
2284
+ return { body: out, adjusted };
2285
+ }
2286
+
2166
2287
  // src/translate/validate-translation.ts
2167
2288
  init_esm_shims();
2168
2289
  function formatZodIssues(error) {
@@ -2289,11 +2410,17 @@ async function translatePage(config, item, options = {}) {
2289
2410
  model,
2290
2411
  responseSchema: responseSchema ?? void 0
2291
2412
  });
2292
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2413
+ const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2414
+ const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
2415
+ const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
2416
+ const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
2293
2417
  const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
2294
2418
  if (!validated.ok) {
2295
2419
  throw new Error(`Translation validation failed: ${validated.error}`);
2296
2420
  }
2421
+ const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
2422
+ result.parsed.body
2423
+ );
2297
2424
  const writeDb = openStore(config, "readwrite");
2298
2425
  const snapshotId = recordEnSnapshot(
2299
2426
  config,
@@ -2312,7 +2439,7 @@ async function translatePage(config, item, options = {}) {
2312
2439
  locale: item.locale,
2313
2440
  slug,
2314
2441
  frontmatter: validated.frontmatter,
2315
- body: result.parsed.body,
2442
+ body: translatedBody,
2316
2443
  enHash: currentEnHash,
2317
2444
  translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2318
2445
  model: result.model,
@@ -2330,7 +2457,9 @@ async function translatePage(config, item, options = {}) {
2330
2457
  model: result.model,
2331
2458
  usage: result.usage,
2332
2459
  estimatedCostUsd,
2333
- durationMs: Date.now() - startedAt
2460
+ durationMs: Date.now() - startedAt,
2461
+ slugAdjusted,
2462
+ mdxAdjusted: mdxAdjusted || void 0
2334
2463
  };
2335
2464
  } catch (error) {
2336
2465
  return {
@@ -2956,16 +3085,6 @@ async function startStudio(project, options = {}) {
2956
3085
 
2957
3086
  // cli/prompt-translate.ts
2958
3087
  init_esm_shims();
2959
-
2960
- // ../../node_modules/@inquirer/core/dist/lib/errors.js
2961
- init_esm_shims();
2962
- var CancelPromptError = class extends Error {
2963
- constructor() {
2964
- super(...arguments);
2965
- __publicField(this, "name", "CancelPromptError");
2966
- __publicField(this, "message", "Prompt was canceled");
2967
- }
2968
- };
2969
3088
  function isInteractive() {
2970
3089
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
2971
3090
  }
@@ -3077,8 +3196,17 @@ function statusForResult(result, dryRun) {
3077
3196
  if (dryRun) return yellow("would translate");
3078
3197
  return green("translated");
3079
3198
  }
3199
+ function slugAdjustedMessage(result) {
3200
+ if (!result.slugAdjusted) return void 0;
3201
+ const { from, to, matchedCode } = result.slugAdjusted;
3202
+ return yellow(
3203
+ `slug adjusted: "${from}" \u2192 "${to}" (stripped -${matchedCode})`
3204
+ );
3205
+ }
3080
3206
  function detailForResult(result) {
3081
3207
  const parts = [];
3208
+ const slugMsg = slugAdjustedMessage(result);
3209
+ if (slugMsg) parts.push(slugMsg);
3082
3210
  if (result.durationMs !== void 0) parts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
3083
3211
  if (result.usage) {
3084
3212
  parts.push(`${formatTokenCount(result.usage.inputTokens)} in`);
@@ -3112,6 +3240,10 @@ function createTranslateProgressReporter(options = {}) {
3112
3240
  const status = statusForResult(event.result, dryRun);
3113
3241
  const detail = detailForResult(event.result);
3114
3242
  console.log(`${label}: ${status}${detail ? ` (${detail.replace(/\x1b\[[0-9;]*m/g, "")})` : ""}`);
3243
+ const slugMsg = slugAdjustedMessage(event.result);
3244
+ if (slugMsg) {
3245
+ console.log(`[warning] ${labelForResult(event.result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`);
3246
+ }
3115
3247
  if (event.result.failed && event.result.error) {
3116
3248
  console.error(event.result.error);
3117
3249
  }
@@ -3215,6 +3347,14 @@ function createTranslateProgressReporter(options = {}) {
3215
3347
  for (const result of event.results.filter((entry) => entry.failed)) {
3216
3348
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
3217
3349
  }
3350
+ for (const result of event.results.filter((entry) => entry.slugAdjusted)) {
3351
+ const slugMsg = slugAdjustedMessage(result);
3352
+ if (slugMsg) {
3353
+ process.stdout.write(
3354
+ "\x1B[2K" + yellow(`[warning] ${labelForResult(result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`) + "\n"
3355
+ );
3356
+ }
3357
+ }
3218
3358
  break;
3219
3359
  }
3220
3360
  },