@justanarthur/payload-plugin-translator 1.3.21 → 3.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 +150 -0
- package/dist/client.css +225 -0
- package/dist/client.d.ts +14 -2
- package/dist/client.js +149 -63
- package/dist/index.d.ts +11 -38
- package/dist/index.js +179 -1071
- package/dist/jobs.d.ts +1 -146
- package/dist/jobs.js +22 -817
- package/dist/resolvers/copy.d.ts +1 -3
- package/dist/resolvers/copy.js +6 -16
- package/dist/resolvers/google.d.ts +1 -7
- package/dist/resolvers/google.js +6 -57
- package/dist/resolvers/libreTranslate.d.ts +1 -11
- package/dist/resolvers/libreTranslate.js +6 -59
- package/dist/resolvers/openAI.d.ts +5 -13
- package/dist/resolvers/openAI.js +6 -128
- package/dist/resolvers/types.d.ts +1 -3
- package/dist/rsc.d.ts +5 -0
- package/dist/rsc.js +483 -0
- package/dist/shared/chunk-15n5q7wd.js +253 -0
- package/dist/shared/chunk-31qgv1zk.js +5 -0
- package/dist/shared/chunk-9d433kmv.js +359 -0
- package/dist/shared/chunk-e6c0qzkt.js +6 -0
- package/dist/shared/chunk-mvqymw6n.js +57 -0
- package/dist/shared/chunk-nxddacte.js +243 -0
- package/dist/shared/chunk-p03jz82h.js +623 -0
- package/dist/shared/chunk-zenq7s47.js +15 -0
- package/dist/shared/chunk-zs19h344.js +55 -0
- package/dist/types.d.ts +6 -36
- package/package.json +23 -11
- package/dist/styles-jt3fm5fh.scss +0 -5
- package/dist/styles-vs6m7h55.scss +0 -76
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
samePlaceholders,
|
|
3
|
+
isOpaqueText,
|
|
4
|
+
collectTranslatableFields,
|
|
5
|
+
entityKey,
|
|
6
|
+
sourceHash
|
|
7
|
+
} from "./chunk-9d433kmv.js";
|
|
8
|
+
import {
|
|
9
|
+
TRANSLATION_STATUS_SLUG2
|
|
10
|
+
} from "./chunk-31qgv1zk.js";
|
|
11
|
+
|
|
12
|
+
// src/review/plainText.ts
|
|
13
|
+
var plainText = (value) => {
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return value;
|
|
16
|
+
if (!value || typeof value !== "object")
|
|
17
|
+
return "";
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(plainText).filter(Boolean).join(" ");
|
|
20
|
+
const node = value;
|
|
21
|
+
if (typeof node.text === "string")
|
|
22
|
+
return node.text;
|
|
23
|
+
if ("root" in node)
|
|
24
|
+
return plainText(node.root);
|
|
25
|
+
if (Array.isArray(node.children))
|
|
26
|
+
return node.children.map(plainText).filter(Boolean).join(node.type === "root" ? `
|
|
27
|
+
` : "");
|
|
28
|
+
return "";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/review/computeStatus.ts
|
|
32
|
+
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
33
|
+
var classify = (field, sourceText, targetText) => {
|
|
34
|
+
if (!normalize(targetText))
|
|
35
|
+
return "missing";
|
|
36
|
+
if (field.type === "json" && !samePlaceholders(sourceText, targetText))
|
|
37
|
+
return "placeholders";
|
|
38
|
+
const same = normalize(sourceText) === normalize(targetText);
|
|
39
|
+
if (same && field.type !== "slug" && normalize(sourceText).length > 3 && !isOpaqueText(sourceText) && /\s/.test(normalize(sourceText)))
|
|
40
|
+
return "identical";
|
|
41
|
+
return "ok";
|
|
42
|
+
};
|
|
43
|
+
var computeStatus = (fields) => {
|
|
44
|
+
const statuses = [];
|
|
45
|
+
for (const field of fields) {
|
|
46
|
+
const sourceText = plainText(field.source);
|
|
47
|
+
if (!normalize(sourceText))
|
|
48
|
+
continue;
|
|
49
|
+
const targetText = plainText(field.target);
|
|
50
|
+
statuses.push({ ...field, sourceText, targetText, state: classify(field, sourceText, targetText) });
|
|
51
|
+
}
|
|
52
|
+
const count = (state) => statuses.filter((each) => each.state === state).length;
|
|
53
|
+
const ok = count("ok");
|
|
54
|
+
return {
|
|
55
|
+
fields: statuses,
|
|
56
|
+
summary: {
|
|
57
|
+
total: statuses.length,
|
|
58
|
+
ok,
|
|
59
|
+
missing: count("missing"),
|
|
60
|
+
identical: count("identical"),
|
|
61
|
+
placeholders: count("placeholders"),
|
|
62
|
+
coverage: statuses.length ? Math.floor(ok / statuses.length * 100) : 100,
|
|
63
|
+
slugMissing: statuses.some((each) => each.type === "slug" && each.state === "missing")
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/review/loadReview.ts
|
|
69
|
+
var readLocales = (req) => {
|
|
70
|
+
const localization = req.payload.config.localization;
|
|
71
|
+
if (!localization)
|
|
72
|
+
return { defaultLocale: "", targetLocales: [] };
|
|
73
|
+
const codes = localization.locales.map((each) => typeof each === "string" ? each : each.code);
|
|
74
|
+
return { defaultLocale: localization.defaultLocale, targetLocales: codes.filter((code) => code !== localization.defaultLocale) };
|
|
75
|
+
};
|
|
76
|
+
var pluginOptions = (req) => req.payload.config.custom?.translator;
|
|
77
|
+
var loadStatusRows = async (req, keys) => {
|
|
78
|
+
if (!keys.length || !req.payload.collections[TRANSLATION_STATUS_SLUG2])
|
|
79
|
+
return [];
|
|
80
|
+
const { docs } = await req.payload.find({
|
|
81
|
+
collection: TRANSLATION_STATUS_SLUG2,
|
|
82
|
+
where: { entity: { in: keys } },
|
|
83
|
+
pagination: false,
|
|
84
|
+
depth: 0,
|
|
85
|
+
overrideAccess: false,
|
|
86
|
+
req
|
|
87
|
+
});
|
|
88
|
+
return docs;
|
|
89
|
+
};
|
|
90
|
+
var reviewLocale = (config, source, target, hash, row, req) => {
|
|
91
|
+
const { fields, summary } = computeStatus(collectTranslatableFields({
|
|
92
|
+
config,
|
|
93
|
+
dataFrom: source,
|
|
94
|
+
dataTarget: target ?? {},
|
|
95
|
+
options: pluginOptions(req)?._options
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
fields,
|
|
99
|
+
summary,
|
|
100
|
+
stale: Boolean(row?.sourceHash) && row?.sourceHash !== hash,
|
|
101
|
+
reviewed: Boolean(row?.reviewedHash) && row?.reviewedHash === hash
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
var labelOf = (config, doc) => {
|
|
105
|
+
const title = doc[config.admin?.useAsTitle ?? "id"];
|
|
106
|
+
return typeof title === "string" && title.trim() ? title : `#${doc.id}`;
|
|
107
|
+
};
|
|
108
|
+
var globalLabel = (config) => typeof config.label === "string" ? config.label : config.slug;
|
|
109
|
+
var hashOf = (config, source, req) => sourceHash(collectTranslatableFields({ config, dataFrom: source, dataTarget: {}, options: pluginOptions(req)?._options }));
|
|
110
|
+
var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 }) => {
|
|
111
|
+
const config = req.payload.collections[collectionSlug]?.config;
|
|
112
|
+
if (!config)
|
|
113
|
+
throw new Error(`unknown collection ${collectionSlug}`);
|
|
114
|
+
const { defaultLocale, targetLocales } = readLocales(req);
|
|
115
|
+
const sources = await req.payload.find({
|
|
116
|
+
collection: collectionSlug,
|
|
117
|
+
locale: defaultLocale,
|
|
118
|
+
fallbackLocale: false,
|
|
119
|
+
depth: 0,
|
|
120
|
+
limit,
|
|
121
|
+
page,
|
|
122
|
+
sort: "-updatedAt",
|
|
123
|
+
overrideAccess: false,
|
|
124
|
+
req
|
|
125
|
+
});
|
|
126
|
+
const ids = sources.docs.map((doc) => doc.id);
|
|
127
|
+
const targets = {};
|
|
128
|
+
for (const locale of targetLocales) {
|
|
129
|
+
const { docs } = ids.length ? await req.payload.find({
|
|
130
|
+
collection: collectionSlug,
|
|
131
|
+
locale,
|
|
132
|
+
fallbackLocale: false,
|
|
133
|
+
depth: 0,
|
|
134
|
+
where: { id: { in: ids } },
|
|
135
|
+
pagination: false,
|
|
136
|
+
overrideAccess: false,
|
|
137
|
+
req
|
|
138
|
+
}) : { docs: [] };
|
|
139
|
+
targets[locale] = new Map(docs.map((doc) => [String(doc.id), doc]));
|
|
140
|
+
}
|
|
141
|
+
const keys = ids.map((id) => entityKey({ collectionSlug, id }));
|
|
142
|
+
const rows = await loadStatusRows(req, keys);
|
|
143
|
+
const entities = sources.docs.map((doc) => {
|
|
144
|
+
const source = doc;
|
|
145
|
+
const key = entityKey({ collectionSlug, id: source.id });
|
|
146
|
+
const hash = hashOf(config, source, req);
|
|
147
|
+
return {
|
|
148
|
+
key,
|
|
149
|
+
label: labelOf(config, source),
|
|
150
|
+
collectionSlug,
|
|
151
|
+
id: source.id,
|
|
152
|
+
locales: Object.fromEntries(targetLocales.map((locale) => {
|
|
153
|
+
const { fields: _fields, ...review } = reviewLocale(config, source, targets[locale].get(String(source.id)), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
154
|
+
return [locale, review];
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
entities,
|
|
160
|
+
locales: targetLocales,
|
|
161
|
+
page: sources.page ?? page,
|
|
162
|
+
totalPages: sources.totalPages,
|
|
163
|
+
totalDocs: sources.totalDocs
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var loadGlobalsReview = async ({ req, globalSlugs }) => {
|
|
167
|
+
const { defaultLocale, targetLocales } = readLocales(req);
|
|
168
|
+
const rows = await loadStatusRows(req, globalSlugs.map((globalSlug) => entityKey({ globalSlug })));
|
|
169
|
+
const entities = [];
|
|
170
|
+
for (const globalSlug of globalSlugs) {
|
|
171
|
+
const config = req.payload.config.globals.find((each) => each.slug === globalSlug);
|
|
172
|
+
if (!config)
|
|
173
|
+
continue;
|
|
174
|
+
const read = (locale) => req.payload.findGlobal({
|
|
175
|
+
slug: globalSlug,
|
|
176
|
+
locale,
|
|
177
|
+
fallbackLocale: false,
|
|
178
|
+
depth: 0,
|
|
179
|
+
overrideAccess: false,
|
|
180
|
+
req
|
|
181
|
+
});
|
|
182
|
+
const source = await read(defaultLocale);
|
|
183
|
+
const hash = hashOf(config, source, req);
|
|
184
|
+
const key = entityKey({ globalSlug });
|
|
185
|
+
const locales = {};
|
|
186
|
+
for (const locale of targetLocales) {
|
|
187
|
+
const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
188
|
+
locales[locale] = review;
|
|
189
|
+
}
|
|
190
|
+
entities.push({ key, label: globalLabel(config), globalSlug, locales });
|
|
191
|
+
}
|
|
192
|
+
return { entities, locales: targetLocales };
|
|
193
|
+
};
|
|
194
|
+
var parseEntityKey = (key) => {
|
|
195
|
+
const [scope, ...rest] = key.split(":");
|
|
196
|
+
const value = rest.join(":");
|
|
197
|
+
return scope === "global" ? { globalSlug: value } : { collectionSlug: scope, id: value };
|
|
198
|
+
};
|
|
199
|
+
var loadEntityLocaleReview = async ({ req, entity, locale }) => {
|
|
200
|
+
const { defaultLocale } = readLocales(req);
|
|
201
|
+
const { collectionSlug, globalSlug, id } = parseEntityKey(entity);
|
|
202
|
+
const config = globalSlug ? req.payload.config.globals.find((each) => each.slug === globalSlug) : req.payload.collections[collectionSlug]?.config;
|
|
203
|
+
if (!config)
|
|
204
|
+
throw new Error(`unknown entity ${entity}`);
|
|
205
|
+
const read = (readLocale) => globalSlug ? req.payload.findGlobal({ slug: globalSlug, locale: readLocale, fallbackLocale: false, depth: 0, overrideAccess: false, req }) : req.payload.findByID({ collection: collectionSlug, id, locale: readLocale, fallbackLocale: false, depth: 0, overrideAccess: false, req });
|
|
206
|
+
const source = await read(defaultLocale);
|
|
207
|
+
const target = await read(locale);
|
|
208
|
+
const rows = await loadStatusRows(req, [entity]);
|
|
209
|
+
const row = rows.find((each) => each.locale === locale);
|
|
210
|
+
const review = reviewLocale(config, source, target, hashOf(config, source, req), row, req);
|
|
211
|
+
return {
|
|
212
|
+
...review,
|
|
213
|
+
entity,
|
|
214
|
+
locale,
|
|
215
|
+
defaultLocale,
|
|
216
|
+
collectionSlug,
|
|
217
|
+
globalSlug,
|
|
218
|
+
id,
|
|
219
|
+
label: globalSlug ? globalLabel(config) : labelOf(config, source),
|
|
220
|
+
translatedAt: row?.translatedAt,
|
|
221
|
+
reviewedAt: row?.reviewedAt,
|
|
222
|
+
reviewedBy: row?.reviewedBy,
|
|
223
|
+
lastJob: await loadLastJob(req, { collectionSlug, globalSlug, id }, locale)
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
var loadLastJob = async (req, { collectionSlug, globalSlug, id }, locale) => {
|
|
227
|
+
if (!req.payload.collections["payload-jobs"])
|
|
228
|
+
return null;
|
|
229
|
+
try {
|
|
230
|
+
const { docs } = await req.payload.find({
|
|
231
|
+
collection: "payload-jobs",
|
|
232
|
+
sort: "-createdAt",
|
|
233
|
+
limit: 100,
|
|
234
|
+
depth: 0,
|
|
235
|
+
overrideAccess: true,
|
|
236
|
+
req
|
|
237
|
+
});
|
|
238
|
+
const job = docs.find(({ input }) => input && (globalSlug ? input.global === globalSlug : input.collection === collectionSlug && String(input.id) === String(id)) && (!Array.isArray(input.toLocales) || input.toLocales.includes(locale)));
|
|
239
|
+
if (!job)
|
|
240
|
+
return null;
|
|
241
|
+
return {
|
|
242
|
+
id: job.id,
|
|
243
|
+
createdAt: job.createdAt,
|
|
244
|
+
completedAt: job.completedAt,
|
|
245
|
+
processing: Boolean(job.processing),
|
|
246
|
+
error: job.hasError ? typeof job.error === "string" ? job.error : JSON.stringify(job.error) : undefined
|
|
247
|
+
};
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export { readLocales, loadCollectionReview, loadGlobalsReview, parseEntityKey, loadEntityLocaleReview };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/utils/placeholders.ts
|
|
2
|
+
var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
|
|
3
|
+
var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
|
|
4
|
+
|
|
5
|
+
// src/translate/traverseFields.ts
|
|
6
|
+
import ObjectID from "bson-objectid";
|
|
7
|
+
import { tabHasName } from "payload/shared";
|
|
8
|
+
|
|
9
|
+
// src/utils/isEmpty.ts
|
|
10
|
+
var isEmpty = (value) => {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.length === 0;
|
|
13
|
+
if (value === null || typeof value === "undefined")
|
|
14
|
+
return true;
|
|
15
|
+
if (typeof value === "object" && Object.keys(value).length === 0)
|
|
16
|
+
return true;
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/utils/hasText.ts
|
|
21
|
+
var hasText = (value) => {
|
|
22
|
+
if (typeof value === "string")
|
|
23
|
+
return value.trim().length > 0;
|
|
24
|
+
if (isEmpty(value))
|
|
25
|
+
return false;
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.some(hasText);
|
|
28
|
+
if (typeof value === "object") {
|
|
29
|
+
const node = value;
|
|
30
|
+
if (typeof node.text === "string" && node.text.trim())
|
|
31
|
+
return true;
|
|
32
|
+
if ("root" in node)
|
|
33
|
+
return hasText(node.root);
|
|
34
|
+
if (Array.isArray(node.children))
|
|
35
|
+
return node.children.some(hasText);
|
|
36
|
+
if (node.type === "block" || node.type === "inlineBlock" || node.type === "upload")
|
|
37
|
+
return true;
|
|
38
|
+
return !("children" in node) && Object.values(node).some(hasText);
|
|
39
|
+
}
|
|
40
|
+
return value !== null && value !== undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/utils/isOpaqueText.ts
|
|
44
|
+
var isOpaqueText = (value) => {
|
|
45
|
+
const text = value.trim();
|
|
46
|
+
return /^([a-z][a-z0-9+.-]*:|\/\/|\/|#|www\.)/i.test(text) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) || !/\p{L}/u.test(text);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/utils/sanitizeSlug.ts
|
|
50
|
+
var sanitizeSlug = (text) => text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
51
|
+
|
|
52
|
+
// src/translate/traverseRichText.ts
|
|
53
|
+
var traverseRichText = ({
|
|
54
|
+
onText,
|
|
55
|
+
root,
|
|
56
|
+
siblingData,
|
|
57
|
+
additionalTraverseRichText
|
|
58
|
+
}) => {
|
|
59
|
+
siblingData = siblingData ?? root;
|
|
60
|
+
if (typeof siblingData.text === "string" && siblingData.text.trim()) {
|
|
61
|
+
onText(siblingData);
|
|
62
|
+
}
|
|
63
|
+
if (!("text" in siblingData)) {
|
|
64
|
+
additionalTraverseRichText?.({ onText, root, siblingData });
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(siblingData.children)) {
|
|
67
|
+
for (const child of siblingData.children) {
|
|
68
|
+
if (child && typeof child === "object")
|
|
69
|
+
traverseRichText({
|
|
70
|
+
onText,
|
|
71
|
+
root,
|
|
72
|
+
siblingData: child,
|
|
73
|
+
additionalTraverseRichText
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/translate/traverseFields.ts
|
|
80
|
+
var joinPath = (base, segment) => base ? `${base}.${segment}` : segment;
|
|
81
|
+
var buildLocalizedRows = (from, existing, isBlocks, emptyOnly) => {
|
|
82
|
+
const sameShape = existing.length === from.length && (!isBlocks || existing.every((row, index) => row.blockType === from[index].blockType));
|
|
83
|
+
if (emptyOnly && sameShape)
|
|
84
|
+
return existing;
|
|
85
|
+
return from.map((row, index) => {
|
|
86
|
+
const previous = existing[index] && (!isBlocks || existing[index].blockType === row.blockType) ? existing[index] : undefined;
|
|
87
|
+
return {
|
|
88
|
+
...previous ?? {},
|
|
89
|
+
id: previous?.id ?? ObjectID().toHexString(),
|
|
90
|
+
...isBlocks ? { blockType: row.blockType, blockName: row.blockName ?? previous?.blockName } : {}
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
var traverseFields = (args) => {
|
|
95
|
+
const {
|
|
96
|
+
dataFrom,
|
|
97
|
+
emptyOnly,
|
|
98
|
+
fields,
|
|
99
|
+
localizedParent,
|
|
100
|
+
path,
|
|
101
|
+
translatedData,
|
|
102
|
+
valuesToTranslate,
|
|
103
|
+
onField,
|
|
104
|
+
_options
|
|
105
|
+
} = args;
|
|
106
|
+
const { additionalTraverseRichText } = _options ?? {};
|
|
107
|
+
const siblingDataFrom = args.siblingDataFrom ?? dataFrom;
|
|
108
|
+
const siblingDataTranslated = args.siblingDataTranslated ?? translatedData;
|
|
109
|
+
const recurse = (overrides) => traverseFields({ ...args, siblingDataFrom, siblingDataTranslated, ...overrides });
|
|
110
|
+
for (const field of fields) {
|
|
111
|
+
switch (field.type) {
|
|
112
|
+
case "tabs":
|
|
113
|
+
for (const tab of field.tabs) {
|
|
114
|
+
const hasName = tabHasName(tab);
|
|
115
|
+
const tabDataFrom = hasName ? siblingDataFrom[tab.name] : siblingDataFrom;
|
|
116
|
+
if (!tabDataFrom)
|
|
117
|
+
continue;
|
|
118
|
+
let tabDataTranslated;
|
|
119
|
+
if (hasName) {
|
|
120
|
+
if (!siblingDataTranslated[tab.name])
|
|
121
|
+
siblingDataTranslated[tab.name] = {};
|
|
122
|
+
tabDataTranslated = siblingDataTranslated[tab.name];
|
|
123
|
+
} else {
|
|
124
|
+
tabDataTranslated = siblingDataTranslated;
|
|
125
|
+
}
|
|
126
|
+
recurse({
|
|
127
|
+
fields: tab.fields,
|
|
128
|
+
localizedParent: localizedParent || "localized" in tab && Boolean(tab.localized),
|
|
129
|
+
path: hasName ? joinPath(path, tab.name) : path,
|
|
130
|
+
siblingDataFrom: tabDataFrom,
|
|
131
|
+
siblingDataTranslated: tabDataTranslated
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case "group": {
|
|
136
|
+
if (!("name" in field) || !field.name) {
|
|
137
|
+
recurse({ fields: field.fields });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const groupDataFrom = siblingDataFrom[field.name];
|
|
141
|
+
if (!groupDataFrom)
|
|
142
|
+
break;
|
|
143
|
+
if (!siblingDataTranslated[field.name])
|
|
144
|
+
siblingDataTranslated[field.name] = {};
|
|
145
|
+
recurse({
|
|
146
|
+
fields: field.fields,
|
|
147
|
+
localizedParent: localizedParent || Boolean(field.localized),
|
|
148
|
+
path: joinPath(path, field.name),
|
|
149
|
+
siblingDataFrom: groupDataFrom,
|
|
150
|
+
siblingDataTranslated: siblingDataTranslated[field.name]
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "array":
|
|
155
|
+
case "blocks": {
|
|
156
|
+
const isBlocks = field.type === "blocks";
|
|
157
|
+
const rowsFrom = siblingDataFrom[field.name];
|
|
158
|
+
if (isEmpty(rowsFrom) || !Array.isArray(rowsFrom))
|
|
159
|
+
break;
|
|
160
|
+
const existing = Array.isArray(siblingDataTranslated[field.name]) ? siblingDataTranslated[field.name] : [];
|
|
161
|
+
const localized = Boolean(field.localized || localizedParent);
|
|
162
|
+
const rows = localized ? buildLocalizedRows(rowsFrom, existing, isBlocks, emptyOnly) : existing.length ? existing : structuredClone(rowsFrom);
|
|
163
|
+
rows.forEach((row, index) => {
|
|
164
|
+
const rowFrom = rowsFrom[index];
|
|
165
|
+
if (!rowFrom)
|
|
166
|
+
return;
|
|
167
|
+
if (!isBlocks) {
|
|
168
|
+
recurse({
|
|
169
|
+
fields: field.fields,
|
|
170
|
+
localizedParent: localized,
|
|
171
|
+
path: `${joinPath(path, field.name)}[${index}]`,
|
|
172
|
+
siblingDataFrom: rowFrom,
|
|
173
|
+
siblingDataTranslated: row
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const block = field.blocks.find((each) => each.slug === row.blockType);
|
|
178
|
+
if (!block)
|
|
179
|
+
return;
|
|
180
|
+
recurse({
|
|
181
|
+
fields: block.fields,
|
|
182
|
+
localizedParent: localized,
|
|
183
|
+
path: `${joinPath(path, field.name)}[${index}](${row.blockType})`,
|
|
184
|
+
siblingDataFrom: rowFrom,
|
|
185
|
+
siblingDataTranslated: row
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
siblingDataTranslated[field.name] = rows;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "collapsible":
|
|
192
|
+
case "row":
|
|
193
|
+
recurse({ fields: field.fields });
|
|
194
|
+
break;
|
|
195
|
+
case "date":
|
|
196
|
+
case "checkbox":
|
|
197
|
+
case "code":
|
|
198
|
+
case "email":
|
|
199
|
+
case "number":
|
|
200
|
+
case "point":
|
|
201
|
+
case "radio":
|
|
202
|
+
case "relationship":
|
|
203
|
+
case "select":
|
|
204
|
+
case "upload":
|
|
205
|
+
siblingDataTranslated[field.name] = siblingDataFrom[field.name];
|
|
206
|
+
break;
|
|
207
|
+
case "json": {
|
|
208
|
+
if (!(field.localized || localizedParent))
|
|
209
|
+
break;
|
|
210
|
+
const jsonDataFrom = siblingDataFrom[field.name];
|
|
211
|
+
if (isEmpty(jsonDataFrom))
|
|
212
|
+
break;
|
|
213
|
+
const jsonPath = joinPath(path, field.name);
|
|
214
|
+
const current = siblingDataTranslated[field.name];
|
|
215
|
+
const currentIsObject = Boolean(current) && typeof current === "object";
|
|
216
|
+
const jsonDataTranslated = emptyOnly && currentIsObject ? structuredClone(current) : structuredClone(jsonDataFrom);
|
|
217
|
+
siblingDataTranslated[field.name] = jsonDataTranslated;
|
|
218
|
+
const traverseObject = (source, target, target0, objPath) => {
|
|
219
|
+
if (!source || typeof source !== "object")
|
|
220
|
+
return;
|
|
221
|
+
for (const key of Object.keys(source)) {
|
|
222
|
+
const value = source[key];
|
|
223
|
+
const previous = target0 && typeof target0 === "object" ? target0[key] : undefined;
|
|
224
|
+
const keyPath = joinPath(objPath, key);
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
onField?.({ path: keyPath, type: "json", source: value, target: previous });
|
|
227
|
+
if (!value.trim())
|
|
228
|
+
continue;
|
|
229
|
+
if (emptyOnly && typeof previous === "string" && previous.trim())
|
|
230
|
+
continue;
|
|
231
|
+
target[key] = value;
|
|
232
|
+
if (isOpaqueText(value))
|
|
233
|
+
continue;
|
|
234
|
+
valuesToTranslate.push({
|
|
235
|
+
onTranslate: (translated) => {
|
|
236
|
+
target[key] = translated;
|
|
237
|
+
},
|
|
238
|
+
value,
|
|
239
|
+
path: keyPath
|
|
240
|
+
});
|
|
241
|
+
} else if (value && typeof value === "object") {
|
|
242
|
+
if (!target[key] || typeof target[key] !== "object")
|
|
243
|
+
target[key] = Array.isArray(value) ? [] : {};
|
|
244
|
+
traverseObject(value, target[key], previous, keyPath);
|
|
245
|
+
} else if (!(key in target)) {
|
|
246
|
+
target[key] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
traverseObject(jsonDataFrom, jsonDataTranslated, currentIsObject ? current : undefined, jsonPath);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "text":
|
|
254
|
+
case "textarea": {
|
|
255
|
+
if (field.custom && typeof field.custom === "object" && field.custom.translatorSkip)
|
|
256
|
+
break;
|
|
257
|
+
if (!(field.localized || localizedParent))
|
|
258
|
+
break;
|
|
259
|
+
if (field.name === "blockName" || field.name === "id")
|
|
260
|
+
break;
|
|
261
|
+
const value = siblingDataFrom[field.name];
|
|
262
|
+
const current = siblingDataTranslated[field.name];
|
|
263
|
+
const fieldPath = joinPath(path, field.name);
|
|
264
|
+
const isSlug = field.name === "slug";
|
|
265
|
+
onField?.({ path: fieldPath, type: isSlug ? "slug" : "text", source: value, target: current });
|
|
266
|
+
if (typeof value !== "string" || !value.trim())
|
|
267
|
+
break;
|
|
268
|
+
const hasCurrent = typeof current === "string" && current.trim().length > 0;
|
|
269
|
+
if (hasCurrent && (emptyOnly || isSlug))
|
|
270
|
+
break;
|
|
271
|
+
if (isOpaqueText(value)) {
|
|
272
|
+
siblingDataTranslated[field.name] = value;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (isSlug) {
|
|
276
|
+
const segments = value.split("_");
|
|
277
|
+
segments.forEach((segment, index) => {
|
|
278
|
+
valuesToTranslate.push({
|
|
279
|
+
onTranslate: (translated) => {
|
|
280
|
+
segments[index] = sanitizeSlug(translated) || segment;
|
|
281
|
+
siblingDataTranslated[field.name] = segments.join("_");
|
|
282
|
+
},
|
|
283
|
+
value: segment.replace(/-/g, " "),
|
|
284
|
+
path: `${fieldPath}#${index}`
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
valuesToTranslate.push({
|
|
290
|
+
onTranslate: (translated) => {
|
|
291
|
+
siblingDataTranslated[field.name] = translated;
|
|
292
|
+
},
|
|
293
|
+
value,
|
|
294
|
+
path: fieldPath
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "richText": {
|
|
299
|
+
if (!(field.localized || localizedParent))
|
|
300
|
+
break;
|
|
301
|
+
const richTextDataFrom = siblingDataFrom[field.name];
|
|
302
|
+
const current = siblingDataTranslated[field.name];
|
|
303
|
+
const richTextPath = joinPath(path, field.name);
|
|
304
|
+
onField?.({ path: richTextPath, type: "richText", source: richTextDataFrom, target: current });
|
|
305
|
+
if (!richTextDataFrom || !hasText(richTextDataFrom))
|
|
306
|
+
break;
|
|
307
|
+
if (emptyOnly && hasText(current))
|
|
308
|
+
break;
|
|
309
|
+
const isSlate = Array.isArray(richTextDataFrom);
|
|
310
|
+
const isLexical = typeof richTextDataFrom === "object" && "root" in richTextDataFrom;
|
|
311
|
+
if (!isSlate && !isLexical)
|
|
312
|
+
break;
|
|
313
|
+
const richTextTranslated = structuredClone(richTextDataFrom);
|
|
314
|
+
siblingDataTranslated[field.name] = richTextTranslated;
|
|
315
|
+
let richTextNodeIndex = 0;
|
|
316
|
+
const onText = (siblingData, attribute = "text") => {
|
|
317
|
+
valuesToTranslate.push({
|
|
318
|
+
onTranslate: (translated) => {
|
|
319
|
+
siblingData[attribute] = translated;
|
|
320
|
+
},
|
|
321
|
+
value: siblingData[attribute],
|
|
322
|
+
path: `${richTextPath}#${richTextNodeIndex++}`
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
const roots = isLexical ? [richTextTranslated.root] : richTextTranslated;
|
|
326
|
+
for (const root of roots) {
|
|
327
|
+
if (root && typeof root === "object")
|
|
328
|
+
traverseRichText({ onText, root, additionalTraverseRichText });
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
default:
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/review/collectTranslatableFields.ts
|
|
339
|
+
var collectTranslatableFields = ({ config, dataFrom, dataTarget, options }) => {
|
|
340
|
+
const fields = [];
|
|
341
|
+
traverseFields({
|
|
342
|
+
dataFrom,
|
|
343
|
+
fields: config.fields,
|
|
344
|
+
translatedData: structuredClone(dataTarget ?? {}),
|
|
345
|
+
valuesToTranslate: [],
|
|
346
|
+
onField: (field) => fields.push(field),
|
|
347
|
+
_options: options
|
|
348
|
+
});
|
|
349
|
+
return fields;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/review/entityKey.ts
|
|
353
|
+
var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${globalSlug}` : `${collectionSlug}:${id}`;
|
|
354
|
+
|
|
355
|
+
// src/review/sourceHash.ts
|
|
356
|
+
import { createHash } from "node:crypto";
|
|
357
|
+
var sourceHash = (fields) => createHash("sha1").update(JSON.stringify(fields.map(({ path, source }) => [path, source]))).digest("hex");
|
|
358
|
+
|
|
359
|
+
export { samePlaceholders, isOpaqueText, traverseFields, collectTranslatableFields, entityKey, sourceHash };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chunkArray
|
|
3
|
+
} from "./chunk-e6c0qzkt.js";
|
|
4
|
+
|
|
5
|
+
// src/resolvers/libreTranslate.ts
|
|
6
|
+
var localeToCountryCodeMapper = {
|
|
7
|
+
ua: "uk"
|
|
8
|
+
};
|
|
9
|
+
var mapLocale = (incoming) => (incoming in localeToCountryCodeMapper) ? localeToCountryCodeMapper[incoming] : incoming;
|
|
10
|
+
var libreResolver2 = ({
|
|
11
|
+
apiKey,
|
|
12
|
+
chunkLength = 100,
|
|
13
|
+
url = "https://libretranslate.com/translate"
|
|
14
|
+
}) => {
|
|
15
|
+
return {
|
|
16
|
+
key: "libre",
|
|
17
|
+
resolve: async (args) => {
|
|
18
|
+
const { localeFrom, localeTo, req, texts } = args;
|
|
19
|
+
const apiUrl = url;
|
|
20
|
+
const responses = await Promise.all(chunkArray(texts, chunkLength).map((q) => fetch(apiUrl, {
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
api_key: apiKey,
|
|
23
|
+
q,
|
|
24
|
+
source: mapLocale(localeFrom),
|
|
25
|
+
target: mapLocale(localeTo)
|
|
26
|
+
}),
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
method: "POST"
|
|
31
|
+
}).then(async (res) => {
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
req.payload.logger.info({
|
|
35
|
+
libreResponse: data,
|
|
36
|
+
message: "An error occurred when trying to translate the data using LibreTranslate API"
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
data,
|
|
40
|
+
success: res.ok
|
|
41
|
+
};
|
|
42
|
+
})));
|
|
43
|
+
if (responses.some((res) => !res.success)) {
|
|
44
|
+
return {
|
|
45
|
+
success: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const translatedTexts = responses.flatMap((chunk) => chunk.data.translatedText);
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
translatedTexts
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { libreResolver2 };
|