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/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;
|
|
@@ -1771,7 +1903,7 @@ function validateTypeRedirects(project) {
|
|
|
1771
1903
|
}
|
|
1772
1904
|
for (const entry of loaded.entries) {
|
|
1773
1905
|
for (const from of entry.fromSlugs) {
|
|
1774
|
-
if (from === entry.toSlug) {
|
|
1906
|
+
if (entry.kind === "same-type" && from === entry.toSlug) {
|
|
1775
1907
|
issues.push({
|
|
1776
1908
|
level: "error",
|
|
1777
1909
|
contentType: type.id,
|
|
@@ -1865,16 +1997,17 @@ function validateTypeRedirects(project) {
|
|
|
1865
1997
|
if (!targetType?.path) continue;
|
|
1866
1998
|
for (const from of entry.fromSlugs) {
|
|
1867
1999
|
if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
|
|
1868
|
-
const
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2000
|
+
const targetAsSource = globalFrom.get(entry.toSlug);
|
|
2001
|
+
if (!targetAsSource) continue;
|
|
2002
|
+
const sameSlugCrossType = entry.kind === "cross-type" && entry.fromSlugs.includes(entry.toSlug) && targetAsSource.typeId === file.contentTypeId;
|
|
2003
|
+
if (sameSlugCrossType) continue;
|
|
2004
|
+
issues.push({
|
|
2005
|
+
level: "error",
|
|
2006
|
+
contentType: file.contentTypeId,
|
|
2007
|
+
enSlug: from,
|
|
2008
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
2009
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
2010
|
+
});
|
|
1878
2011
|
}
|
|
1879
2012
|
}
|
|
1880
2013
|
}
|
|
@@ -2147,15 +2280,27 @@ function validateProject(config) {
|
|
|
2147
2280
|
message: issue.message
|
|
2148
2281
|
});
|
|
2149
2282
|
}
|
|
2283
|
+
const preparedBody = prepareTranslatedMdxBody(row.body).body;
|
|
2150
2284
|
for (const issue of validateDocumentAssets(config, {
|
|
2151
2285
|
contentType: type.id,
|
|
2152
2286
|
enSlug,
|
|
2153
2287
|
locale,
|
|
2154
2288
|
frontmatter: localeFm,
|
|
2155
|
-
body:
|
|
2289
|
+
body: preparedBody
|
|
2156
2290
|
})) {
|
|
2157
2291
|
issues.push(issue);
|
|
2158
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
|
+
}
|
|
2159
2304
|
}
|
|
2160
2305
|
}
|
|
2161
2306
|
try {
|
|
@@ -2487,24 +2632,21 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2487
2632
|
}
|
|
2488
2633
|
|
|
2489
2634
|
// src/translate/resolve-translate-config.ts
|
|
2490
|
-
|
|
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) {
|
|
2491
2641
|
if (slugStrategy === "localized") {
|
|
2492
|
-
|
|
2642
|
+
return [
|
|
2493
2643
|
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2494
2644
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2495
2645
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2496
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).",
|
|
2497
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.",
|
|
2498
|
-
|
|
2648
|
+
"Preserve 4-digit years and proper nouns in slugs."
|
|
2499
2649
|
];
|
|
2500
|
-
if (preserveTerms?.length) {
|
|
2501
|
-
rules.push(
|
|
2502
|
-
`Preserve these terms verbatim in slugs (lowercase): ${preserveTerms.join(", ")}.`
|
|
2503
|
-
);
|
|
2504
|
-
} else {
|
|
2505
|
-
rules.push("Preserve 4-digit years and proper nouns in slugs.");
|
|
2506
|
-
}
|
|
2507
|
-
return rules;
|
|
2508
2650
|
}
|
|
2509
2651
|
return ["Do NOT output a slug \u2014 slugStrategy is fixed."];
|
|
2510
2652
|
}
|
|
@@ -2517,9 +2659,10 @@ function mergeTranslateConfig(project, type, slugStrategy) {
|
|
|
2517
2659
|
promptOverride: type?.prompt ?? project?.prompt,
|
|
2518
2660
|
context: mergeContext(project, type),
|
|
2519
2661
|
rules: [
|
|
2662
|
+
...BUILTIN_TRANSLATE_RULES,
|
|
2520
2663
|
...project?.rules ?? [],
|
|
2521
2664
|
...type?.rules ?? [],
|
|
2522
|
-
...slugStrategyRules(slugStrategy
|
|
2665
|
+
...slugStrategyRules(slugStrategy)
|
|
2523
2666
|
],
|
|
2524
2667
|
model: type?.model ?? project?.defaultModel
|
|
2525
2668
|
};
|
|
@@ -2528,95 +2671,6 @@ function resolveTranslateConfig(config, type) {
|
|
|
2528
2671
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2529
2672
|
}
|
|
2530
2673
|
|
|
2531
|
-
// src/translate/sanitize-mdx-jsx.ts
|
|
2532
|
-
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2533
|
-
let adjusted = false;
|
|
2534
|
-
let out = "";
|
|
2535
|
-
let i = 0;
|
|
2536
|
-
while (i < body.length) {
|
|
2537
|
-
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2538
|
-
out += body[i];
|
|
2539
|
-
i += 1;
|
|
2540
|
-
continue;
|
|
2541
|
-
}
|
|
2542
|
-
const tagStart = i;
|
|
2543
|
-
i += 1;
|
|
2544
|
-
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2545
|
-
const tagName = body.slice(tagStart + 1, i);
|
|
2546
|
-
let tagOut = `<${tagName}`;
|
|
2547
|
-
let tagAdjusted = false;
|
|
2548
|
-
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2549
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2550
|
-
tagOut += body[i];
|
|
2551
|
-
i += 1;
|
|
2552
|
-
}
|
|
2553
|
-
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2554
|
-
const attrStart = i;
|
|
2555
|
-
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2556
|
-
body.slice(attrStart, i);
|
|
2557
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2558
|
-
if (body[i] !== "=") {
|
|
2559
|
-
tagOut += body.slice(attrStart, i);
|
|
2560
|
-
continue;
|
|
2561
|
-
}
|
|
2562
|
-
tagOut += body.slice(attrStart, i);
|
|
2563
|
-
tagOut += "=";
|
|
2564
|
-
i += 1;
|
|
2565
|
-
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2566
|
-
const quote = body[i];
|
|
2567
|
-
if (quote !== '"' && quote !== "'") {
|
|
2568
|
-
continue;
|
|
2569
|
-
}
|
|
2570
|
-
if (quote === "'") {
|
|
2571
|
-
const valStart2 = i + 1;
|
|
2572
|
-
i += 1;
|
|
2573
|
-
while (i < body.length) {
|
|
2574
|
-
if (body[i] === "\\") {
|
|
2575
|
-
i += 2;
|
|
2576
|
-
continue;
|
|
2577
|
-
}
|
|
2578
|
-
if (body[i] === "'") break;
|
|
2579
|
-
i += 1;
|
|
2580
|
-
}
|
|
2581
|
-
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2582
|
-
i += 1;
|
|
2583
|
-
continue;
|
|
2584
|
-
}
|
|
2585
|
-
const valStart = i + 1;
|
|
2586
|
-
i += 1;
|
|
2587
|
-
let closeIdx = -1;
|
|
2588
|
-
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2589
|
-
if (body[scan] !== '"') continue;
|
|
2590
|
-
let j = scan + 1;
|
|
2591
|
-
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2592
|
-
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2593
|
-
closeIdx = scan;
|
|
2594
|
-
break;
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
if (closeIdx === -1) {
|
|
2598
|
-
tagOut += body.slice(valStart - 1, i);
|
|
2599
|
-
break;
|
|
2600
|
-
}
|
|
2601
|
-
const value = body.slice(valStart, closeIdx);
|
|
2602
|
-
const hasInternalQuote = value.includes('"');
|
|
2603
|
-
if (hasInternalQuote) {
|
|
2604
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2605
|
-
tagOut += `'${escaped}'`;
|
|
2606
|
-
tagAdjusted = true;
|
|
2607
|
-
} else {
|
|
2608
|
-
tagOut += `"${value}"`;
|
|
2609
|
-
}
|
|
2610
|
-
i = closeIdx + 1;
|
|
2611
|
-
}
|
|
2612
|
-
if (tagAdjusted) adjusted = true;
|
|
2613
|
-
tagOut += body[i] ?? "";
|
|
2614
|
-
out += tagOut;
|
|
2615
|
-
i += 1;
|
|
2616
|
-
}
|
|
2617
|
-
return { body: out, adjusted };
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
2674
|
// src/translate/validate-translation.ts
|
|
2621
2675
|
function formatZodIssues(error) {
|
|
2622
2676
|
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -2750,7 +2804,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2750
2804
|
if (!validated.ok) {
|
|
2751
2805
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2752
2806
|
}
|
|
2753
|
-
const { body: translatedBody, adjusted: mdxAdjusted } =
|
|
2807
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
|
|
2754
2808
|
result.parsed.body
|
|
2755
2809
|
);
|
|
2756
2810
|
const writeDb = openStore(config, "readwrite");
|