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.cjs +170 -30
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +169 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/translate-progress.d.ts.map +1 -1
- package/dist/index.cjs +145 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -18
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +5 -0
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +5 -1
- 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/core/types.d.ts +3 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/create-project.d.ts.map +1 -1
- package/dist/src/create-scribe.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts +6 -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/translate/sanitize-mdx-jsx.d.ts +9 -0
- package/dist/src/translate/sanitize-mdx-jsx.d.ts.map +1 -0
- package/dist/src/validate/validate-slug-suffix.d.ts.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
},
|