gt-sanity 0.0.5 → 1.0.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/LICENSE.md +1 -8
- package/README.md +5 -5
- package/dist/index.d.mts +122 -95
- package/dist/index.d.ts +122 -95
- package/dist/index.js +9089 -1119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9099 -1100
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- package/src/adapter/core.ts +111 -9
- package/src/adapter/createTask.ts +1 -1
- package/src/adapter/getLocales.ts +2 -2
- package/src/adapter/types.ts +9 -0
- package/src/components/TranslationsProvider.tsx +942 -0
- package/src/components/page/BatchProgress.tsx +27 -0
- package/src/components/page/ImportAllDialog.tsx +51 -0
- package/src/components/page/ImportMissingDialog.tsx +55 -0
- package/src/components/page/TranslateAllDialog.tsx +55 -0
- package/src/components/page/TranslationsTable.tsx +81 -0
- package/src/components/page/TranslationsTool.tsx +338 -0
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +2 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -0
- package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +2 -0
- package/src/components/shared/SingleDocumentView.tsx +108 -0
- package/src/components/tab/TranslationView.tsx +379 -0
- package/src/components/tab/TranslationsTab.tsx +25 -0
- package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +21 -11
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +57 -23
- package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +31 -8
- package/src/configuration/baseDocumentLevelConfig/index.ts +18 -101
- package/src/configuration/baseFieldLevelConfig.ts +19 -51
- package/src/configuration/utils/checkSerializationVersion.ts +2 -0
- package/src/configuration/utils/findDocumentAtRevision.ts +2 -0
- package/src/configuration/utils/findLatestDraft.ts +2 -0
- package/src/hooks/useClient.ts +3 -1
- package/src/hooks/useSecrets.ts +2 -0
- package/src/index.ts +91 -67
- package/src/sanity-api/findDocuments.ts +44 -0
- package/src/sanity-api/publishDocuments.ts +49 -0
- package/src/sanity-api/resolveRefs.ts +146 -0
- package/src/serialization/BaseDocumentMerger.ts +138 -0
- package/src/serialization/BaseSerializationConfig.ts +220 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
- package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
- package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
- package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
- package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
- package/src/serialization/__tests__/README +2 -0
- package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
- package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
- package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
- package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
- package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
- package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
- package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
- package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
- package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
- package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
- package/src/serialization/__tests__/global.setup.ts +40 -0
- package/src/serialization/__tests__/helpers.ts +132 -0
- package/src/serialization/data.ts +82 -0
- package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
- package/src/serialization/deserialize/helpers.ts +42 -0
- package/src/serialization/helpers.ts +18 -0
- package/src/serialization/index.ts +11 -0
- package/src/serialization/serialize/fieldFilters.ts +124 -0
- package/src/serialization/serialize/index.ts +284 -0
- package/src/serialization/types.ts +41 -0
- package/src/translation/checkTranslationStatus.ts +42 -0
- package/src/translation/createJobs.ts +16 -0
- package/src/translation/downloadTranslations.ts +68 -0
- package/src/translation/importDocument.ts +23 -0
- package/src/translation/initProject.ts +61 -0
- package/src/translation/uploadFiles.ts +32 -0
- package/src/types.ts +7 -20
- package/src/utils/applyDocuments.ts +72 -0
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +52 -0
- package/src/utils/shared.ts +1 -0
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -249
- package/src/components/TaskView.tsx +0 -255
- package/src/components/TranslationContext.tsx +0 -19
- package/src/components/TranslationView.tsx +0 -82
- package/src/components/TranslationsTab.tsx +0 -177
- package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +0 -5
- package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +0 -69
- package/src/configuration/index.ts +0 -18
- package/src/configuration/utils/index.ts +0 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
|
|
2
|
+
import { htmlToBlocks } from '@portabletext/block-tools';
|
|
3
|
+
import {
|
|
4
|
+
customDeserializers,
|
|
5
|
+
customBlockDeserializers,
|
|
6
|
+
} from '../BaseSerializationConfig';
|
|
7
|
+
import { Deserializer } from '../types';
|
|
8
|
+
import { blockContentType, preprocess } from './helpers';
|
|
9
|
+
import { mergeBlocks } from '../helpers';
|
|
10
|
+
|
|
11
|
+
export const deserializeArray = (
|
|
12
|
+
arrayHTML: Element,
|
|
13
|
+
deserializers: Record<string, any> = customDeserializers,
|
|
14
|
+
blockDeserializers = customBlockDeserializers
|
|
15
|
+
) => {
|
|
16
|
+
const output: any[] = [];
|
|
17
|
+
const children = Array.from(arrayHTML.children);
|
|
18
|
+
children.forEach((child) => {
|
|
19
|
+
let deserializedObject: any;
|
|
20
|
+
try {
|
|
21
|
+
if (child.tagName?.toLowerCase() === 'span') {
|
|
22
|
+
deserializedObject = preprocess(child.innerHTML);
|
|
23
|
+
}
|
|
24
|
+
//has specific class name or data type, so it's an obj
|
|
25
|
+
else if (
|
|
26
|
+
child.className ||
|
|
27
|
+
child.getAttribute('data-type') === 'object'
|
|
28
|
+
) {
|
|
29
|
+
deserializedObject = deserializeObject(
|
|
30
|
+
child,
|
|
31
|
+
deserializers,
|
|
32
|
+
blockDeserializers
|
|
33
|
+
);
|
|
34
|
+
deserializedObject._key = child.id;
|
|
35
|
+
} else {
|
|
36
|
+
deserializedObject = htmlToBlocks(child.outerHTML, blockContentType, {
|
|
37
|
+
rules: blockDeserializers,
|
|
38
|
+
});
|
|
39
|
+
deserializedObject = mergeBlocks(deserializedObject);
|
|
40
|
+
deserializedObject._key = child.id;
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
//eslint-disable-next-line no-console
|
|
44
|
+
console.debug(
|
|
45
|
+
`Tried to deserialize block: ${child.outerHTML} in an array but failed to identify it! Error: ${e}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
output.push(deserializedObject);
|
|
49
|
+
});
|
|
50
|
+
return output;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const deserializeObject = (
|
|
54
|
+
objectHTML: Element,
|
|
55
|
+
deserializers: Record<string, any> = customDeserializers,
|
|
56
|
+
blockDeserializers = customBlockDeserializers
|
|
57
|
+
) => {
|
|
58
|
+
const deserialize = deserializers.types[objectHTML.className];
|
|
59
|
+
if (deserialize) {
|
|
60
|
+
return deserialize(objectHTML);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const output: Record<string, any> = {};
|
|
64
|
+
//account for anonymous inline objects
|
|
65
|
+
if (objectHTML.className) {
|
|
66
|
+
output._type = objectHTML.className;
|
|
67
|
+
}
|
|
68
|
+
const children = Array.from(objectHTML.children);
|
|
69
|
+
|
|
70
|
+
children.forEach((child) => {
|
|
71
|
+
//string field
|
|
72
|
+
if (child.tagName?.toLowerCase() === 'span') {
|
|
73
|
+
output[child.className] = preprocess(child.innerHTML);
|
|
74
|
+
}
|
|
75
|
+
//richer field, either object or array
|
|
76
|
+
else if (child.getAttribute('data-level') === 'field') {
|
|
77
|
+
const deserialized = deserializeHTML(
|
|
78
|
+
child.outerHTML,
|
|
79
|
+
deserializers,
|
|
80
|
+
blockDeserializers
|
|
81
|
+
);
|
|
82
|
+
if (deserialized && Object.keys(deserialized).length) {
|
|
83
|
+
output[child.className] = deserialized;
|
|
84
|
+
} else {
|
|
85
|
+
//eslint-disable-next-line no-console
|
|
86
|
+
console.debug(
|
|
87
|
+
`Deserializer: Skipping empty or unreadable HTML: ${child.outerHTML}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} else if (child.getAttribute('data-type') === 'array') {
|
|
91
|
+
output[child.className] = deserializeArray(
|
|
92
|
+
child,
|
|
93
|
+
deserializers,
|
|
94
|
+
blockDeserializers
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return output;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const deserializeHTML = (
|
|
102
|
+
html: string,
|
|
103
|
+
deserializers: Record<string, any>,
|
|
104
|
+
blockDeserializers: Array<any>
|
|
105
|
+
): Record<string, any> | any[] => {
|
|
106
|
+
//parent node is always div with classname of field -- get its child
|
|
107
|
+
let HTMLnode = new DOMParser().parseFromString(html, 'text/html').body
|
|
108
|
+
.children[0];
|
|
109
|
+
|
|
110
|
+
//catch embedded object as a field
|
|
111
|
+
if (HTMLnode?.getAttribute('data-level') === 'field') {
|
|
112
|
+
HTMLnode = HTMLnode.children[0];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!HTMLnode) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let output: Record<string, any> | any[];
|
|
120
|
+
|
|
121
|
+
//prioritize custom deserialization
|
|
122
|
+
const deserialize = deserializers.types[HTMLnode.className];
|
|
123
|
+
if (deserialize) {
|
|
124
|
+
output = deserialize(HTMLnode);
|
|
125
|
+
} else if (HTMLnode.getAttribute('data-type') === 'object') {
|
|
126
|
+
output = deserializeObject(HTMLnode, deserializers, blockDeserializers);
|
|
127
|
+
} else if (HTMLnode.getAttribute('data-type') === 'array') {
|
|
128
|
+
output = deserializeArray(HTMLnode, deserializers, blockDeserializers);
|
|
129
|
+
} else {
|
|
130
|
+
output = {};
|
|
131
|
+
//eslint-disable-next-line no-console
|
|
132
|
+
console.debug(
|
|
133
|
+
`Tried to deserialize block ${HTMLnode.outerHTML} but failed to identify it!`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return output;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const deserializeDocument = (
|
|
141
|
+
serializedDoc: string,
|
|
142
|
+
deserializers: Record<string, any> = customDeserializers,
|
|
143
|
+
blockDeserializers = customBlockDeserializers
|
|
144
|
+
): Record<string, any> => {
|
|
145
|
+
const metadata: Record<string, any> = {};
|
|
146
|
+
const head = new DOMParser().parseFromString(serializedDoc, 'text/html').head;
|
|
147
|
+
|
|
148
|
+
Array.from(head.children).forEach((metaTag) => {
|
|
149
|
+
const validTags = ['_id', '_rev', '_type'];
|
|
150
|
+
const metaName = metaTag.getAttribute('name');
|
|
151
|
+
if (metaName && validTags.includes(metaName)) {
|
|
152
|
+
metadata[metaName] = metaTag.getAttribute('content');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const content: Record<string, any> = deserializeHTML(
|
|
157
|
+
serializedDoc,
|
|
158
|
+
deserializers,
|
|
159
|
+
blockDeserializers
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...content,
|
|
164
|
+
...metadata,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const BaseDocumentDeserializer: Deserializer = {
|
|
169
|
+
deserializeDocument,
|
|
170
|
+
deserializeHTML,
|
|
171
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
|
|
2
|
+
import { htmlToBlocks } from '@portabletext/block-tools';
|
|
3
|
+
import { Schema } from '@sanity/schema';
|
|
4
|
+
import { ObjectField, PortableTextSpan, PortableTextTextBlock } from 'sanity';
|
|
5
|
+
|
|
6
|
+
const defaultSchema = Schema.compile({
|
|
7
|
+
name: 'default',
|
|
8
|
+
types: [
|
|
9
|
+
{
|
|
10
|
+
type: 'object',
|
|
11
|
+
name: 'default',
|
|
12
|
+
fields: [
|
|
13
|
+
{
|
|
14
|
+
name: 'block',
|
|
15
|
+
type: 'array',
|
|
16
|
+
of: [{ type: 'block' }],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const blockContentType = defaultSchema
|
|
24
|
+
.get('default')
|
|
25
|
+
.fields.find((field: ObjectField) => field.name === 'block').type;
|
|
26
|
+
|
|
27
|
+
export const noSchemaWarning = (obj: Element): string =>
|
|
28
|
+
`WARNING: Unfortunately the deserializer may have issues with this field or object: ${obj.className}.
|
|
29
|
+
If it's a specific type, you may need to declare at the top level, or write a custom deserializer.`;
|
|
30
|
+
|
|
31
|
+
//helper to handle messy input -- take advantage
|
|
32
|
+
//of blockTools' sanitizing behavior for single strings
|
|
33
|
+
export const preprocess = (html: string): string => {
|
|
34
|
+
const intermediateBlocks = htmlToBlocks(
|
|
35
|
+
`<p>${html}</p>`,
|
|
36
|
+
blockContentType
|
|
37
|
+
) as PortableTextTextBlock<PortableTextSpan>[];
|
|
38
|
+
if (!intermediateBlocks.length) {
|
|
39
|
+
throw new Error(`Error parsing string '${html}'`);
|
|
40
|
+
}
|
|
41
|
+
return intermediateBlocks[0].children[0].text;
|
|
42
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PortableTextTextBlock } from 'sanity';
|
|
2
|
+
|
|
3
|
+
// Helper function to merge multiple blocks
|
|
4
|
+
// Prioritize blocks[0]
|
|
5
|
+
export function mergeBlocks(blocks: PortableTextTextBlock[]) {
|
|
6
|
+
const mergedBlock = { ...blocks[0] };
|
|
7
|
+
mergedBlock.markDefs = mergedBlock.markDefs ?? [];
|
|
8
|
+
for (const [idx, block] of blocks.entries()) {
|
|
9
|
+
if (idx === 0) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
mergedBlock.children.push(...block.children);
|
|
13
|
+
mergedBlock.markDefs.push(...(block.markDefs ?? []));
|
|
14
|
+
}
|
|
15
|
+
mergedBlock._type = 'block';
|
|
16
|
+
|
|
17
|
+
return mergedBlock;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { BaseDocumentMerger } from './BaseDocumentMerger';
|
|
2
|
+
export { BaseDocumentSerializer } from './serialize';
|
|
3
|
+
export { BaseDocumentDeserializer } from './deserialize/BaseDocumentDeserializer';
|
|
4
|
+
export {
|
|
5
|
+
defaultStopTypes,
|
|
6
|
+
customSerializers,
|
|
7
|
+
customBlockDeserializers,
|
|
8
|
+
} from './BaseSerializationConfig';
|
|
9
|
+
|
|
10
|
+
export type { SerializedDocument, Deserializer, Merger } from './types';
|
|
11
|
+
export { attachGTData, detachGTData } from './data';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
|
|
2
|
+
|
|
3
|
+
import { ObjectField, TypedObject } from 'sanity';
|
|
4
|
+
|
|
5
|
+
const META_FIELDS = ['_key', '_type', '_id'];
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Helper. If field-level translation pattern used, only sends over
|
|
9
|
+
* content from the base language. Works recursively, so if users
|
|
10
|
+
* use this pattern several layers deep, base language fields will still be found.
|
|
11
|
+
*/
|
|
12
|
+
export const languageObjectFieldFilter = (
|
|
13
|
+
obj: Record<string, any>,
|
|
14
|
+
baseLang: string
|
|
15
|
+
): Record<string, any> => {
|
|
16
|
+
const filterToLangField = (childObj: Record<string, any>) => {
|
|
17
|
+
const filteredObj: Record<string, any> = {};
|
|
18
|
+
filteredObj[baseLang] = childObj[baseLang];
|
|
19
|
+
META_FIELDS.forEach((field) => {
|
|
20
|
+
if (childObj[field]) {
|
|
21
|
+
filteredObj[field] = childObj[field];
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return filteredObj;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const findBaseLang = (childObj: Record<string, any>): Record<string, any> => {
|
|
28
|
+
const filteredObj: Record<string, any> = {};
|
|
29
|
+
META_FIELDS.forEach((field) => {
|
|
30
|
+
if (childObj[field]) {
|
|
31
|
+
filteredObj[field] = childObj[field];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (const key in childObj) {
|
|
36
|
+
if (childObj.hasOwnProperty(key)) {
|
|
37
|
+
const value: any = childObj[key];
|
|
38
|
+
//we've reached a base language field, add it to
|
|
39
|
+
//what we want to send to translation
|
|
40
|
+
if (value.hasOwnProperty(baseLang)) {
|
|
41
|
+
filteredObj[key] = filterToLangField(value);
|
|
42
|
+
}
|
|
43
|
+
//we have an array that may have language fields in its objects
|
|
44
|
+
else if (
|
|
45
|
+
Array.isArray(value) &&
|
|
46
|
+
value.length &&
|
|
47
|
+
typeof value[0] === 'object'
|
|
48
|
+
) {
|
|
49
|
+
//recursively find and filter for any objects that have the base language
|
|
50
|
+
const validLangObjects = value.reduce((validArr, objInArray) => {
|
|
51
|
+
if (objInArray._type === 'block') {
|
|
52
|
+
validArr.push(objInArray);
|
|
53
|
+
} else if (objInArray.hasOwnProperty(baseLang)) {
|
|
54
|
+
validArr.push(filterToLangField(objInArray));
|
|
55
|
+
} else {
|
|
56
|
+
const filtered = findBaseLang(objInArray);
|
|
57
|
+
const nonMetaFields = Object.keys(filtered).filter(
|
|
58
|
+
(objInArrayKey) => META_FIELDS.indexOf(objInArrayKey) === -1
|
|
59
|
+
);
|
|
60
|
+
if (nonMetaFields.length) {
|
|
61
|
+
validArr.push(filtered);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return validArr;
|
|
65
|
+
}, []);
|
|
66
|
+
if (validLangObjects.length) {
|
|
67
|
+
filteredObj[key] = validLangObjects;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//we have an object nested in an object
|
|
71
|
+
//recurse down the tree
|
|
72
|
+
else if (typeof value === 'object') {
|
|
73
|
+
const nestedLangObj = findBaseLang(value);
|
|
74
|
+
const nonMetaFields = Object.keys(nestedLangObj).filter(
|
|
75
|
+
(nestedObjKey) => META_FIELDS.indexOf(nestedObjKey) === -1
|
|
76
|
+
);
|
|
77
|
+
if (nonMetaFields.length) {
|
|
78
|
+
filteredObj[key] = nestedLangObj;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return filteredObj;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//send top level object into recursive function
|
|
87
|
+
return findBaseLang(obj);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* Eliminates stop-types and non-localizable fields
|
|
92
|
+
* for document-level translation.
|
|
93
|
+
*/
|
|
94
|
+
export const fieldFilter = (
|
|
95
|
+
obj: Record<string, any>,
|
|
96
|
+
objFields: ObjectField[],
|
|
97
|
+
stopTypes: string[]
|
|
98
|
+
): TypedObject => {
|
|
99
|
+
const filteredObj: TypedObject = { _type: obj._type };
|
|
100
|
+
|
|
101
|
+
const fieldFilterFunc = (field: Record<string, any>) => {
|
|
102
|
+
if (field.localize === false) {
|
|
103
|
+
return false;
|
|
104
|
+
} else if (field.type === 'string' || field.type === 'text') {
|
|
105
|
+
return true;
|
|
106
|
+
} else if (Array.isArray(obj[field.name])) {
|
|
107
|
+
return true;
|
|
108
|
+
} else if (!stopTypes.includes(field.type)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const validFields = [
|
|
115
|
+
...META_FIELDS,
|
|
116
|
+
...objFields?.filter(fieldFilterFunc)?.map((field) => field.name),
|
|
117
|
+
];
|
|
118
|
+
validFields.forEach((field) => {
|
|
119
|
+
if (obj[field]) {
|
|
120
|
+
filteredObj[field] = obj[field];
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return filteredObj;
|
|
124
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
defaultStopTypes,
|
|
5
|
+
customSerializers,
|
|
6
|
+
} from '../BaseSerializationConfig';
|
|
7
|
+
import { SanityDocument, TypedObject, Schema } from 'sanity';
|
|
8
|
+
import { TranslationLevel } from '../types';
|
|
9
|
+
import { fieldFilter, languageObjectFieldFilter } from './fieldFilters';
|
|
10
|
+
import { PortableTextTypeComponent, toHTML } from '@portabletext/to-html';
|
|
11
|
+
|
|
12
|
+
const META_FIELDS = ['_key', '_type', '_id', '_weak'];
|
|
13
|
+
|
|
14
|
+
export const BaseDocumentSerializer = (schemas: Schema) => {
|
|
15
|
+
/*
|
|
16
|
+
* Helper function that allows us to get metadata (like `localize: false`) from schema fields.
|
|
17
|
+
*/
|
|
18
|
+
const getSchema = (name: string) =>
|
|
19
|
+
schemas?._original?.types.find((s) => s.name === name) as any;
|
|
20
|
+
|
|
21
|
+
const serializeObject = (
|
|
22
|
+
obj: TypedObject,
|
|
23
|
+
stopTypes: string[],
|
|
24
|
+
serializers: Record<string, any>
|
|
25
|
+
) => {
|
|
26
|
+
if (stopTypes.includes(obj._type)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//if user has declared a custom serializer, use that
|
|
31
|
+
//instead of this method
|
|
32
|
+
const hasSerializer =
|
|
33
|
+
serializers.types && Object.keys(serializers.types).includes(obj._type);
|
|
34
|
+
if (hasSerializer) {
|
|
35
|
+
return toHTML([obj], { components: serializers });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//we don't need to worry about PT types
|
|
39
|
+
if (obj._type === 'span' || obj._type === 'block') {
|
|
40
|
+
return toHTML(obj, { components: serializers });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//if schema is available, encode values in the order they're declared in the schema,
|
|
44
|
+
//since this will likely be more intuitive for a translator.
|
|
45
|
+
let fieldNames = Object.keys(obj).filter((key) => key !== '_type');
|
|
46
|
+
const schema = getSchema(obj._type);
|
|
47
|
+
if (schema && schema.fields) {
|
|
48
|
+
fieldNames = schema.fields
|
|
49
|
+
.map((field: Record<string, any>) => field.name)
|
|
50
|
+
.filter((schemaKey: string) => Object.keys(obj).includes(schemaKey));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//account for anonymous inline objects
|
|
54
|
+
if (typeof obj === 'object' && !obj._type) {
|
|
55
|
+
obj._type = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//in some cases, we might recurse through many objects of the same type
|
|
59
|
+
//we should take all methods necessary to ensure state does not persist
|
|
60
|
+
//otherwise we risk using old serialization methods on new items
|
|
61
|
+
const newSerializationMethods: Record<string, PortableTextTypeComponent> =
|
|
62
|
+
{};
|
|
63
|
+
const tempType = `${obj._type}__temp_type__${Math.random().toString(36).substring(7)}`;
|
|
64
|
+
const objToSerialize: TypedObject = { _type: tempType };
|
|
65
|
+
//for our default serialization method, we only need to
|
|
66
|
+
//capture metadata. the rest will be recursively turned into strings.
|
|
67
|
+
META_FIELDS.filter((f) => f !== '_type').forEach((field) => {
|
|
68
|
+
objToSerialize[field] = obj[field];
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let innerHTML = '';
|
|
72
|
+
|
|
73
|
+
//if it's a custom object, iterate through its keys to find and serialize translatable content
|
|
74
|
+
fieldNames.forEach((fieldName) => {
|
|
75
|
+
let htmlField = '';
|
|
76
|
+
|
|
77
|
+
if (!META_FIELDS.includes(fieldName)) {
|
|
78
|
+
const value = obj[fieldName];
|
|
79
|
+
//strings are either string fields or have recursively been turned
|
|
80
|
+
//into HTML because they were a nested object or array
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
const htmlRegex = new RegExp(/<("[^"]*"|'[^']*'|[^'">])*>/);
|
|
83
|
+
if (htmlRegex.test(value)) {
|
|
84
|
+
htmlField = value;
|
|
85
|
+
} else {
|
|
86
|
+
htmlField = `<span class="${fieldName}">${value}</span>`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//array fields get filtered and its children serialized
|
|
91
|
+
else if (Array.isArray(value)) {
|
|
92
|
+
htmlField = serializeArray(value, fieldName, stopTypes, {
|
|
93
|
+
...serializers,
|
|
94
|
+
types: { ...serializers.types },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//this is an object in an object, serialize it first
|
|
99
|
+
else {
|
|
100
|
+
const embeddedObject = value as TypedObject;
|
|
101
|
+
const embeddedObjectSchema = getSchema(embeddedObject._type);
|
|
102
|
+
let toTranslate = embeddedObject;
|
|
103
|
+
if (embeddedObjectSchema && embeddedObjectSchema.fields) {
|
|
104
|
+
toTranslate = fieldFilter(
|
|
105
|
+
toTranslate,
|
|
106
|
+
embeddedObjectSchema.fields,
|
|
107
|
+
stopTypes
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const objHTML = serializeObject(toTranslate, stopTypes, {
|
|
111
|
+
...serializers,
|
|
112
|
+
types: { ...serializers.types },
|
|
113
|
+
});
|
|
114
|
+
htmlField = `<div class="${fieldName}" data-level="field">${objHTML}</div>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
innerHTML += htmlField;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!innerHTML) {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
newSerializationMethods[tempType] = ({ value }: { value: TypedObject }) => {
|
|
126
|
+
let div = `<div class="${value._type.split('__temp_type__')[0]}"`;
|
|
127
|
+
if (value._key || value._id) {
|
|
128
|
+
div += `id="${value._key ?? value._id}"`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [div, ` data-type="object">${innerHTML}</div>`].join('');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
let serializedBlock = '';
|
|
135
|
+
try {
|
|
136
|
+
serializedBlock = toHTML(objToSerialize, {
|
|
137
|
+
components: {
|
|
138
|
+
...serializers,
|
|
139
|
+
types: {
|
|
140
|
+
...serializers.types,
|
|
141
|
+
...newSerializationMethods,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
} catch (err) {
|
|
146
|
+
//eslint-disable-next-line no-console -- this is a warning
|
|
147
|
+
console.warn(
|
|
148
|
+
`Had issues serializing block of type "${obj._type}". Please specify a serialization method for this block in your serialization config. Received error: ${err}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return serializedBlock;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const serializeArray = (
|
|
156
|
+
fieldContent: Record<string, any>[],
|
|
157
|
+
fieldName: string,
|
|
158
|
+
stopTypes: string[],
|
|
159
|
+
serializers: Record<string, any>
|
|
160
|
+
) => {
|
|
161
|
+
//filter for any blocks that user has indicated
|
|
162
|
+
//should not be sent for translation
|
|
163
|
+
const validBlocks = fieldContent.filter(
|
|
164
|
+
(block) => !stopTypes.includes(block._type)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
//take out any fields in these blocks that should
|
|
168
|
+
//not be sent to translation
|
|
169
|
+
const filteredBlocks = validBlocks.map((block) => {
|
|
170
|
+
const schema = getSchema(block._type);
|
|
171
|
+
if (schema && schema.fields) {
|
|
172
|
+
return fieldFilter(block, schema.fields, stopTypes);
|
|
173
|
+
}
|
|
174
|
+
return block;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const output = filteredBlocks.map((obj) => {
|
|
178
|
+
//if object in array is just a string, just return it
|
|
179
|
+
if (typeof obj === 'string') {
|
|
180
|
+
return `<span>${obj}</span>`;
|
|
181
|
+
}
|
|
182
|
+
//send to serialization method
|
|
183
|
+
return serializeObject(obj as TypedObject, stopTypes, serializers);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
//encode this with data-level field
|
|
187
|
+
return `<div class="${fieldName}" data-type="array">${output.join('')}</div>`;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Main parent function: finds fields to translate, and feeds them to appropriate child serialization
|
|
192
|
+
* methods.
|
|
193
|
+
*/
|
|
194
|
+
const serializeDocument = (
|
|
195
|
+
doc: SanityDocument,
|
|
196
|
+
translationLevel: TranslationLevel = 'document',
|
|
197
|
+
baseLang = 'en',
|
|
198
|
+
stopTypes = defaultStopTypes,
|
|
199
|
+
serializers = customSerializers
|
|
200
|
+
) => {
|
|
201
|
+
const schema = getSchema(doc._type);
|
|
202
|
+
let filteredObj: Record<string, any> = {};
|
|
203
|
+
|
|
204
|
+
//field level translations explicitly send over any fields that
|
|
205
|
+
//match the base language, regardless of depth
|
|
206
|
+
if (translationLevel === 'field') {
|
|
207
|
+
filteredObj = languageObjectFieldFilter(doc, baseLang);
|
|
208
|
+
}
|
|
209
|
+
//otherwise, we can refer to the schema and a list of stop types
|
|
210
|
+
//to determine what should not be sent
|
|
211
|
+
else {
|
|
212
|
+
filteredObj = fieldFilter(doc, schema.fields, stopTypes);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const serializedFields: Record<string, any> = {};
|
|
216
|
+
|
|
217
|
+
for (const key in filteredObj) {
|
|
218
|
+
if (filteredObj.hasOwnProperty(key) === false) continue;
|
|
219
|
+
const value: Record<string, any> | Array<any> | string = filteredObj[key];
|
|
220
|
+
|
|
221
|
+
if (typeof value === 'string') {
|
|
222
|
+
serializedFields[key] = value;
|
|
223
|
+
} else if (Array.isArray(value)) {
|
|
224
|
+
serializedFields[key] = serializeArray(
|
|
225
|
+
value,
|
|
226
|
+
key,
|
|
227
|
+
stopTypes,
|
|
228
|
+
serializers
|
|
229
|
+
);
|
|
230
|
+
} else if (
|
|
231
|
+
value &&
|
|
232
|
+
!stopTypes.find((stopType) => stopType == value?._type)
|
|
233
|
+
) {
|
|
234
|
+
const serialized = serializeObject(
|
|
235
|
+
value as TypedObject,
|
|
236
|
+
stopTypes,
|
|
237
|
+
serializers
|
|
238
|
+
);
|
|
239
|
+
serializedFields[key] =
|
|
240
|
+
`<div class="${key}" data-level='field'>${serialized}</div>`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//create a valid HTML file
|
|
245
|
+
const rawHTMLBody = document.createElement('body');
|
|
246
|
+
rawHTMLBody.innerHTML = serializeObject(
|
|
247
|
+
serializedFields as TypedObject,
|
|
248
|
+
stopTypes,
|
|
249
|
+
serializers
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const rawHTMLHead = document.createElement('head');
|
|
253
|
+
const metaFields = ['_id', '_type', '_rev'];
|
|
254
|
+
//save our metadata as meta tags so we can use them later on
|
|
255
|
+
metaFields.forEach((field) => {
|
|
256
|
+
const metaEl = document.createElement('meta');
|
|
257
|
+
metaEl.setAttribute('name', field);
|
|
258
|
+
metaEl.setAttribute('content', doc[field] as string);
|
|
259
|
+
rawHTMLHead.appendChild(metaEl);
|
|
260
|
+
});
|
|
261
|
+
//encode version so we can use the correct deserialization methods
|
|
262
|
+
const versionMeta = document.createElement('meta');
|
|
263
|
+
versionMeta.setAttribute('name', 'version');
|
|
264
|
+
versionMeta.setAttribute('content', '3');
|
|
265
|
+
rawHTMLHead.appendChild(versionMeta);
|
|
266
|
+
|
|
267
|
+
const rawHTML = document.createElement('html');
|
|
268
|
+
rawHTML.appendChild(rawHTMLHead);
|
|
269
|
+
rawHTML.appendChild(rawHTMLBody);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
name: doc._id,
|
|
273
|
+
content: rawHTML.outerHTML,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
serializeDocument,
|
|
279
|
+
fieldFilter,
|
|
280
|
+
languageObjectFieldFilter,
|
|
281
|
+
serializeArray,
|
|
282
|
+
serializeObject,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
|
|
2
|
+
|
|
3
|
+
import { ObjectField, SanityDocument, TypedObject, Schema } from 'sanity';
|
|
4
|
+
|
|
5
|
+
export type SerializedDocument = {
|
|
6
|
+
name: string;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type TranslationLevel = 'document' | 'field';
|
|
11
|
+
|
|
12
|
+
export interface Deserializer {
|
|
13
|
+
deserializeDocument: (
|
|
14
|
+
serializedDoc: string,
|
|
15
|
+
deserializers?: Record<string, any>,
|
|
16
|
+
blockDeserializers?: Array<any>
|
|
17
|
+
) => Record<string, any>;
|
|
18
|
+
deserializeHTML: (
|
|
19
|
+
html: string,
|
|
20
|
+
deserializers: Record<string, any>,
|
|
21
|
+
blockDeserializers: Array<any>
|
|
22
|
+
) => Record<string, any> | any[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Merger {
|
|
26
|
+
fieldLevelMerge: (
|
|
27
|
+
translatedFields: Record<string, any>,
|
|
28
|
+
baseDoc: SanityDocument,
|
|
29
|
+
localeId: string,
|
|
30
|
+
baseLang: string
|
|
31
|
+
) => Record<string, any>;
|
|
32
|
+
documentLevelMerge: (
|
|
33
|
+
translatedFields: Record<string, any>,
|
|
34
|
+
baseDoc: SanityDocument
|
|
35
|
+
) => Record<string, any>;
|
|
36
|
+
reconcileArray: (origArray: any[], translatedArray: any[]) => any[];
|
|
37
|
+
reconcileObject: (
|
|
38
|
+
origObject: Record<string, any>,
|
|
39
|
+
translatedObject: Record<string, any>
|
|
40
|
+
) => Record<string, any>;
|
|
41
|
+
}
|