@smartcat/sanity-plugin 1.0.0 → 1.1.0
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/README.md +294 -0
- package/dist/_chunks-cjs/constants.cjs +13 -0
- package/dist/_chunks-cjs/constants.cjs.map +1 -0
- package/dist/_chunks-cjs/index.cjs +174 -0
- package/dist/_chunks-cjs/index.cjs.map +1 -0
- package/dist/_chunks-cjs/index2.cjs +338 -0
- package/dist/_chunks-cjs/index2.cjs.map +1 -0
- package/dist/_chunks-cjs/workflow.cjs +16 -0
- package/dist/_chunks-cjs/workflow.cjs.map +1 -0
- package/dist/_chunks-es/constants.js +14 -0
- package/dist/_chunks-es/constants.js.map +1 -0
- package/dist/_chunks-es/index.js +175 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/index2.js +340 -0
- package/dist/_chunks-es/index2.js.map +1 -0
- package/dist/_chunks-es/workflow.js +17 -0
- package/dist/_chunks-es/workflow.js.map +1 -0
- package/dist/export.cjs +160 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +162 -0
- package/dist/export.d.ts +162 -0
- package/dist/export.js +162 -0
- package/dist/export.js.map +1 -0
- package/dist/import.cjs +169 -0
- package/dist/import.cjs.map +1 -0
- package/dist/import.d.cts +155 -0
- package/dist/import.d.ts +155 -0
- package/dist/import.js +169 -0
- package/dist/import.js.map +1 -0
- package/dist/index.cjs +2172 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2179 -0
- package/dist/index.js.map +1 -0
- package/dist/locjson.cjs +12 -0
- package/dist/locjson.cjs.map +1 -0
- package/dist/locjson.d.cts +291 -0
- package/dist/locjson.d.ts +291 -0
- package/dist/locjson.js +12 -0
- package/dist/locjson.js.map +1 -0
- package/dist/progress.cjs +16 -0
- package/dist/progress.cjs.map +1 -0
- package/dist/progress.d.cts +277 -0
- package/dist/progress.d.ts +277 -0
- package/dist/progress.js +16 -0
- package/dist/progress.js.map +1 -0
- package/dist/smartcat.cjs +287 -0
- package/dist/smartcat.cjs.map +1 -0
- package/dist/smartcat.d.cts +234 -0
- package/dist/smartcat.d.ts +234 -0
- package/dist/smartcat.js +287 -0
- package/dist/smartcat.js.map +1 -0
- package/dist/templates.cjs +12 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +52 -0
- package/dist/templates.d.ts +52 -0
- package/dist/templates.js +12 -0
- package/dist/templates.js.map +1 -0
- package/package.json +101 -15
- package/src/actions/AddToProjectAction.tsx +274 -0
- package/src/export/export.test.ts +537 -0
- package/src/export/index.ts +393 -0
- package/src/form/TranslationStatusInput.tsx +212 -0
- package/src/import/import.test.ts +346 -0
- package/src/import/index.ts +418 -0
- package/src/index.ts +63 -0
- package/src/lib/constants.ts +23 -0
- package/src/lib/documentTitle.test.ts +43 -0
- package/src/lib/documentTitle.ts +30 -0
- package/src/lib/languageMap.test.ts +56 -0
- package/src/lib/languageMap.ts +71 -0
- package/src/lib/linkedDocuments.test.ts +56 -0
- package/src/lib/linkedDocuments.ts +100 -0
- package/src/lib/locjson/deserialize.ts +60 -0
- package/src/lib/locjson/fields.ts +355 -0
- package/src/lib/locjson/filename.ts +34 -0
- package/src/lib/locjson/index.ts +10 -0
- package/src/lib/locjson/locjson.test.ts +615 -0
- package/src/lib/locjson/paths.test.ts +41 -0
- package/src/lib/locjson/paths.ts +73 -0
- package/src/lib/locjson/portableText.ts +157 -0
- package/src/lib/locjson/serialize.ts +124 -0
- package/src/lib/locjson/types.ts +106 -0
- package/src/lib/log.ts +33 -0
- package/src/lib/projectItems.ts +31 -0
- package/src/lib/resolveConfig.test.ts +62 -0
- package/src/lib/resolveConfig.ts +54 -0
- package/src/lib/workflow.test.ts +37 -0
- package/src/lib/workflow.ts +48 -0
- package/src/lib/zip.fixtures.ts +75 -0
- package/src/lib/zip.test.ts +110 -0
- package/src/lib/zip.ts +164 -0
- package/src/progress/core.ts +409 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/progress.test.ts +290 -0
- package/src/schema/settings.ts +55 -0
- package/src/schema/translatableOptions.ts +25 -0
- package/src/schema/translationProject.ts +268 -0
- package/src/smartcat/client.test.ts +222 -0
- package/src/smartcat/client.ts +351 -0
- package/src/smartcat/index.ts +10 -0
- package/src/smartcat/types.ts +99 -0
- package/src/templates/core.ts +55 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/templates.test.ts +50 -0
- package/src/tool/Dashboard.tsx +46 -0
- package/src/tool/ItemProgress.tsx +176 -0
- package/src/tool/LogPanel.tsx +207 -0
- package/src/tool/ProjectView.tsx +1004 -0
- package/src/tool/ProjectsView.tsx +208 -0
- package/src/tool/StatusBadge.tsx +33 -0
- package/src/tool/StudioInit.tsx +42 -0
- package/src/tool/WorkflowSelect.tsx +41 -0
- package/src/tool/data.ts +86 -0
- package/src/tool/index.ts +19 -0
- package/src/tool/processing.test.ts +698 -0
- package/src/tool/processing.ts +839 -0
- package/src/tool/useTemplates.ts +84 -0
- package/src/tool/useWorkflowSelection.ts +65 -0
- package/src/types.ts +77 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var toHtml = require("@portabletext/to-html"), blockTools = require("@sanity/block-tools");
|
|
3
|
+
function isExcludedByDocumentI18n(field) {
|
|
4
|
+
return field.type?.options?.documentInternationalization?.exclude === !0;
|
|
5
|
+
}
|
|
6
|
+
function isSystemType(typeName) {
|
|
7
|
+
return typeof typeName == "string" && typeName.startsWith("sanity.");
|
|
8
|
+
}
|
|
9
|
+
function isInternationalizedArrayType(type) {
|
|
10
|
+
return type?.jsonType === "array" && typeof type.name == "string" && type.name.startsWith("internationalizedArray");
|
|
11
|
+
}
|
|
12
|
+
function isInternationalizedArrayMemberType(typeName) {
|
|
13
|
+
return typeof typeName == "string" && /^internationalizedArray.+Value$/.test(typeName);
|
|
14
|
+
}
|
|
15
|
+
function innerValueType(arrayType) {
|
|
16
|
+
return arrayType?.of?.[0]?.fields?.find((f) => f.name === "value")?.type;
|
|
17
|
+
}
|
|
18
|
+
function localizationMode(compiledType) {
|
|
19
|
+
const fields = compiledType?.fields;
|
|
20
|
+
return Array.isArray(fields) && fields.some((f) => isInternationalizedArrayType(f.type)) ? "field" : "document";
|
|
21
|
+
}
|
|
22
|
+
function getTranslatableFields(compiledType, options = {}) {
|
|
23
|
+
const rootType = compiledType;
|
|
24
|
+
if (isSystemType(rootType?.name)) return [];
|
|
25
|
+
if (!Array.isArray(rootType?.fields)) return [];
|
|
26
|
+
const result = [];
|
|
27
|
+
return walkFields(rootType, "", localizationMode(rootType), new Set(options.exclude ?? []), result), result;
|
|
28
|
+
}
|
|
29
|
+
function walkFields(compiledType, prefix, mode, exclude, out) {
|
|
30
|
+
for (const field of compiledType?.fields ?? []) {
|
|
31
|
+
if (!field?.name || field.name.startsWith("_") || prefix === "" && exclude.has(field.name)) continue;
|
|
32
|
+
const forceInclude = field.type?.options?.smartcatTranslation?.include === !0;
|
|
33
|
+
if (isExcludedByDocumentI18n(field) && !forceInclude) continue;
|
|
34
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name, title = field.type?.title || titleCase(field.name);
|
|
35
|
+
if (mode === "field")
|
|
36
|
+
if (isInternationalizedArrayType(field.type)) {
|
|
37
|
+
const inner = innerValueType(field.type), shape = classifyValueShape(inner) ?? (forceInclude ? classifyValueShapeByJsonType(inner) : null);
|
|
38
|
+
shape ? out.push({ path, localization: "field", kind: shape.container === "scalar" ? shape.kind : "plain", title, value: shape }) : forceInclude && warnUntranslatableInclude(field.name, inner);
|
|
39
|
+
} else isPlainObject(field.type) && walkFields(field.type, path, mode, exclude, out);
|
|
40
|
+
else {
|
|
41
|
+
const kind = classifyField(field.type) ?? (forceInclude ? classifyByJsonType(field.type) : null);
|
|
42
|
+
kind ? out.push({ path, localization: "document", kind, title, value: { container: "scalar", kind } }) : forceInclude && warnUntranslatableInclude(field.name, field.type);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function enumerateLeaves(field, sourceValue) {
|
|
47
|
+
const base = field.path, shape = field.value ?? { container: "scalar", kind: field.kind };
|
|
48
|
+
if (shape.container === "scalar")
|
|
49
|
+
return [{ fullKey: base, valuePath: "", kind: shape.kind, title: field.title }];
|
|
50
|
+
if (shape.container === "arrayOfScalar")
|
|
51
|
+
return (Array.isArray(sourceValue) ? sourceValue : []).map((_, i) => ({ fullKey: `${base}[${i}]`, valuePath: `[${i}]`, kind: shape.kind, title: field.title }));
|
|
52
|
+
if (shape.container === "object")
|
|
53
|
+
return shape.leaves.map((l) => ({ fullKey: `${base}.${l.subPath}`, valuePath: l.subPath, kind: l.kind, title: l.title ?? field.title }));
|
|
54
|
+
const arr = Array.isArray(sourceValue) ? sourceValue : [], out = [];
|
|
55
|
+
for (const item of arr) {
|
|
56
|
+
const key = item?._key;
|
|
57
|
+
if (typeof key == "string")
|
|
58
|
+
for (const l of shape.leaves) {
|
|
59
|
+
const valuePath = l.subPath ? `[${key}].${l.subPath}` : `[${key}]`;
|
|
60
|
+
out.push({ fullKey: `${base}${valuePath}`, valuePath, kind: l.kind, title: l.title ?? field.title });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function isPlainObject(type) {
|
|
66
|
+
return type?.jsonType !== "object" || !Array.isArray(type.fields) || type.fields.some((f) => f.name === "_ref") ? !1 : !["reference", "image", "file", "slug", "geopoint"].includes(type.name ?? "");
|
|
67
|
+
}
|
|
68
|
+
function classifyValueShape(inner) {
|
|
69
|
+
if (!inner) return null;
|
|
70
|
+
const scalar = classifyField(inner);
|
|
71
|
+
if (scalar) return { container: "scalar", kind: scalar };
|
|
72
|
+
if (inner.jsonType === "array" && Array.isArray(inner.of)) {
|
|
73
|
+
const member = inner.of[0];
|
|
74
|
+
if (member?.jsonType === "string" && (member.name === "string" || member.name === "text"))
|
|
75
|
+
return { container: "arrayOfScalar", kind: "plain" };
|
|
76
|
+
if (isPlainObject(member)) {
|
|
77
|
+
const leaves = collectLeaves(member, "");
|
|
78
|
+
if (leaves.length) return { container: "arrayOfObject", leaves };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (isPlainObject(inner)) {
|
|
83
|
+
const leaves = collectLeaves(inner, "");
|
|
84
|
+
if (leaves.length) return { container: "object", leaves };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function classifyValueShapeByJsonType(inner) {
|
|
89
|
+
const kind = classifyByJsonType(inner);
|
|
90
|
+
return kind ? { container: "scalar", kind } : null;
|
|
91
|
+
}
|
|
92
|
+
function collectLeaves(objType, prefix) {
|
|
93
|
+
const leaves = [];
|
|
94
|
+
for (const f of objType?.fields ?? []) {
|
|
95
|
+
if (!f?.name || f.name.startsWith("_") || isExcludedByDocumentI18n(f) && f.type?.options?.smartcatTranslation?.include !== !0) continue;
|
|
96
|
+
const subPath = prefix ? `${prefix}.${f.name}` : f.name, kind = classifyField(f.type);
|
|
97
|
+
kind ? leaves.push({ subPath, kind, title: f.type?.title || titleCase(f.name) }) : isPlainObject(f.type) && leaves.push(...collectLeaves(f.type, subPath));
|
|
98
|
+
}
|
|
99
|
+
return leaves;
|
|
100
|
+
}
|
|
101
|
+
function isPortableTextArray(type) {
|
|
102
|
+
return type.jsonType === "array" && Array.isArray(type.of) && type.of.some((member) => member?.name === "block" || member?.jsonType === "block");
|
|
103
|
+
}
|
|
104
|
+
function classifyField(type) {
|
|
105
|
+
return type ? type.jsonType === "string" && (type.name === "string" || type.name === "text") ? "plain" : isPortableTextArray(type) ? "portableText" : null : null;
|
|
106
|
+
}
|
|
107
|
+
function classifyByJsonType(type) {
|
|
108
|
+
return type ? type.jsonType === "string" ? "plain" : isPortableTextArray(type) ? "portableText" : null : null;
|
|
109
|
+
}
|
|
110
|
+
function warnUntranslatableInclude(fieldName, type) {
|
|
111
|
+
console.warn(
|
|
112
|
+
`[smartcat-translation] Field "${fieldName}" is marked smartcatTranslation.include but its type (name: ${JSON.stringify(type?.name)}, jsonType: ${JSON.stringify(type?.jsonType)}) has no translatable representation; skipping.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function titleCase(name) {
|
|
116
|
+
return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase());
|
|
117
|
+
}
|
|
118
|
+
function inferTranslatableFields(doc, options = {}) {
|
|
119
|
+
if (isSystemType(doc._type)) return [];
|
|
120
|
+
const exclude = new Set(options.exclude ?? []), isI18nArray = (value) => Array.isArray(value) && value.some((m) => m && typeof m == "object" && isInternationalizedArrayMemberType(m._type)), entries = Object.entries(doc).filter(([key]) => !key.startsWith("_") && !exclude.has(key)), fieldMode = entries.some(([, value]) => isI18nArray(value)), result = [];
|
|
121
|
+
for (const [key, value] of entries)
|
|
122
|
+
if (fieldMode) {
|
|
123
|
+
if (!isI18nArray(value)) continue;
|
|
124
|
+
const sample = value.find((m) => m && typeof m == "object")?.value, kind = sampleKind(sample);
|
|
125
|
+
kind && result.push({ path: key, kind, localization: "field", title: titleCase(key) });
|
|
126
|
+
} else typeof value == "string" ? value.length > 0 && result.push({ path: key, kind: "plain", localization: "document", title: titleCase(key) }) : Array.isArray(value) && value.some(isPortableTextBlock) && result.push({ path: key, kind: "portableText", localization: "document", title: titleCase(key) });
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
function sampleKind(value) {
|
|
130
|
+
return typeof value == "string" ? "plain" : Array.isArray(value) && value.some(isPortableTextBlock) ? "portableText" : null;
|
|
131
|
+
}
|
|
132
|
+
function isPortableTextBlock(value) {
|
|
133
|
+
return !!(value && typeof value == "object" && value._type === "block");
|
|
134
|
+
}
|
|
135
|
+
function orderFieldsBy(fields, orderedPaths) {
|
|
136
|
+
if (!orderedPaths || orderedPaths.length === 0) return fields;
|
|
137
|
+
const rank = new Map(orderedPaths.map((path, i) => [path, i])), at = (path) => rank.has(path) ? rank.get(path) : Number.MAX_SAFE_INTEGER;
|
|
138
|
+
return fields.map((field, i) => ({ field, i })).sort((a, b) => at(a.field.path) - at(b.field.path) || a.i - b.i).map((x) => x.field);
|
|
139
|
+
}
|
|
140
|
+
function parsePath(path) {
|
|
141
|
+
const segs = [], re = /\.?([^.[\]]+)|\[([^\]]+)\]/g;
|
|
142
|
+
let m;
|
|
143
|
+
for (; (m = re.exec(path)) !== null; )
|
|
144
|
+
m[1] !== void 0 ? segs.push({ field: m[1] }) : /^\d+$/.test(m[2]) ? segs.push({ index: Number(m[2]) }) : segs.push({ key: m[2] });
|
|
145
|
+
return segs;
|
|
146
|
+
}
|
|
147
|
+
function child(container, seg) {
|
|
148
|
+
if (!(container == null || typeof container != "object"))
|
|
149
|
+
return "field" in seg ? container[seg.field] : "index" in seg ? Array.isArray(container) ? container[seg.index] : void 0 : Array.isArray(container) ? container.find((m) => m && typeof m == "object" && m._key === seg.key) : void 0;
|
|
150
|
+
}
|
|
151
|
+
function getAtPath(root, path) {
|
|
152
|
+
let cur = root;
|
|
153
|
+
for (const seg of parsePath(path))
|
|
154
|
+
if (cur = child(cur, seg), cur === void 0) return;
|
|
155
|
+
return cur;
|
|
156
|
+
}
|
|
157
|
+
function setAtPath(root, path, value) {
|
|
158
|
+
const segs = parsePath(path);
|
|
159
|
+
if (segs.length === 0) return value;
|
|
160
|
+
const clone = structuredClone(root);
|
|
161
|
+
let cur = clone;
|
|
162
|
+
for (let i = 0; i < segs.length - 1; i++)
|
|
163
|
+
if (cur = child(cur, segs[i]), cur == null || typeof cur != "object") return clone;
|
|
164
|
+
const last = segs[segs.length - 1];
|
|
165
|
+
if ("field" in last) cur[last.field] = value;
|
|
166
|
+
else if ("index" in last && Array.isArray(cur)) cur[last.index] = value;
|
|
167
|
+
else if ("key" in last && Array.isArray(cur)) {
|
|
168
|
+
const idx = cur.findIndex((m) => m && typeof m == "object" && m._key === last.key);
|
|
169
|
+
idx >= 0 && (cur[idx] = value);
|
|
170
|
+
}
|
|
171
|
+
return clone;
|
|
172
|
+
}
|
|
173
|
+
const SERIALIZABLE_CUSTOM_TYPES = /* @__PURE__ */ new Set(["table"]);
|
|
174
|
+
function tableToHtml(value) {
|
|
175
|
+
return `<table><tbody>${(Array.isArray(value?.rows) ? value.rows : []).map((row) => `<tr>${(Array.isArray(row?.cells) ? row.cells : []).map((cell) => `<td>${toHtml.escapeHTML(typeof cell == "string" ? cell : "")}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
176
|
+
}
|
|
177
|
+
const tableRule = {
|
|
178
|
+
deserialize(el, _next, createBlock) {
|
|
179
|
+
if (el.tagName?.toLowerCase() !== "table") return;
|
|
180
|
+
const rows = Array.from(el.querySelectorAll("tr")).map((tr, i) => ({
|
|
181
|
+
_type: "tableRow",
|
|
182
|
+
_key: `row${i}`,
|
|
183
|
+
cells: Array.from(tr.querySelectorAll("td, th")).map((cell) => cell.textContent ?? "")
|
|
184
|
+
}));
|
|
185
|
+
return createBlock({ _type: "table", rows });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
function portableTextToHtml(blocks) {
|
|
189
|
+
return !blocks || blocks.length === 0 ? "" : toHtml.toHTML(blocks, {
|
|
190
|
+
// Fallback only. Serialization skips any field containing an unserializable
|
|
191
|
+
// block up front (see findUnserializableBlockTypes), so to-html should never
|
|
192
|
+
// meet an unknown type here. If one slips through, render it as empty rather
|
|
193
|
+
// than let @portabletext/to-html emit its default placeholder — a hidden
|
|
194
|
+
// `<div style="display:none">Unknown block type "…"</div>` whose English text
|
|
195
|
+
// would otherwise be sent to Smartcat and translated into every locale.
|
|
196
|
+
// (`onMissingComponent: false` only silences the warning; it does NOT stop the
|
|
197
|
+
// placeholder — the `unknownType` override is what suppresses it.)
|
|
198
|
+
onMissingComponent: !1,
|
|
199
|
+
components: {
|
|
200
|
+
unknownType: () => "",
|
|
201
|
+
// Serializable custom block types get real markup so their text is
|
|
202
|
+
// translatable and can be parsed back (see SERIALIZABLE_CUSTOM_TYPES).
|
|
203
|
+
types: { table: ({ value }) => tableToHtml(value) }
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function findUnserializableBlockTypes(blocks) {
|
|
208
|
+
if (!Array.isArray(blocks)) return [];
|
|
209
|
+
const types = /* @__PURE__ */ new Set();
|
|
210
|
+
for (const block of blocks) {
|
|
211
|
+
if (!block || typeof block != "object") continue;
|
|
212
|
+
const type = block._type;
|
|
213
|
+
if (type !== "block") {
|
|
214
|
+
typeof type == "string" && !SERIALIZABLE_CUSTOM_TYPES.has(type) && types.add(type);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const children = block.children;
|
|
218
|
+
if (Array.isArray(children))
|
|
219
|
+
for (const child2 of children) {
|
|
220
|
+
const childType = child2?._type;
|
|
221
|
+
typeof childType == "string" && childType !== "span" && types.add(childType);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return [...types].sort();
|
|
225
|
+
}
|
|
226
|
+
function portableTextToPlainText(blocks) {
|
|
227
|
+
if (!Array.isArray(blocks)) return "";
|
|
228
|
+
let text = "";
|
|
229
|
+
const visit = (node) => {
|
|
230
|
+
if (Array.isArray(node))
|
|
231
|
+
node.forEach(visit);
|
|
232
|
+
else if (node && typeof node == "object") {
|
|
233
|
+
const value = node;
|
|
234
|
+
if (typeof value.text == "string" && (text += value.text), Array.isArray(value.children) && visit(value.children), Array.isArray(value.rows))
|
|
235
|
+
for (const row of value.rows) {
|
|
236
|
+
const cells = row?.cells;
|
|
237
|
+
if (Array.isArray(cells)) for (const cell of cells) typeof cell == "string" && (text += cell);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
return visit(blocks), text;
|
|
242
|
+
}
|
|
243
|
+
function htmlToPortableText(html, blockContentType, parseHtml) {
|
|
244
|
+
return html.trim() ? blockTools.htmlToBlocks(html, blockContentType, {
|
|
245
|
+
parseHtml,
|
|
246
|
+
rules: [tableRule]
|
|
247
|
+
}) : [];
|
|
248
|
+
}
|
|
249
|
+
function serializeToLocjson(doc, fields, options) {
|
|
250
|
+
const units = [];
|
|
251
|
+
for (const field of fields) {
|
|
252
|
+
const source = field.localization === "field" ? memberValue(getAtPath(doc, field.path), options.sourceLanguage, options.fieldLanguageKey) : getAtPath(doc, field.path);
|
|
253
|
+
for (const leaf of enumerateLeaves(field, source)) {
|
|
254
|
+
const value = leaf.valuePath ? getAtPath(source, leaf.valuePath) : source, comments = leaf.title ? [leaf.title] : void 0;
|
|
255
|
+
if (leaf.kind === "plain") {
|
|
256
|
+
if (typeof value != "string" || value.trim().length === 0) continue;
|
|
257
|
+
units.push({
|
|
258
|
+
key: leaf.fullKey,
|
|
259
|
+
properties: { comments, "x-smartcat-split": "full", "x-sanity": { fieldPath: leaf.fullKey } },
|
|
260
|
+
// Split into line segments but keep the trailing newline on each, so
|
|
261
|
+
// concatenating the segments (LocJSON's '' join) reconstructs the original.
|
|
262
|
+
source: value.split(new RegExp("(?<=\\n)")),
|
|
263
|
+
target: []
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
const blocks = value, unserializable = findUnserializableBlockTypes(blocks);
|
|
267
|
+
if (unserializable.length > 0) {
|
|
268
|
+
options.onSkippedField?.({ fieldPath: field.path, title: field.title, types: unserializable });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const html = portableTextToHtml(blocks);
|
|
272
|
+
if (!html || portableTextToPlainText(blocks).trim().length === 0) continue;
|
|
273
|
+
units.push({
|
|
274
|
+
key: leaf.fullKey,
|
|
275
|
+
properties: { comments, "x-smartcat-format": "html", "x-smartcat-split": "full", "x-sanity": { fieldPath: leaf.fullKey } },
|
|
276
|
+
source: [html],
|
|
277
|
+
target: []
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
properties: {
|
|
284
|
+
version: 1,
|
|
285
|
+
"x-sanity": {
|
|
286
|
+
documentId: doc._id,
|
|
287
|
+
documentType: doc._type,
|
|
288
|
+
rev: doc._rev,
|
|
289
|
+
sourceLanguage: options.sourceLanguage
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
units
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function memberValue(field, language, languageKey) {
|
|
296
|
+
return Array.isArray(field) ? field.find(
|
|
297
|
+
(m) => m && typeof m == "object" && m[languageKey] === language
|
|
298
|
+
)?.value : void 0;
|
|
299
|
+
}
|
|
300
|
+
function buildLocjsonFilename(documentType, documentId, title, options = {}) {
|
|
301
|
+
const idPrefix = stripDraft(documentId).slice(0, 8) || "unknown", safeTitle = sanitizeSegment(title || "Untitled"), safeType = sanitizeSegment(documentType) || "document", draftMarker = options.draft ? "-draft" : "";
|
|
302
|
+
return `${safeType}/${safeTitle}-${idPrefix}${draftMarker}.locjson`;
|
|
303
|
+
}
|
|
304
|
+
function stripDraft(id) {
|
|
305
|
+
return id.replace(/^drafts\./, "");
|
|
306
|
+
}
|
|
307
|
+
function sanitizeSegment(value) {
|
|
308
|
+
return value.replace(/[/\\?%*:|"<>]/g, " ").replace(/\s+/g, " ").trim();
|
|
309
|
+
}
|
|
310
|
+
function deserializeFromLocjson(locjson, options) {
|
|
311
|
+
const fileMeta = locjson.properties?.["x-sanity"], fields = {}, sources = {};
|
|
312
|
+
for (const unit of locjson.units ?? []) {
|
|
313
|
+
const fieldPath = unit.properties?.["x-sanity"]?.fieldPath ?? unit.key, value = ((unit.target?.length ? unit.target : unit.source) ?? []).join("");
|
|
314
|
+
sources[fieldPath] = (unit.source ?? []).join(""), unit.properties?.["x-smartcat-format"] === "html" ? fields[fieldPath] = htmlToPortableText(value, options.getBlockContentType(fieldPath), options.parseHtml) : fields[fieldPath] = value;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
documentId: fileMeta?.documentId,
|
|
318
|
+
documentType: fileMeta?.documentType,
|
|
319
|
+
sourceLanguage: fileMeta?.sourceLanguage,
|
|
320
|
+
fields,
|
|
321
|
+
sources
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
exports.buildLocjsonFilename = buildLocjsonFilename;
|
|
325
|
+
exports.deserializeFromLocjson = deserializeFromLocjson;
|
|
326
|
+
exports.enumerateLeaves = enumerateLeaves;
|
|
327
|
+
exports.getAtPath = getAtPath;
|
|
328
|
+
exports.getTranslatableFields = getTranslatableFields;
|
|
329
|
+
exports.htmlToPortableText = htmlToPortableText;
|
|
330
|
+
exports.inferTranslatableFields = inferTranslatableFields;
|
|
331
|
+
exports.innerValueType = innerValueType;
|
|
332
|
+
exports.isInternationalizedArrayType = isInternationalizedArrayType;
|
|
333
|
+
exports.localizationMode = localizationMode;
|
|
334
|
+
exports.orderFieldsBy = orderFieldsBy;
|
|
335
|
+
exports.portableTextToHtml = portableTextToHtml;
|
|
336
|
+
exports.serializeToLocjson = serializeToLocjson;
|
|
337
|
+
exports.setAtPath = setAtPath;
|
|
338
|
+
//# sourceMappingURL=index2.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index2.cjs","sources":["../../src/lib/locjson/fields.ts","../../src/lib/locjson/paths.ts","../../src/lib/locjson/portableText.ts","../../src/lib/locjson/serialize.ts","../../src/lib/locjson/filename.ts","../../src/lib/locjson/deserialize.ts"],"sourcesContent":["import type {LocalizationMode, TranslatableField, TranslatableLeaf, ValueShape} from './types'\n\nexport interface GetTranslatableFieldsOptions {\n /**\n * Field names to exclude (e.g. the i18n language field, passed by the caller\n * as the configured `documentI18nPluginLanguageField`). Defaults to none.\n */\n exclude?: string[]\n}\n\ninterface CompiledType {\n jsonType?: string\n name?: string\n title?: string\n of?: CompiledType[]\n fields?: CompiledField[]\n options?: {\n documentInternationalization?: {exclude?: boolean}\n smartcatTranslation?: {include?: boolean}\n }\n}\n\n/**\n * True when a field is marked non-translatable via document-internationalization's\n * own field option (`options.documentInternationalization.exclude`). Honoring it\n * means consumers exclude fields per type using the i18n plugin's native marker —\n * no separate config here.\n */\nfunction isExcludedByDocumentI18n(field: CompiledField): boolean {\n return field.type?.options?.documentInternationalization?.exclude === true\n}\n\ninterface CompiledField {\n name: string\n type?: CompiledType\n}\n\n/**\n * True for a Sanity **system** type — the reserved `sanity.` namespace, notably\n * the `sanity.imageAsset` / `sanity.fileAsset` documents that image/file fields\n * reference. Their string fields (`originalFilename`, `path`, `url`, …) look\n * translatable but must never be sent: assets are shared across locales, and\n * translating a filename or CDN path corrupts the reference. Excluding them here\n * keeps them out of both the linked-document crawl and export serialization,\n * since both decide translatability via {@link getTranslatableFields}.\n */\nexport function isSystemType(typeName: unknown): boolean {\n return typeof typeName === 'string' && typeName.startsWith('sanity.')\n}\n\n/**\n * True for a `sanity-plugin-internationalized-array` array type (e.g.\n * `internationalizedArrayString`). Detected structurally by naming convention so\n * we never need a dependency on that plugin.\n */\nexport function isInternationalizedArrayType(type?: CompiledType): boolean {\n return (\n type?.jsonType === 'array' &&\n typeof type.name === 'string' &&\n type.name.startsWith('internationalizedArray')\n )\n}\n\n/** True for an array member `_type` like `internationalizedArrayStringValue`. */\nexport function isInternationalizedArrayMemberType(typeName: unknown): boolean {\n return typeof typeName === 'string' && /^internationalizedArray.+Value$/.test(typeName)\n}\n\n/** The inner value type of an internationalized-array field (its member's `value` field). */\nexport function innerValueType(arrayType?: CompiledType): CompiledType | undefined {\n return arrayType?.of?.[0]?.fields?.find((f) => f.name === 'value')?.type\n}\n\n/**\n * Localization mode of a **compiled** schema type: `field` if it declares any\n * internationalized-array field (in which case only those fields are\n * translatable), otherwise `document`. A pure function of the type, so callers\n * should compute it once per type.\n */\nexport function localizationMode(compiledType: unknown): LocalizationMode {\n const fields = (compiledType as CompiledType)?.fields\n if (!Array.isArray(fields)) return 'document'\n return fields.some((f) => isInternationalizedArrayType(f.type)) ? 'field' : 'document'\n}\n\n/**\n * Selects translatable fields from a **compiled** Sanity schema type.\n *\n * Document-mode types (default policy, installation-agnostic):\n * - `string` and `text` fields -> plain units\n * - Portable Text (arrays containing `block` members) -> HTML units\n * - everything else (slug, url, datetime, number, boolean, reference, image,\n * non-block arrays, objects, system `_` fields, excluded names) is excluded.\n *\n * Field-mode types (any internationalized-array field present): only the\n * internationalized-array fields are translatable; each is classified by its\n * inner `value` type (string/text -> plain, Portable Text -> HTML).\n *\n * The compiled schema shape isn't strongly typed publicly, so this reads it\n * defensively. Callers that need overrides can post-filter the result.\n */\nexport function getTranslatableFields(\n compiledType: unknown,\n options: GetTranslatableFieldsOptions = {},\n): TranslatableField[] {\n const rootType = compiledType as CompiledType\n if (isSystemType(rootType?.name)) return []\n if (!Array.isArray(rootType?.fields)) return []\n const result: TranslatableField[] = []\n walkFields(rootType, '', localizationMode(rootType), new Set(options.exclude ?? []), result)\n return result\n}\n\n/**\n * Recursively selects translatable fields. In field-mode it descends through\n * plain object fields (so nested internationalized-array fields, e.g.\n * `benefits.items`, are reached) and classifies each i18n field's member value\n * shape. Document-mode keeps the flat top-level behavior.\n */\nfunction walkFields(\n compiledType: CompiledType | undefined,\n prefix: string,\n mode: LocalizationMode,\n exclude: Set<string>,\n out: TranslatableField[],\n): void {\n for (const field of compiledType?.fields ?? []) {\n // `exclude` (the i18n language field) only ever appears at the top level.\n if (!field?.name || field.name.startsWith('_') || (prefix === '' && exclude.has(field.name))) continue\n const forceInclude = field.type?.options?.smartcatTranslation?.include === true\n if (isExcludedByDocumentI18n(field) && !forceInclude) continue\n const path = prefix ? `${prefix}.${field.name}` : field.name\n const title = field.type?.title || titleCase(field.name)\n\n if (mode === 'field') {\n if (isInternationalizedArrayType(field.type)) {\n const inner = innerValueType(field.type)\n const shape = classifyValueShape(inner) ?? (forceInclude ? classifyValueShapeByJsonType(inner) : null)\n if (shape) out.push({path, localization: 'field', kind: shape.container === 'scalar' ? shape.kind : 'plain', title, value: shape})\n else if (forceInclude) warnUntranslatableInclude(field.name, inner)\n } else if (isPlainObject(field.type)) {\n walkFields(field.type, path, mode, exclude, out)\n }\n } else {\n const kind = classifyField(field.type) ?? (forceInclude ? classifyByJsonType(field.type) : null)\n if (kind) out.push({path, localization: 'document', kind, title, value: {container: 'scalar', kind}})\n else if (forceInclude) warnUntranslatableInclude(field.name, field.type)\n }\n }\n}\n\n/** One translatable leaf of a field, expanded against a concrete source value. */\nexport interface FieldLeaf {\n /** Unique unit key / x-sanity fieldPath. */\n fullKey: string\n /** Path within the member value to the leaf (empty = the value itself). */\n valuePath: string\n kind: TranslatableField['kind']\n title?: string\n}\n\n/**\n * Expand a field into its translatable leaves against a concrete source value\n * (the source-language member value for field-mode). Serialize and import both\n * call this with the SAME source value, so the unit keys they compute match.\n */\nexport function enumerateLeaves(field: TranslatableField, sourceValue: unknown): FieldLeaf[] {\n const base = field.path\n const shape: ValueShape = field.value ?? {container: 'scalar', kind: field.kind}\n if (shape.container === 'scalar') {\n return [{fullKey: base, valuePath: '', kind: shape.kind, title: field.title}]\n }\n if (shape.container === 'arrayOfScalar') {\n const arr = Array.isArray(sourceValue) ? sourceValue : []\n return arr.map((_, i) => ({fullKey: `${base}[${i}]`, valuePath: `[${i}]`, kind: shape.kind, title: field.title}))\n }\n if (shape.container === 'object') {\n return shape.leaves.map((l) => ({fullKey: `${base}.${l.subPath}`, valuePath: l.subPath, kind: l.kind, title: l.title ?? field.title}))\n }\n const arr = Array.isArray(sourceValue) ? sourceValue : []\n const out: FieldLeaf[] = []\n for (const item of arr) {\n const key = (item as Record<string, unknown>)?._key\n if (typeof key !== 'string') continue\n for (const l of shape.leaves) {\n const valuePath = l.subPath ? `[${key}].${l.subPath}` : `[${key}]`\n out.push({fullKey: `${base}${valuePath}`, valuePath, kind: l.kind, title: l.title ?? field.title})\n }\n }\n return out\n}\n\n/** A recursable plain object (not a reference/image/file/slug/geopoint). */\nfunction isPlainObject(type?: CompiledType): boolean {\n if (type?.jsonType !== 'object' || !Array.isArray(type.fields)) return false\n if (type.fields.some((f) => f.name === '_ref')) return false\n return !['reference', 'image', 'file', 'slug', 'geopoint'].includes(type.name ?? '')\n}\n\n/** Classify an internationalized-array member's `value` type into a ValueShape. */\nfunction classifyValueShape(inner?: CompiledType): ValueShape | null {\n if (!inner) return null\n const scalar = classifyField(inner)\n if (scalar) return {container: 'scalar', kind: scalar}\n if (inner.jsonType === 'array' && Array.isArray(inner.of)) {\n const member = inner.of[0]\n if (member?.jsonType === 'string' && (member.name === 'string' || member.name === 'text')) {\n return {container: 'arrayOfScalar', kind: 'plain'}\n }\n if (isPlainObject(member)) {\n const leaves = collectLeaves(member, '')\n if (leaves.length) return {container: 'arrayOfObject', leaves}\n }\n return null\n }\n if (isPlainObject(inner)) {\n const leaves = collectLeaves(inner, '')\n if (leaves.length) return {container: 'object', leaves}\n }\n return null\n}\n\n/** ValueShape by jsonType only (for `smartcatTranslation.include` overrides). */\nfunction classifyValueShapeByJsonType(inner?: CompiledType): ValueShape | null {\n const kind = classifyByJsonType(inner)\n return kind ? {container: 'scalar', kind} : null\n}\n\n/** Translatable string/text/Portable-Text leaves within an object (recursing plain objects). */\nfunction collectLeaves(objType: CompiledType | undefined, prefix: string): TranslatableLeaf[] {\n const leaves: TranslatableLeaf[] = []\n for (const f of objType?.fields ?? []) {\n if (!f?.name || f.name.startsWith('_')) continue\n if (isExcludedByDocumentI18n(f) && f.type?.options?.smartcatTranslation?.include !== true) continue\n const subPath = prefix ? `${prefix}.${f.name}` : f.name\n const kind = classifyField(f.type)\n if (kind) leaves.push({subPath, kind, title: f.type?.title || titleCase(f.name)})\n else if (isPlainObject(f.type)) leaves.push(...collectLeaves(f.type, subPath))\n }\n return leaves\n}\n\n/** True for an array whose members include a Portable Text `block` type. */\nfunction isPortableTextArray(type: CompiledType): boolean {\n return (\n type.jsonType === 'array' &&\n Array.isArray(type.of) &&\n type.of.some((member) => member?.name === 'block' || member?.jsonType === 'block')\n )\n}\n\nfunction classifyField(type?: CompiledType): TranslatableField['kind'] | null {\n if (!type) return null\n\n // Plain text: only the `string` and `text` base types (not url, slug, datetime…).\n if (type.jsonType === 'string' && (type.name === 'string' || type.name === 'text')) {\n return 'plain'\n }\n\n if (isPortableTextArray(type)) return 'portableText'\n\n return null\n}\n\n/**\n * Classify by base `jsonType` only, ignoring the type name. Used for fields\n * explicitly opted in with `smartcatTranslation.include`, where the name isn't in\n * the default allowlist (e.g. a custom string alias) but the underlying shape is\n * still translatable.\n */\nfunction classifyByJsonType(type?: CompiledType): TranslatableField['kind'] | null {\n if (!type) return null\n if (type.jsonType === 'string') return 'plain'\n if (isPortableTextArray(type)) return 'portableText'\n return null\n}\n\nfunction warnUntranslatableInclude(fieldName: string, type?: CompiledType): void {\n // eslint-disable-next-line no-console\n console.warn(\n `[smartcat-translation] Field \"${fieldName}\" is marked smartcatTranslation.include ` +\n `but its type (name: ${JSON.stringify(type?.name)}, jsonType: ${JSON.stringify(type?.jsonType)}) ` +\n `has no translatable representation; skipping.`,\n )\n}\n\nfunction titleCase(name: string): string {\n return name.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase())\n}\n\n/**\n * Selects translatable fields from a document's **data** (no schema needed) —\n * for use in Sanity Functions, which run server-side without the Studio schema.\n *\n * Applies the same homogeneity rule as {@link getTranslatableFields}: if any\n * field holds internationalized-array members, the document is field-mode and\n * only those fields are selected (classified by a sample member's `value`);\n * otherwise top-level `string`/Portable Text fields are selected (document-mode).\n */\nexport function inferTranslatableFields(\n doc: Record<string, unknown>,\n options: GetTranslatableFieldsOptions = {},\n): TranslatableField[] {\n if (isSystemType(doc._type)) return []\n const exclude = new Set(options.exclude ?? [])\n\n const isI18nArray = (value: unknown): value is Array<{value?: unknown}> =>\n Array.isArray(value) &&\n value.some((m) => m && typeof m === 'object' && isInternationalizedArrayMemberType((m as {_type?: unknown})._type))\n\n const entries = Object.entries(doc).filter(([key]) => !key.startsWith('_') && !exclude.has(key))\n const fieldMode = entries.some(([, value]) => isI18nArray(value))\n\n const result: TranslatableField[] = []\n for (const [key, value] of entries) {\n if (fieldMode) {\n if (!isI18nArray(value)) continue\n const sample = value.find((m) => m && typeof m === 'object')?.value\n const kind = sampleKind(sample)\n if (kind) result.push({path: key, kind, localization: 'field', title: titleCase(key)})\n } else if (typeof value === 'string') {\n if (value.length > 0) result.push({path: key, kind: 'plain', localization: 'document', title: titleCase(key)})\n } else if (Array.isArray(value) && value.some(isPortableTextBlock)) {\n result.push({path: key, kind: 'portableText', localization: 'document', title: titleCase(key)})\n }\n }\n return result\n}\n\nfunction sampleKind(value: unknown): TranslatableField['kind'] | null {\n if (typeof value === 'string') return 'plain'\n if (Array.isArray(value) && value.some(isPortableTextBlock)) return 'portableText'\n return null\n}\n\nfunction isPortableTextBlock(value: unknown): boolean {\n return Boolean(value && typeof value === 'object' && (value as {_type?: string})._type === 'block')\n}\n\n/**\n * Reorders fields to match `orderedPaths` (e.g. a schema's field order). Fields\n * not listed keep their original relative order and go last. Stable.\n */\nexport function orderFieldsBy(\n fields: TranslatableField[],\n orderedPaths: string[] | undefined,\n): TranslatableField[] {\n if (!orderedPaths || orderedPaths.length === 0) return fields\n const rank = new Map(orderedPaths.map((path, i) => [path, i]))\n const at = (path: string) => (rank.has(path) ? (rank.get(path) as number) : Number.MAX_SAFE_INTEGER)\n return fields\n .map((field, i) => ({field, i}))\n .sort((a, b) => at(a.field.path) - at(b.field.path) || a.i - b.i)\n .map((x) => x.field)\n}\n","/**\n * Tiny path grammar for addressing translatable leaves nested inside a field's\n * value: `.field` for object properties, `[key]` for array members by `_key`,\n * `[n]` for array members by index. Used to expand array/object member values\n * into per-leaf LocJSON units and to overlay translations back onto a cloned\n * source value.\n *\n * e.g. `items[b1].value`, `tabs[2]`, `` (empty = the value itself).\n */\nexport type PathSeg = {field: string} | {key: string} | {index: number}\n\nexport function parsePath(path: string): PathSeg[] {\n const segs: PathSeg[] = []\n const re = /\\.?([^.[\\]]+)|\\[([^\\]]+)\\]/g\n let m: RegExpExecArray | null\n while ((m = re.exec(path)) !== null) {\n if (m[1] !== undefined) segs.push({field: m[1]})\n else if (/^\\d+$/.test(m[2])) segs.push({index: Number(m[2])})\n else segs.push({key: m[2]})\n }\n return segs\n}\n\n/** Append a segment to a base path string, producing a valid path. */\nexport function joinPath(base: string, seg: PathSeg): string {\n if ('field' in seg) return base ? `${base}.${seg.field}` : seg.field\n if ('index' in seg) return `${base}[${seg.index}]`\n return `${base}[${seg.key}]`\n}\n\nfunction child(container: unknown, seg: PathSeg): unknown {\n if (container == null || typeof container !== 'object') return undefined\n if ('field' in seg) return (container as Record<string, unknown>)[seg.field]\n if ('index' in seg) return Array.isArray(container) ? container[seg.index] : undefined\n return Array.isArray(container)\n ? container.find((m) => m && typeof m === 'object' && (m as Record<string, unknown>)._key === seg.key)\n : undefined\n}\n\n/** Read the value at a path from a root value (undefined if any segment misses). */\nexport function getAtPath(root: unknown, path: string): unknown {\n let cur = root\n for (const seg of parsePath(path)) {\n cur = child(cur, seg)\n if (cur === undefined) return undefined\n }\n return cur\n}\n\n/**\n * Return a deep clone of `root` with the leaf at `path` replaced by `value`.\n * An empty path replaces the whole value. Missing intermediate segments are a\n * no-op (the clone is returned unchanged), so a template that lacks a leaf can't\n * be corrupted.\n */\nexport function setAtPath<T>(root: T, path: string, value: unknown): T {\n const segs = parsePath(path)\n if (segs.length === 0) return value as T\n const clone = structuredClone(root)\n let cur: unknown = clone\n for (let i = 0; i < segs.length - 1; i++) {\n cur = child(cur, segs[i])\n if (cur == null || typeof cur !== 'object') return clone\n }\n const last = segs[segs.length - 1]\n if ('field' in last) (cur as Record<string, unknown>)[last.field] = value\n else if ('index' in last && Array.isArray(cur)) cur[last.index] = value\n else if ('key' in last && Array.isArray(cur)) {\n const idx = cur.findIndex((m) => m && typeof m === 'object' && (m as Record<string, unknown>)._key === last.key)\n if (idx >= 0) cur[idx] = value\n }\n return clone\n}\n","import {escapeHTML, toHTML} from '@portabletext/to-html'\nimport {htmlToBlocks, type DeserializerRule} from '@sanity/block-tools'\n\n/** A Portable Text block (loosely typed — shape varies by schema). */\nexport type PortableTextBlock = Record<string, unknown>\n\n/**\n * Custom (non-`block`) top-level Portable Text types that we *can* round-trip\n * through HTML, and so must NOT treat as unserializable. Each needs both a\n * `toHTML` component (below) and an `htmlToBlocks` rule (see {@link tableRule})\n * so the block survives export → translation → import intact.\n *\n * Currently just `@sanity/table`: `{_type:'table', rows:[{_type:'tableRow',\n * cells: string[]}]}` — plain-string cells, no header row, no col/row spans.\n */\nconst SERIALIZABLE_CUSTOM_TYPES = new Set(['table'])\n\n/** Renders a `@sanity/table` block to an HTML `<table>` for translation. */\nfunction tableToHtml(value: unknown): string {\n const rows = Array.isArray((value as {rows?: unknown})?.rows) ? (value as {rows: unknown[]}).rows : []\n const trs = rows\n .map((row) => {\n const cells = Array.isArray((row as {cells?: unknown})?.cells) ? (row as {cells: unknown[]}).cells : []\n const tds = cells.map((cell) => `<td>${escapeHTML(typeof cell === 'string' ? cell : '')}</td>`).join('')\n return `<tr>${tds}</tr>`\n })\n .join('')\n return `<table><tbody>${trs}</tbody></table>`\n}\n\n/**\n * An `htmlToBlocks` rule that turns a translated `<table>` back into a\n * `@sanity/table` block. Rows get deterministic index-based `_key`s (the whole\n * field value is replaced on import, so keys need only be unique within the\n * table); cells are plain strings, matching the schema.\n */\nconst tableRule: DeserializerRule = {\n deserialize(el, _next, createBlock) {\n if ((el as Element).tagName?.toLowerCase() !== 'table') return undefined\n const rows = Array.from((el as Element).querySelectorAll('tr')).map((tr, i) => ({\n _type: 'tableRow',\n _key: `row${i}`,\n cells: Array.from(tr.querySelectorAll('td, th')).map((cell) => cell.textContent ?? ''),\n }))\n return createBlock({_type: 'table', rows}) as never\n },\n}\n\n/** Parses an HTML string into a DOM Document. Provided by the environment. */\nexport type ParseHtml = (html: string) => Document\n\n/**\n * Renders Portable Text to an HTML string for translation.\n * Whole-field: the entire block array becomes one HTML document fragment.\n */\nexport function portableTextToHtml(blocks: PortableTextBlock[] | undefined | null): string {\n if (!blocks || blocks.length === 0) return ''\n return toHTML(blocks as never, {\n // Fallback only. Serialization skips any field containing an unserializable\n // block up front (see findUnserializableBlockTypes), so to-html should never\n // meet an unknown type here. If one slips through, render it as empty rather\n // than let @portabletext/to-html emit its default placeholder — a hidden\n // `<div style=\"display:none\">Unknown block type \"…\"</div>` whose English text\n // would otherwise be sent to Smartcat and translated into every locale.\n // (`onMissingComponent: false` only silences the warning; it does NOT stop the\n // placeholder — the `unknownType` override is what suppresses it.)\n onMissingComponent: false,\n components: {\n unknownType: () => '',\n // Serializable custom block types get real markup so their text is\n // translatable and can be parsed back (see SERIALIZABLE_CUSTOM_TYPES).\n types: {table: ({value}) => tableToHtml(value)},\n },\n })\n}\n\n/**\n * Distinct `_type`s in a Portable Text value that `@portabletext/to-html` cannot\n * serialize: any top-level member that isn't a standard `block` (and isn't in\n * {@link SERIALIZABLE_CUSTOM_TYPES}, e.g. `table`, which we render explicitly),\n * or any inline child that isn't a `span` (e.g. an inline object). Such nodes\n * render to nothing, so a field containing them can't survive the HTML\n * round-trip — the caller skips the whole field and warns rather than silently\n * drop or corrupt it. Marks/annotations are intentionally not included: their\n * text survives (only the annotation would be lost).\n */\nexport function findUnserializableBlockTypes(\n blocks: PortableTextBlock[] | undefined | null,\n): string[] {\n if (!Array.isArray(blocks)) return []\n const types = new Set<string>()\n for (const block of blocks) {\n if (!block || typeof block !== 'object') continue\n const type = (block as {_type?: unknown})._type\n if (type !== 'block') {\n if (typeof type === 'string' && !SERIALIZABLE_CUSTOM_TYPES.has(type)) types.add(type)\n continue\n }\n const children = (block as {children?: unknown}).children\n if (!Array.isArray(children)) continue\n for (const child of children) {\n const childType = (child as {_type?: unknown} | null)?._type\n if (typeof childType === 'string' && childType !== 'span') types.add(childType)\n }\n }\n return [...types].sort()\n}\n\n/**\n * Extracts the visible text of Portable Text — the concatenation of every span's\n * `text` across all blocks (recursing into `children`). Used to decide whether a\n * field carries anything translatable: structural-but-textless blocks render to\n * non-empty HTML (`<p></p>`, `<br/>`, `<ul><li></li></ul>`), so emptiness must be\n * judged from the source blocks, not from stripping tags/entities out of the HTML\n * (which would mishandle ` ` and void elements).\n */\nexport function portableTextToPlainText(blocks: PortableTextBlock[] | undefined | null): string {\n if (!Array.isArray(blocks)) return ''\n let text = ''\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n node.forEach(visit)\n } else if (node && typeof node === 'object') {\n const value = node as {text?: unknown; children?: unknown; rows?: unknown}\n if (typeof value.text === 'string') text += value.text\n if (Array.isArray(value.children)) visit(value.children)\n // `table` blocks carry text in `rows[].cells[]` (plain strings), not spans;\n // count it so a table-only field isn't judged empty and skipped on export.\n if (Array.isArray(value.rows)) {\n for (const row of value.rows) {\n const cells = (row as {cells?: unknown})?.cells\n if (Array.isArray(cells)) for (const cell of cells) if (typeof cell === 'string') text += cell\n }\n }\n }\n }\n visit(blocks)\n return text\n}\n\n/**\n * Parses translated HTML back into Portable Text using the field's compiled\n * block content type. `parseHtml` supplies a DOM parser:\n * - Studio: (html) => new DOMParser().parseFromString(html, 'text/html')\n * - Functions: (html) => new JSDOM(html).window.document\n */\nexport function htmlToPortableText(\n html: string,\n blockContentType: unknown,\n parseHtml: ParseHtml,\n): PortableTextBlock[] {\n if (!html.trim()) return []\n return htmlToBlocks(html, blockContentType as never, {\n parseHtml,\n rules: [tableRule],\n }) as unknown as PortableTextBlock[]\n}\n","import {\n findUnserializableBlockTypes,\n portableTextToHtml,\n portableTextToPlainText,\n type PortableTextBlock,\n} from './portableText'\nimport {enumerateLeaves} from './fields'\nimport {getAtPath} from './paths'\nimport type {LocJSON, LocUnit, TranslatableField} from './types'\n\nexport interface SerializeOptions {\n sourceLanguage: string\n /**\n * How internationalized-array members identify their locale: a field name\n * (e.g. `'language'`) or `'_key'` (v4.x and lower). Resolved by the caller (see\n * `resolveConfig`); no default is assumed here.\n */\n fieldLanguageKey: string\n /**\n * Called when a Portable Text field is skipped because it contains block types\n * that can't be serialized (see {@link findUnserializableBlockTypes}). The\n * whole field is left out so its content isn't dropped or corrupted on the\n * round-trip; the caller surfaces this to the user's log.\n */\n onSkippedField?: (info: {fieldPath: string; title?: string; types: string[]}) => void\n}\n\n/** A minimal view of a Sanity document needed for serialization. */\nexport interface SerializableDocument {\n _id: string\n _type: string\n _rev?: string\n [key: string]: unknown\n}\n\n/**\n * Serializes a Sanity document into a LocJSON file: one unit per translatable\n * field. Plain fields become plain units (segments split on newlines); Portable\n * Text fields become whole-field HTML units (Smartcat segments them internally).\n *\n * Fields with no value are skipped, so the file only contains real source text.\n */\nexport function serializeToLocjson(\n doc: SerializableDocument,\n fields: TranslatableField[],\n options: SerializeOptions,\n): LocJSON {\n const units: LocUnit[] = []\n\n for (const field of fields) {\n // For field-level (internationalized-array) fields the source content is the\n // source-language member's `value` (the field may be nested through objects).\n const source =\n field.localization === 'field'\n ? memberValue(getAtPath(doc, field.path), options.sourceLanguage, options.fieldLanguageKey)\n : getAtPath(doc, field.path)\n\n // A scalar field yields one leaf; array/object member values yield one leaf\n // per translatable string inside (addressed by valuePath).\n for (const leaf of enumerateLeaves(field, source)) {\n const value = leaf.valuePath ? getAtPath(source, leaf.valuePath) : source\n const comments = leaf.title ? [leaf.title] : undefined\n\n if (leaf.kind === 'plain') {\n // Skip when there's no translatable text. Trim only for the check — the\n // stored source keeps its original whitespace so the round-trip is exact.\n if (typeof value !== 'string' || value.trim().length === 0) continue\n units.push({\n key: leaf.fullKey,\n properties: {comments, 'x-smartcat-split': 'full', 'x-sanity': {fieldPath: leaf.fullKey}},\n // Split into line segments but keep the trailing newline on each, so\n // concatenating the segments (LocJSON's '' join) reconstructs the original.\n source: value.split(/(?<=\\n)/),\n target: [],\n })\n } else {\n const blocks = value as PortableTextBlock[] | undefined\n // A field with a block to-html can't serialize (e.g. a custom `table`)\n // is skipped whole: partial HTML would drop that block on the round-trip.\n const unserializable = findUnserializableBlockTypes(blocks)\n if (unserializable.length > 0) {\n options.onSkippedField?.({fieldPath: field.path, title: field.title, types: unserializable})\n continue\n }\n // `html` is non-empty for structural-but-textless blocks (e.g. `<p></p>`,\n // `<br/>`), so also require actual visible text before emitting a unit.\n const html = portableTextToHtml(blocks)\n if (!html || portableTextToPlainText(blocks).trim().length === 0) continue\n units.push({\n key: leaf.fullKey,\n properties: {comments, 'x-smartcat-format': 'html', 'x-smartcat-split': 'full', 'x-sanity': {fieldPath: leaf.fullKey}},\n source: [html],\n target: [],\n })\n }\n }\n }\n\n return {\n properties: {\n version: 1,\n 'x-sanity': {\n documentId: doc._id,\n documentType: doc._type,\n rev: doc._rev,\n sourceLanguage: options.sourceLanguage,\n },\n },\n units,\n }\n}\n\n/**\n * Reads the `value` of an internationalized-array field's member for a given\n * language. Members are identified strictly by `languageKey` — a field name\n * (`'language'`, v5+) or `'_key'` (v4.x and lower).\n */\nfunction memberValue(field: unknown, language: string, languageKey: string): unknown {\n if (!Array.isArray(field)) return undefined\n const member = field.find(\n (m) => m && typeof m === 'object' && (m as Record<string, unknown>)[languageKey] === language,\n )\n return (member as {value?: unknown} | undefined)?.value\n}\n","/**\n * Builds the Smartcat document filename for a Sanity document:\n * \"<type>/<sanitized title>-<id prefix>.locjson\"\n * e.g. \"page/About Us-3f14f092.locjson\".\n *\n * The type acts as a folder; the id prefix guarantees uniqueness. Identity on\n * import comes from the file's `x-sanity` metadata, not this name, so renames\n * are safe. When the exported content came from the document's draft, a `-draft`\n * marker is appended so it's visible in Smartcat (`…-3f14f092-draft.locjson`).\n */\nexport function buildLocjsonFilename(\n documentType: string,\n documentId: string,\n title?: string,\n options: {draft?: boolean} = {},\n): string {\n const idPrefix = stripDraft(documentId).slice(0, 8) || 'unknown'\n const safeTitle = sanitizeSegment(title || 'Untitled')\n const safeType = sanitizeSegment(documentType) || 'document'\n const draftMarker = options.draft ? '-draft' : ''\n return `${safeType}/${safeTitle}-${idPrefix}${draftMarker}.locjson`\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/** Removes characters unsafe for file paths while keeping it human-readable. */\nfunction sanitizeSegment(value: string): string {\n return value\n .replace(/[/\\\\?%*:|\"<>]/g, ' ') // path-unsafe characters\n .replace(/\\s+/g, ' ')\n .trim()\n}\n","import {htmlToPortableText, type ParseHtml} from './portableText'\nimport type {LocJSON} from './types'\n\nexport interface DeserializeOptions {\n /** DOM parser for HTML units (DOMParser in Studio, jsdom in Functions). */\n parseHtml: ParseHtml\n /** Returns the compiled block content type for a Portable Text field path. */\n getBlockContentType: (fieldPath: string) => unknown\n}\n\nexport interface DeserializedDocument {\n documentId?: string\n documentType?: string\n sourceLanguage?: string\n /** Translated field values, keyed by field path. */\n fields: Record<string, unknown>\n /**\n * Source (pre-translation) text per field path, as the raw joined string. Used\n * to decide whether a field is JSON (so its translation can be validated before\n * import). Always the string form, even for HTML units.\n */\n sources: Record<string, string>\n}\n\n/**\n * Parses a (translated) LocJSON file back into Sanity field values.\n *\n * Uses each unit's `target` (falling back to `source` when a unit hasn't been\n * translated yet, so the result is always coherent). HTML units are converted\n * back to Portable Text via the field's compiled block content type.\n */\nexport function deserializeFromLocjson(locjson: LocJSON, options: DeserializeOptions): DeserializedDocument {\n const fileMeta = locjson.properties?.['x-sanity']\n const fields: Record<string, unknown> = {}\n const sources: Record<string, string> = {}\n\n for (const unit of locjson.units ?? []) {\n const fieldPath = unit.properties?.['x-sanity']?.fieldPath ?? unit.key\n const text = (unit.target?.length ? unit.target : unit.source) ?? []\n\n // LocJSON splits a string into arbitrary chunks with no implied delimiter,\n // so the original is always reconstructed by concatenating the segments.\n const value = text.join('')\n sources[fieldPath] = (unit.source ?? []).join('')\n\n if (unit.properties?.['x-smartcat-format'] === 'html') {\n fields[fieldPath] = htmlToPortableText(value, options.getBlockContentType(fieldPath), options.parseHtml)\n } else {\n fields[fieldPath] = value\n }\n }\n\n return {\n documentId: fileMeta?.documentId,\n documentType: fileMeta?.documentType,\n sourceLanguage: fileMeta?.sourceLanguage,\n fields,\n sources,\n }\n}\n"],"names":["escapeHTML","toHTML","child","htmlToBlocks"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const TEMPLATE_PREFIX = "template:", STANDARD_AI_HUMAN = "standard:ai-human", STANDARD_AI = "standard:ai", STANDARD_WORKFLOW_OPTIONS = [
|
|
3
|
+
{ value: STANDARD_AI_HUMAN, label: "AI Translation + Human Review" },
|
|
4
|
+
{ value: STANDARD_AI, label: "AI Translation" }
|
|
5
|
+
];
|
|
6
|
+
function isWorkflowValid(workflow, templateIds) {
|
|
7
|
+
return workflow === STANDARD_AI_HUMAN || workflow === STANDARD_AI ? !0 : workflow.startsWith(TEMPLATE_PREFIX) ? templateIds.includes(workflow.slice(TEMPLATE_PREFIX.length)) : !1;
|
|
8
|
+
}
|
|
9
|
+
function resolveWorkflowCreateParams(workflow) {
|
|
10
|
+
return workflow && workflow.startsWith(TEMPLATE_PREFIX) ? { templateId: workflow.slice(TEMPLATE_PREFIX.length) } : workflow === STANDARD_AI ? { workflowStages: ["translation"] } : { workflowStages: ["translation", "postediting"] };
|
|
11
|
+
}
|
|
12
|
+
exports.STANDARD_WORKFLOW_OPTIONS = STANDARD_WORKFLOW_OPTIONS;
|
|
13
|
+
exports.TEMPLATE_PREFIX = TEMPLATE_PREFIX;
|
|
14
|
+
exports.isWorkflowValid = isWorkflowValid;
|
|
15
|
+
exports.resolveWorkflowCreateParams = resolveWorkflowCreateParams;
|
|
16
|
+
//# sourceMappingURL=workflow.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.cjs","sources":["../../src/lib/workflow.ts"],"sourcesContent":["/**\n * Workflow selection for new projects. A project stores a single `workflow`\n * string: either `template:<smartcatTemplateId>` or one of the standard presets.\n * The browser sets it at creation; the export Function decodes it into the\n * Smartcat `createProject` params (a template id, or explicit workflow stages).\n *\n * Dependency-free so it bundles into the thin export Function.\n */\n\nexport const TEMPLATE_PREFIX = 'template:'\nexport const STANDARD_AI_HUMAN = 'standard:ai-human'\nexport const STANDARD_AI = 'standard:ai'\n\n/** Backwards-compatible default: matches the previous hard-coded behavior. */\nexport const DEFAULT_WORKFLOW = STANDARD_AI_HUMAN\n\n/** Labels for the two standard presets (rendered under the \"Standard workflows\" group). */\nexport const STANDARD_WORKFLOW_OPTIONS: {value: string; label: string}[] = [\n {value: STANDARD_AI_HUMAN, label: 'AI Translation + Human Review'},\n {value: STANDARD_AI, label: 'AI Translation'},\n]\n\nexport interface WorkflowCreateParams {\n templateId?: string\n workflowStages?: string[]\n}\n\n/**\n * Whether a `workflow` value is currently selectable: the standard presets always\n * are; a `template:<id>` is valid only while that id still exists in the cached\n * template list. Empty / unknown values are invalid (no selection).\n */\nexport function isWorkflowValid(workflow: string, templateIds: string[]): boolean {\n if (workflow === STANDARD_AI_HUMAN || workflow === STANDARD_AI) return true\n if (workflow.startsWith(TEMPLATE_PREFIX)) {\n return templateIds.includes(workflow.slice(TEMPLATE_PREFIX.length))\n }\n return false\n}\n\n/** Decodes a stored `workflow` value into Smartcat create-project params. */\nexport function resolveWorkflowCreateParams(workflow?: string): WorkflowCreateParams {\n if (workflow && workflow.startsWith(TEMPLATE_PREFIX)) {\n return {templateId: workflow.slice(TEMPLATE_PREFIX.length)}\n }\n if (workflow === STANDARD_AI) return {workflowStages: ['translation']}\n return {workflowStages: ['translation', 'postediting']}\n}\n"],"names":[],"mappings":";AASO,MAAM,kBAAkB,aAClB,oBAAoB,qBACpB,cAAc,eAMd,4BAA8D;AAAA,EACzE,EAAC,OAAO,mBAAmB,OAAO,gCAAA;AAAA,EAClC,EAAC,OAAO,aAAa,OAAO,iBAAA;AAC9B;AAYO,SAAS,gBAAgB,UAAkB,aAAgC;AAChF,SAAI,aAAa,qBAAqB,aAAa,cAAoB,KACnE,SAAS,WAAW,eAAe,IAC9B,YAAY,SAAS,SAAS,MAAM,gBAAgB,MAAM,CAAC,IAE7D;AACT;AAGO,SAAS,4BAA4B,UAAyC;AACnF,SAAI,YAAY,SAAS,WAAW,eAAe,IAC1C,EAAC,YAAY,SAAS,MAAM,gBAAgB,MAAM,MAEvD,aAAa,cAAoB,EAAC,gBAAgB,CAAC,aAAa,MAC7D,EAAC,gBAAgB,CAAC,eAAe,aAAa,EAAA;AACvD;;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const TRANSLATION_PROJECT_TYPE = "smartcat.translationProject", SETTINGS_TYPE = "smartcat.settings", SETTINGS_DOC_ID = "smartcat.settings", API_VERSION = "2025-02-01";
|
|
2
|
+
function requireSourceLanguage(sourceLanguage) {
|
|
3
|
+
if (!sourceLanguage)
|
|
4
|
+
throw new Error("Smartcat plugin: `sourceLanguage` is not configured");
|
|
5
|
+
return sourceLanguage;
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
API_VERSION,
|
|
9
|
+
SETTINGS_DOC_ID,
|
|
10
|
+
SETTINGS_TYPE,
|
|
11
|
+
TRANSLATION_PROJECT_TYPE,
|
|
12
|
+
requireSourceLanguage
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sources":["../../src/lib/constants.ts"],"sourcesContent":["/**\n * Document type for a translation project (a named set of items to translate).\n *\n * @public\n */\nexport const TRANSLATION_PROJECT_TYPE = 'smartcat.translationProject'\n\n/** Singleton document holding workspace-wide Smartcat settings (template cache). */\nexport const SETTINGS_TYPE = 'smartcat.settings'\n\n/** Fixed `_id` of the settings singleton. */\nexport const SETTINGS_DOC_ID = 'smartcat.settings'\n\n/** Sanity API version used by the plugin's client calls. */\nexport const API_VERSION = '2025-02-01'\n\n/** Resolves the configured source language, failing if it's not set. */\nexport function requireSourceLanguage(sourceLanguage: string | undefined): string {\n if (!sourceLanguage) {\n throw new Error('Smartcat plugin: `sourceLanguage` is not configured')\n }\n return sourceLanguage\n}\n"],"names":[],"mappings":"AAKO,MAAM,2BAA2B,+BAG3B,gBAAgB,qBAGhB,kBAAkB,qBAGlB,cAAc;AAGpB,SAAS,sBAAsB,gBAA4C;AAChF,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,qDAAqD;AAEvE,SAAO;AACT;"}
|