scribe-cms 0.0.10 → 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 +164 -105
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +162 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +159 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +157 -104
- 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/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/index.js
CHANGED
|
@@ -3,6 +3,9 @@ import path3 from 'path';
|
|
|
3
3
|
import fs2 from 'fs';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
|
+
import remarkMdx from 'remark-mdx';
|
|
7
|
+
import remarkParse from 'remark-parse';
|
|
8
|
+
import { unified } from 'unified';
|
|
6
9
|
import { createJiti } from 'jiti';
|
|
7
10
|
import { createHash } from 'crypto';
|
|
8
11
|
import { GoogleGenAI } from '@google/genai';
|
|
@@ -687,6 +690,135 @@ function bulkLoadTranslations(db) {
|
|
|
687
690
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
688
691
|
}
|
|
689
692
|
|
|
693
|
+
// src/translate/normalize-mdx-body.ts
|
|
694
|
+
function needsMdxEscapeNormalization(body) {
|
|
695
|
+
return /\\n[ \t/>]|<[\w.-][^>]*\\n/.test(body);
|
|
696
|
+
}
|
|
697
|
+
function normalizeTranslatedMdxBody(body) {
|
|
698
|
+
if (!needsMdxEscapeNormalization(body)) {
|
|
699
|
+
return { body, adjusted: false };
|
|
700
|
+
}
|
|
701
|
+
const normalized = body.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
702
|
+
return { body: normalized, adjusted: normalized !== body };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/translate/sanitize-mdx-jsx.ts
|
|
706
|
+
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
707
|
+
let adjusted = false;
|
|
708
|
+
let out = "";
|
|
709
|
+
let i = 0;
|
|
710
|
+
while (i < body.length) {
|
|
711
|
+
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
712
|
+
out += body[i];
|
|
713
|
+
i += 1;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const tagStart = i;
|
|
717
|
+
i += 1;
|
|
718
|
+
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
719
|
+
const tagName = body.slice(tagStart + 1, i);
|
|
720
|
+
let tagOut = `<${tagName}`;
|
|
721
|
+
let tagAdjusted = false;
|
|
722
|
+
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
723
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
724
|
+
tagOut += body[i];
|
|
725
|
+
i += 1;
|
|
726
|
+
}
|
|
727
|
+
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
728
|
+
const attrStart = i;
|
|
729
|
+
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
730
|
+
body.slice(attrStart, i);
|
|
731
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
732
|
+
if (body[i] !== "=") {
|
|
733
|
+
tagOut += body.slice(attrStart, i);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
tagOut += body.slice(attrStart, i);
|
|
737
|
+
tagOut += "=";
|
|
738
|
+
i += 1;
|
|
739
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
740
|
+
const quote = body[i];
|
|
741
|
+
if (quote !== '"' && quote !== "'") {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (quote === "'") {
|
|
745
|
+
const valStart2 = i + 1;
|
|
746
|
+
i += 1;
|
|
747
|
+
while (i < body.length) {
|
|
748
|
+
if (body[i] === "\\") {
|
|
749
|
+
i += 2;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (body[i] === "'") break;
|
|
753
|
+
i += 1;
|
|
754
|
+
}
|
|
755
|
+
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
756
|
+
i += 1;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const valStart = i + 1;
|
|
760
|
+
i += 1;
|
|
761
|
+
let closeIdx = -1;
|
|
762
|
+
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
763
|
+
if (body[scan] !== '"') continue;
|
|
764
|
+
let j = scan + 1;
|
|
765
|
+
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
766
|
+
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
767
|
+
closeIdx = scan;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (closeIdx === -1) {
|
|
772
|
+
tagOut += body.slice(valStart - 1, i);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
const value = body.slice(valStart, closeIdx);
|
|
776
|
+
const hasInternalQuote = value.includes('"');
|
|
777
|
+
if (hasInternalQuote) {
|
|
778
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
779
|
+
tagOut += `'${escaped}'`;
|
|
780
|
+
tagAdjusted = true;
|
|
781
|
+
} else {
|
|
782
|
+
tagOut += `"${value}"`;
|
|
783
|
+
}
|
|
784
|
+
i = closeIdx + 1;
|
|
785
|
+
}
|
|
786
|
+
if (tagAdjusted) adjusted = true;
|
|
787
|
+
tagOut += body[i] ?? "";
|
|
788
|
+
out += tagOut;
|
|
789
|
+
i += 1;
|
|
790
|
+
}
|
|
791
|
+
return { body: out, adjusted };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/translate/validate-mdx-body.ts
|
|
795
|
+
var parser = unified().use(remarkParse).use(remarkMdx);
|
|
796
|
+
function prepareTranslatedMdxBody(body) {
|
|
797
|
+
const normalized = normalizeTranslatedMdxBody(body);
|
|
798
|
+
const sanitized = sanitizeMdxJsxAttributeQuotes(normalized.body);
|
|
799
|
+
return {
|
|
800
|
+
body: sanitized.body,
|
|
801
|
+
adjusted: normalized.adjusted || sanitized.adjusted
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function validateMdxBody(body) {
|
|
805
|
+
try {
|
|
806
|
+
parser.parse(body);
|
|
807
|
+
return { ok: true };
|
|
808
|
+
} catch (error) {
|
|
809
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
810
|
+
return { ok: false, error: message };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function assertValidTranslatedMdxBody(body) {
|
|
814
|
+
const prepared = prepareTranslatedMdxBody(body);
|
|
815
|
+
const validated = validateMdxBody(prepared.body);
|
|
816
|
+
if (!validated.ok) {
|
|
817
|
+
throw new Error(`MDX validation failed: ${validated.error}`);
|
|
818
|
+
}
|
|
819
|
+
return prepared;
|
|
820
|
+
}
|
|
821
|
+
|
|
690
822
|
// src/loader/normalize-en.ts
|
|
691
823
|
function normalizeEnFrontmatter(data) {
|
|
692
824
|
const out = { ...data };
|
|
@@ -807,7 +939,7 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
807
939
|
noindex: seo.noindex,
|
|
808
940
|
canonicalPathOverride: seo.canonicalPathOverride,
|
|
809
941
|
frontmatter,
|
|
810
|
-
content: row.body
|
|
942
|
+
content: prepareTranslatedMdxBody(row.body).body
|
|
811
943
|
};
|
|
812
944
|
}
|
|
813
945
|
var DEV_REVALIDATE_MS = 1500;
|
|
@@ -2148,15 +2280,27 @@ function validateProject(config) {
|
|
|
2148
2280
|
message: issue.message
|
|
2149
2281
|
});
|
|
2150
2282
|
}
|
|
2283
|
+
const preparedBody = prepareTranslatedMdxBody(row.body).body;
|
|
2151
2284
|
for (const issue of validateDocumentAssets(config, {
|
|
2152
2285
|
contentType: type.id,
|
|
2153
2286
|
enSlug,
|
|
2154
2287
|
locale,
|
|
2155
2288
|
frontmatter: localeFm,
|
|
2156
|
-
body:
|
|
2289
|
+
body: preparedBody
|
|
2157
2290
|
})) {
|
|
2158
2291
|
issues.push(issue);
|
|
2159
2292
|
}
|
|
2293
|
+
const mdxValidation = validateMdxBody(preparedBody);
|
|
2294
|
+
if (!mdxValidation.ok) {
|
|
2295
|
+
issues.push({
|
|
2296
|
+
level: "error",
|
|
2297
|
+
contentType: type.id,
|
|
2298
|
+
enSlug,
|
|
2299
|
+
locale,
|
|
2300
|
+
field: "body",
|
|
2301
|
+
message: `Invalid MDX: ${mdxValidation.error}`
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2160
2304
|
}
|
|
2161
2305
|
}
|
|
2162
2306
|
try {
|
|
@@ -2488,24 +2632,21 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2488
2632
|
}
|
|
2489
2633
|
|
|
2490
2634
|
// src/translate/resolve-translate-config.ts
|
|
2491
|
-
|
|
2635
|
+
var BUILTIN_TRANSLATE_RULES = [
|
|
2636
|
+
"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.",
|
|
2637
|
+
"Return the MDX body with real line breaks; do not use JSON escape sequences like \\n or \\t in the body string.",
|
|
2638
|
+
'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 \\".'
|
|
2639
|
+
];
|
|
2640
|
+
function slugStrategyRules(slugStrategy) {
|
|
2492
2641
|
if (slugStrategy === "localized") {
|
|
2493
|
-
|
|
2642
|
+
return [
|
|
2494
2643
|
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2495
2644
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2496
2645
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2497
2646
|
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2498
2647
|
"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.",
|
|
2499
|
-
|
|
2648
|
+
"Preserve 4-digit years and proper nouns in slugs."
|
|
2500
2649
|
];
|
|
2501
|
-
if (preserveTerms?.length) {
|
|
2502
|
-
rules.push(
|
|
2503
|
-
`Preserve these terms verbatim in slugs (lowercase): ${preserveTerms.join(", ")}.`
|
|
2504
|
-
);
|
|
2505
|
-
} else {
|
|
2506
|
-
rules.push("Preserve 4-digit years and proper nouns in slugs.");
|
|
2507
|
-
}
|
|
2508
|
-
return rules;
|
|
2509
2650
|
}
|
|
2510
2651
|
return ["Do NOT output a slug \u2014 slugStrategy is fixed."];
|
|
2511
2652
|
}
|
|
@@ -2518,9 +2659,10 @@ function mergeTranslateConfig(project, type, slugStrategy) {
|
|
|
2518
2659
|
promptOverride: type?.prompt ?? project?.prompt,
|
|
2519
2660
|
context: mergeContext(project, type),
|
|
2520
2661
|
rules: [
|
|
2662
|
+
...BUILTIN_TRANSLATE_RULES,
|
|
2521
2663
|
...project?.rules ?? [],
|
|
2522
2664
|
...type?.rules ?? [],
|
|
2523
|
-
...slugStrategyRules(slugStrategy
|
|
2665
|
+
...slugStrategyRules(slugStrategy)
|
|
2524
2666
|
],
|
|
2525
2667
|
model: type?.model ?? project?.defaultModel
|
|
2526
2668
|
};
|
|
@@ -2529,95 +2671,6 @@ function resolveTranslateConfig(config, type) {
|
|
|
2529
2671
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2530
2672
|
}
|
|
2531
2673
|
|
|
2532
|
-
// src/translate/sanitize-mdx-jsx.ts
|
|
2533
|
-
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2534
|
-
let adjusted = false;
|
|
2535
|
-
let out = "";
|
|
2536
|
-
let i = 0;
|
|
2537
|
-
while (i < body.length) {
|
|
2538
|
-
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2539
|
-
out += body[i];
|
|
2540
|
-
i += 1;
|
|
2541
|
-
continue;
|
|
2542
|
-
}
|
|
2543
|
-
const tagStart = i;
|
|
2544
|
-
i += 1;
|
|
2545
|
-
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2546
|
-
const tagName = body.slice(tagStart + 1, i);
|
|
2547
|
-
let tagOut = `<${tagName}`;
|
|
2548
|
-
let tagAdjusted = false;
|
|
2549
|
-
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2550
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2551
|
-
tagOut += body[i];
|
|
2552
|
-
i += 1;
|
|
2553
|
-
}
|
|
2554
|
-
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2555
|
-
const attrStart = i;
|
|
2556
|
-
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2557
|
-
body.slice(attrStart, i);
|
|
2558
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2559
|
-
if (body[i] !== "=") {
|
|
2560
|
-
tagOut += body.slice(attrStart, i);
|
|
2561
|
-
continue;
|
|
2562
|
-
}
|
|
2563
|
-
tagOut += body.slice(attrStart, i);
|
|
2564
|
-
tagOut += "=";
|
|
2565
|
-
i += 1;
|
|
2566
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2567
|
-
const quote = body[i];
|
|
2568
|
-
if (quote !== '"' && quote !== "'") {
|
|
2569
|
-
continue;
|
|
2570
|
-
}
|
|
2571
|
-
if (quote === "'") {
|
|
2572
|
-
const valStart2 = i + 1;
|
|
2573
|
-
i += 1;
|
|
2574
|
-
while (i < body.length) {
|
|
2575
|
-
if (body[i] === "\\") {
|
|
2576
|
-
i += 2;
|
|
2577
|
-
continue;
|
|
2578
|
-
}
|
|
2579
|
-
if (body[i] === "'") break;
|
|
2580
|
-
i += 1;
|
|
2581
|
-
}
|
|
2582
|
-
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2583
|
-
i += 1;
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
const valStart = i + 1;
|
|
2587
|
-
i += 1;
|
|
2588
|
-
let closeIdx = -1;
|
|
2589
|
-
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2590
|
-
if (body[scan] !== '"') continue;
|
|
2591
|
-
let j = scan + 1;
|
|
2592
|
-
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2593
|
-
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2594
|
-
closeIdx = scan;
|
|
2595
|
-
break;
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2598
|
-
if (closeIdx === -1) {
|
|
2599
|
-
tagOut += body.slice(valStart - 1, i);
|
|
2600
|
-
break;
|
|
2601
|
-
}
|
|
2602
|
-
const value = body.slice(valStart, closeIdx);
|
|
2603
|
-
const hasInternalQuote = value.includes('"');
|
|
2604
|
-
if (hasInternalQuote) {
|
|
2605
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2606
|
-
tagOut += `'${escaped}'`;
|
|
2607
|
-
tagAdjusted = true;
|
|
2608
|
-
} else {
|
|
2609
|
-
tagOut += `"${value}"`;
|
|
2610
|
-
}
|
|
2611
|
-
i = closeIdx + 1;
|
|
2612
|
-
}
|
|
2613
|
-
if (tagAdjusted) adjusted = true;
|
|
2614
|
-
tagOut += body[i] ?? "";
|
|
2615
|
-
out += tagOut;
|
|
2616
|
-
i += 1;
|
|
2617
|
-
}
|
|
2618
|
-
return { body: out, adjusted };
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
2674
|
// src/translate/validate-translation.ts
|
|
2622
2675
|
function formatZodIssues(error) {
|
|
2623
2676
|
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -2751,7 +2804,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2751
2804
|
if (!validated.ok) {
|
|
2752
2805
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2753
2806
|
}
|
|
2754
|
-
const { body: translatedBody, adjusted: mdxAdjusted } =
|
|
2807
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
|
|
2755
2808
|
result.parsed.body
|
|
2756
2809
|
);
|
|
2757
2810
|
const writeDb = openStore(config, "readwrite");
|