scribe-cms 0.0.9 → 0.0.11
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 +176 -116
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +174 -116
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +171 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +169 -115
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +118 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +116 -1
- package/dist/runtime.js.map +1 -1
- package/dist/src/core/types.d.ts +0 -2
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/loader/create-loader.d.ts.map +1 -1
- package/dist/src/translate/normalize-mdx-body.d.ts +9 -0
- package/dist/src/translate/normalize-mdx-body.d.ts.map +1 -0
- package/dist/src/translate/resolve-translate-config.d.ts.map +1 -1
- package/dist/src/translate/validate-mdx-body.d.ts +16 -0
- package/dist/src/translate/validate-mdx-body.d.ts.map +1 -0
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/src/validate/validate-redirects.d.ts.map +1 -1
- package/dist/studio/server.cjs +6 -0
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js +4 -0
- package/dist/studio/server.js.map +1 -1
- package/package.json +5 -2
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import fs2 from 'fs';
|
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import matter from 'gray-matter';
|
|
8
|
+
import remarkMdx from 'remark-mdx';
|
|
9
|
+
import remarkParse from 'remark-parse';
|
|
10
|
+
import { unified } from 'unified';
|
|
8
11
|
import { createJiti } from 'jiti';
|
|
9
12
|
import { createHash } from 'crypto';
|
|
10
13
|
import { GoogleGenAI } from '@google/genai';
|
|
@@ -712,6 +715,140 @@ function bulkLoadTranslations(db) {
|
|
|
712
715
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
713
716
|
}
|
|
714
717
|
|
|
718
|
+
// src/translate/validate-mdx-body.ts
|
|
719
|
+
init_esm_shims();
|
|
720
|
+
|
|
721
|
+
// src/translate/normalize-mdx-body.ts
|
|
722
|
+
init_esm_shims();
|
|
723
|
+
function needsMdxEscapeNormalization(body) {
|
|
724
|
+
return /\\n[ \t/>]|<[\w.-][^>]*\\n/.test(body);
|
|
725
|
+
}
|
|
726
|
+
function normalizeTranslatedMdxBody(body) {
|
|
727
|
+
if (!needsMdxEscapeNormalization(body)) {
|
|
728
|
+
return { body, adjusted: false };
|
|
729
|
+
}
|
|
730
|
+
const normalized = body.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
731
|
+
return { body: normalized, adjusted: normalized !== body };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/translate/sanitize-mdx-jsx.ts
|
|
735
|
+
init_esm_shims();
|
|
736
|
+
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
737
|
+
let adjusted = false;
|
|
738
|
+
let out = "";
|
|
739
|
+
let i = 0;
|
|
740
|
+
while (i < body.length) {
|
|
741
|
+
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
742
|
+
out += body[i];
|
|
743
|
+
i += 1;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const tagStart = i;
|
|
747
|
+
i += 1;
|
|
748
|
+
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
749
|
+
const tagName = body.slice(tagStart + 1, i);
|
|
750
|
+
let tagOut = `<${tagName}`;
|
|
751
|
+
let tagAdjusted = false;
|
|
752
|
+
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
753
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
754
|
+
tagOut += body[i];
|
|
755
|
+
i += 1;
|
|
756
|
+
}
|
|
757
|
+
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
758
|
+
const attrStart = i;
|
|
759
|
+
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
760
|
+
body.slice(attrStart, i);
|
|
761
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
762
|
+
if (body[i] !== "=") {
|
|
763
|
+
tagOut += body.slice(attrStart, i);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
tagOut += body.slice(attrStart, i);
|
|
767
|
+
tagOut += "=";
|
|
768
|
+
i += 1;
|
|
769
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
770
|
+
const quote = body[i];
|
|
771
|
+
if (quote !== '"' && quote !== "'") {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (quote === "'") {
|
|
775
|
+
const valStart2 = i + 1;
|
|
776
|
+
i += 1;
|
|
777
|
+
while (i < body.length) {
|
|
778
|
+
if (body[i] === "\\") {
|
|
779
|
+
i += 2;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (body[i] === "'") break;
|
|
783
|
+
i += 1;
|
|
784
|
+
}
|
|
785
|
+
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
786
|
+
i += 1;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const valStart = i + 1;
|
|
790
|
+
i += 1;
|
|
791
|
+
let closeIdx = -1;
|
|
792
|
+
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
793
|
+
if (body[scan] !== '"') continue;
|
|
794
|
+
let j = scan + 1;
|
|
795
|
+
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
796
|
+
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
797
|
+
closeIdx = scan;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (closeIdx === -1) {
|
|
802
|
+
tagOut += body.slice(valStart - 1, i);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
const value = body.slice(valStart, closeIdx);
|
|
806
|
+
const hasInternalQuote = value.includes('"');
|
|
807
|
+
if (hasInternalQuote) {
|
|
808
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
809
|
+
tagOut += `'${escaped}'`;
|
|
810
|
+
tagAdjusted = true;
|
|
811
|
+
} else {
|
|
812
|
+
tagOut += `"${value}"`;
|
|
813
|
+
}
|
|
814
|
+
i = closeIdx + 1;
|
|
815
|
+
}
|
|
816
|
+
if (tagAdjusted) adjusted = true;
|
|
817
|
+
tagOut += body[i] ?? "";
|
|
818
|
+
out += tagOut;
|
|
819
|
+
i += 1;
|
|
820
|
+
}
|
|
821
|
+
return { body: out, adjusted };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/translate/validate-mdx-body.ts
|
|
825
|
+
var parser = unified().use(remarkParse).use(remarkMdx);
|
|
826
|
+
function prepareTranslatedMdxBody(body) {
|
|
827
|
+
const normalized = normalizeTranslatedMdxBody(body);
|
|
828
|
+
const sanitized = sanitizeMdxJsxAttributeQuotes(normalized.body);
|
|
829
|
+
return {
|
|
830
|
+
body: sanitized.body,
|
|
831
|
+
adjusted: normalized.adjusted || sanitized.adjusted
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function validateMdxBody(body) {
|
|
835
|
+
try {
|
|
836
|
+
parser.parse(body);
|
|
837
|
+
return { ok: true };
|
|
838
|
+
} catch (error) {
|
|
839
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
840
|
+
return { ok: false, error: message };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function assertValidTranslatedMdxBody(body) {
|
|
844
|
+
const prepared = prepareTranslatedMdxBody(body);
|
|
845
|
+
const validated = validateMdxBody(prepared.body);
|
|
846
|
+
if (!validated.ok) {
|
|
847
|
+
throw new Error(`MDX validation failed: ${validated.error}`);
|
|
848
|
+
}
|
|
849
|
+
return prepared;
|
|
850
|
+
}
|
|
851
|
+
|
|
715
852
|
// src/loader/normalize-en.ts
|
|
716
853
|
init_esm_shims();
|
|
717
854
|
function normalizeEnFrontmatter(data) {
|
|
@@ -833,7 +970,7 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
833
970
|
noindex: seo.noindex,
|
|
834
971
|
canonicalPathOverride: seo.canonicalPathOverride,
|
|
835
972
|
frontmatter,
|
|
836
|
-
content: row.body
|
|
973
|
+
content: prepareTranslatedMdxBody(row.body).body
|
|
837
974
|
};
|
|
838
975
|
}
|
|
839
976
|
var DEV_REVALIDATE_MS = 1500;
|
|
@@ -1435,7 +1572,7 @@ function validateTypeRedirects(project) {
|
|
|
1435
1572
|
}
|
|
1436
1573
|
for (const entry of loaded.entries) {
|
|
1437
1574
|
for (const from of entry.fromSlugs) {
|
|
1438
|
-
if (from === entry.toSlug) {
|
|
1575
|
+
if (entry.kind === "same-type" && from === entry.toSlug) {
|
|
1439
1576
|
issues.push({
|
|
1440
1577
|
level: "error",
|
|
1441
1578
|
contentType: type.id,
|
|
@@ -1529,16 +1666,17 @@ function validateTypeRedirects(project) {
|
|
|
1529
1666
|
if (!targetType?.path) continue;
|
|
1530
1667
|
for (const from of entry.fromSlugs) {
|
|
1531
1668
|
if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1669
|
+
const targetAsSource = globalFrom.get(entry.toSlug);
|
|
1670
|
+
if (!targetAsSource) continue;
|
|
1671
|
+
const sameSlugCrossType = entry.kind === "cross-type" && entry.fromSlugs.includes(entry.toSlug) && targetAsSource.typeId === file.contentTypeId;
|
|
1672
|
+
if (sameSlugCrossType) continue;
|
|
1673
|
+
issues.push({
|
|
1674
|
+
level: "error",
|
|
1675
|
+
contentType: file.contentTypeId,
|
|
1676
|
+
enSlug: from,
|
|
1677
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1678
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
1679
|
+
});
|
|
1542
1680
|
}
|
|
1543
1681
|
}
|
|
1544
1682
|
}
|
|
@@ -1824,15 +1962,27 @@ function validateProject(config) {
|
|
|
1824
1962
|
message: issue.message
|
|
1825
1963
|
});
|
|
1826
1964
|
}
|
|
1965
|
+
const preparedBody = prepareTranslatedMdxBody(row.body).body;
|
|
1827
1966
|
for (const issue of validateDocumentAssets(config, {
|
|
1828
1967
|
contentType: type.id,
|
|
1829
1968
|
enSlug,
|
|
1830
1969
|
locale,
|
|
1831
1970
|
frontmatter: localeFm,
|
|
1832
|
-
body:
|
|
1971
|
+
body: preparedBody
|
|
1833
1972
|
})) {
|
|
1834
1973
|
issues.push(issue);
|
|
1835
1974
|
}
|
|
1975
|
+
const mdxValidation = validateMdxBody(preparedBody);
|
|
1976
|
+
if (!mdxValidation.ok) {
|
|
1977
|
+
issues.push({
|
|
1978
|
+
level: "error",
|
|
1979
|
+
contentType: type.id,
|
|
1980
|
+
enSlug,
|
|
1981
|
+
locale,
|
|
1982
|
+
field: "body",
|
|
1983
|
+
message: `Invalid MDX: ${mdxValidation.error}`
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1836
1986
|
}
|
|
1837
1987
|
}
|
|
1838
1988
|
try {
|
|
@@ -2200,24 +2350,21 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2200
2350
|
|
|
2201
2351
|
// src/translate/resolve-translate-config.ts
|
|
2202
2352
|
init_esm_shims();
|
|
2203
|
-
|
|
2353
|
+
var BUILTIN_TRANSLATE_RULES = [
|
|
2354
|
+
"Do not translate brand or product names unless the brand has a well-known localized name in the target market (rare). Keep the original spelling and capitalization.",
|
|
2355
|
+
"Return the MDX body with real line breaks; do not use JSON escape sequences like \\n or \\t in the body string.",
|
|
2356
|
+
'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 \\".'
|
|
2357
|
+
];
|
|
2358
|
+
function slugStrategyRules(slugStrategy) {
|
|
2204
2359
|
if (slugStrategy === "localized") {
|
|
2205
|
-
|
|
2360
|
+
return [
|
|
2206
2361
|
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2207
2362
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2208
2363
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2209
2364
|
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2210
2365
|
"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.",
|
|
2211
|
-
|
|
2366
|
+
"Preserve 4-digit years and proper nouns in slugs."
|
|
2212
2367
|
];
|
|
2213
|
-
if (preserveTerms?.length) {
|
|
2214
|
-
rules.push(
|
|
2215
|
-
`Preserve these terms verbatim in slugs (lowercase): ${preserveTerms.join(", ")}.`
|
|
2216
|
-
);
|
|
2217
|
-
} else {
|
|
2218
|
-
rules.push("Preserve 4-digit years and proper nouns in slugs.");
|
|
2219
|
-
}
|
|
2220
|
-
return rules;
|
|
2221
2368
|
}
|
|
2222
2369
|
return ["Do NOT output a slug \u2014 slugStrategy is fixed."];
|
|
2223
2370
|
}
|
|
@@ -2230,9 +2377,10 @@ function mergeTranslateConfig(project, type, slugStrategy) {
|
|
|
2230
2377
|
promptOverride: type?.prompt ?? project?.prompt,
|
|
2231
2378
|
context: mergeContext(project, type),
|
|
2232
2379
|
rules: [
|
|
2380
|
+
...BUILTIN_TRANSLATE_RULES,
|
|
2233
2381
|
...project?.rules ?? [],
|
|
2234
2382
|
...type?.rules ?? [],
|
|
2235
|
-
...slugStrategyRules(slugStrategy
|
|
2383
|
+
...slugStrategyRules(slugStrategy)
|
|
2236
2384
|
],
|
|
2237
2385
|
model: type?.model ?? project?.defaultModel
|
|
2238
2386
|
};
|
|
@@ -2241,96 +2389,6 @@ function resolveTranslateConfig(config, type) {
|
|
|
2241
2389
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2242
2390
|
}
|
|
2243
2391
|
|
|
2244
|
-
// src/translate/sanitize-mdx-jsx.ts
|
|
2245
|
-
init_esm_shims();
|
|
2246
|
-
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2247
|
-
let adjusted = false;
|
|
2248
|
-
let out = "";
|
|
2249
|
-
let i = 0;
|
|
2250
|
-
while (i < body.length) {
|
|
2251
|
-
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2252
|
-
out += body[i];
|
|
2253
|
-
i += 1;
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
const tagStart = i;
|
|
2257
|
-
i += 1;
|
|
2258
|
-
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2259
|
-
const tagName = body.slice(tagStart + 1, i);
|
|
2260
|
-
let tagOut = `<${tagName}`;
|
|
2261
|
-
let tagAdjusted = false;
|
|
2262
|
-
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2263
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2264
|
-
tagOut += body[i];
|
|
2265
|
-
i += 1;
|
|
2266
|
-
}
|
|
2267
|
-
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2268
|
-
const attrStart = i;
|
|
2269
|
-
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2270
|
-
body.slice(attrStart, i);
|
|
2271
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2272
|
-
if (body[i] !== "=") {
|
|
2273
|
-
tagOut += body.slice(attrStart, i);
|
|
2274
|
-
continue;
|
|
2275
|
-
}
|
|
2276
|
-
tagOut += body.slice(attrStart, i);
|
|
2277
|
-
tagOut += "=";
|
|
2278
|
-
i += 1;
|
|
2279
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2280
|
-
const quote = body[i];
|
|
2281
|
-
if (quote !== '"' && quote !== "'") {
|
|
2282
|
-
continue;
|
|
2283
|
-
}
|
|
2284
|
-
if (quote === "'") {
|
|
2285
|
-
const valStart2 = i + 1;
|
|
2286
|
-
i += 1;
|
|
2287
|
-
while (i < body.length) {
|
|
2288
|
-
if (body[i] === "\\") {
|
|
2289
|
-
i += 2;
|
|
2290
|
-
continue;
|
|
2291
|
-
}
|
|
2292
|
-
if (body[i] === "'") break;
|
|
2293
|
-
i += 1;
|
|
2294
|
-
}
|
|
2295
|
-
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2296
|
-
i += 1;
|
|
2297
|
-
continue;
|
|
2298
|
-
}
|
|
2299
|
-
const valStart = i + 1;
|
|
2300
|
-
i += 1;
|
|
2301
|
-
let closeIdx = -1;
|
|
2302
|
-
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2303
|
-
if (body[scan] !== '"') continue;
|
|
2304
|
-
let j = scan + 1;
|
|
2305
|
-
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2306
|
-
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2307
|
-
closeIdx = scan;
|
|
2308
|
-
break;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
if (closeIdx === -1) {
|
|
2312
|
-
tagOut += body.slice(valStart - 1, i);
|
|
2313
|
-
break;
|
|
2314
|
-
}
|
|
2315
|
-
const value = body.slice(valStart, closeIdx);
|
|
2316
|
-
const hasInternalQuote = value.includes('"');
|
|
2317
|
-
if (hasInternalQuote) {
|
|
2318
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2319
|
-
tagOut += `'${escaped}'`;
|
|
2320
|
-
tagAdjusted = true;
|
|
2321
|
-
} else {
|
|
2322
|
-
tagOut += `"${value}"`;
|
|
2323
|
-
}
|
|
2324
|
-
i = closeIdx + 1;
|
|
2325
|
-
}
|
|
2326
|
-
if (tagAdjusted) adjusted = true;
|
|
2327
|
-
tagOut += body[i] ?? "";
|
|
2328
|
-
out += tagOut;
|
|
2329
|
-
i += 1;
|
|
2330
|
-
}
|
|
2331
|
-
return { body: out, adjusted };
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
2392
|
// src/translate/validate-translation.ts
|
|
2335
2393
|
init_esm_shims();
|
|
2336
2394
|
function formatZodIssues(error) {
|
|
@@ -2465,7 +2523,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2465
2523
|
if (!validated.ok) {
|
|
2466
2524
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2467
2525
|
}
|
|
2468
|
-
const { body: translatedBody, adjusted: mdxAdjusted } =
|
|
2526
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
|
|
2469
2527
|
result.parsed.body
|
|
2470
2528
|
);
|
|
2471
2529
|
const writeDb = openStore(config, "readwrite");
|