@justanarthur/payload-plugin-translator 3.0.3 → 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 +20 -2
- package/dist/client.css +184 -0
- package/dist/client.d.ts +14 -2
- package/dist/client.js +97 -11
- package/dist/index.d.ts +13 -1
- package/dist/index.js +171 -42
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +21 -20
- package/dist/resolvers/copy.d.ts +1 -1
- package/dist/resolvers/copy.js +5 -5
- package/dist/resolvers/google.d.ts +1 -1
- package/dist/resolvers/google.js +5 -6
- package/dist/resolvers/libreTranslate.d.ts +1 -1
- package/dist/resolvers/libreTranslate.js +5 -6
- package/dist/resolvers/openAI.d.ts +4 -2
- package/dist/resolvers/openAI.js +5 -6
- package/dist/resolvers/types.d.ts +1 -1
- 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-zhrw0jnz.js → chunk-mvqymw6n.js} +3 -3
- package/dist/shared/chunk-nxddacte.js +243 -0
- package/dist/shared/{chunk-q51q8rd2.js → chunk-p03jz82h.js} +280 -512
- package/dist/shared/{chunk-97ktc51q.js → chunk-zenq7s47.js} +2 -2
- package/dist/shared/{chunk-fsveh29m.js → chunk-zs19h344.js} +3 -3
- package/dist/types.d.ts +8 -0
- package/package.json +18 -9
- package/dist/shared/chunk-ecnph6hw.js +0 -205
- /package/dist/shared/{chunk-frnemj69.js → chunk-e6c0qzkt.js} +0 -0
|
@@ -1,15 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
samePlaceholders,
|
|
3
|
+
traverseFields,
|
|
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/translate/operation.ts
|
|
13
|
+
import he from "he";
|
|
14
|
+
import { APIError as APIError3 } from "payload";
|
|
15
|
+
|
|
16
|
+
// src/translate/findEntityWithConfig.ts
|
|
17
|
+
import { APIError } from "payload";
|
|
18
|
+
var findConfigBySlug = (slug, enities) => enities.find((entity) => entity.slug === slug);
|
|
19
|
+
var findEntityWithConfig = async (args) => {
|
|
20
|
+
const { collectionSlug, globalSlug, id, locale, overrideAccess, req } = args;
|
|
21
|
+
if (!collectionSlug && !globalSlug)
|
|
22
|
+
throw new APIError("Bad Request", 400);
|
|
23
|
+
const { payload } = req;
|
|
24
|
+
const { config } = payload;
|
|
25
|
+
const isGlobal = !!globalSlug;
|
|
26
|
+
if (!isGlobal && !id)
|
|
27
|
+
throw new APIError("Bad Request", 400);
|
|
28
|
+
const entityConfig = isGlobal ? findConfigBySlug(globalSlug, config.globals) : findConfigBySlug(collectionSlug, config.collections);
|
|
29
|
+
if (!entityConfig)
|
|
30
|
+
throw new APIError("Bad Request", 400);
|
|
31
|
+
const docPromise = isGlobal ? payload.findGlobal({
|
|
32
|
+
depth: 0,
|
|
33
|
+
fallbackLocale: false,
|
|
34
|
+
locale,
|
|
35
|
+
overrideAccess,
|
|
36
|
+
req,
|
|
37
|
+
slug: args.globalSlug
|
|
38
|
+
}) : payload.findByID({
|
|
39
|
+
collection: collectionSlug,
|
|
40
|
+
depth: 0,
|
|
41
|
+
fallbackLocale: false,
|
|
42
|
+
id,
|
|
43
|
+
locale,
|
|
44
|
+
overrideAccess,
|
|
45
|
+
req
|
|
46
|
+
});
|
|
47
|
+
const doc = await docPromise;
|
|
48
|
+
return {
|
|
49
|
+
config: entityConfig,
|
|
50
|
+
doc
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/translate/updateEntity.ts
|
|
55
|
+
import { APIError as APIError2 } from "payload";
|
|
56
|
+
var updateEntity = ({
|
|
57
|
+
collectionSlug,
|
|
58
|
+
data,
|
|
59
|
+
depth: incomingDepth,
|
|
60
|
+
globalSlug,
|
|
61
|
+
id,
|
|
62
|
+
locale,
|
|
63
|
+
overrideAccess,
|
|
64
|
+
req
|
|
65
|
+
}) => {
|
|
66
|
+
if (!collectionSlug && !globalSlug)
|
|
67
|
+
throw new APIError2("Bad Request", 400);
|
|
68
|
+
const isGlobal = !!globalSlug;
|
|
69
|
+
if (!isGlobal && !id)
|
|
70
|
+
throw new APIError2("Bad Request", 400);
|
|
71
|
+
const depth = incomingDepth ?? req.payload.config.defaultDepth;
|
|
72
|
+
const promise = isGlobal ? req.payload.updateGlobal({
|
|
73
|
+
data,
|
|
74
|
+
depth,
|
|
75
|
+
context: { disableAutoTranslate: true },
|
|
76
|
+
locale,
|
|
77
|
+
overrideAccess,
|
|
78
|
+
req,
|
|
79
|
+
slug: globalSlug
|
|
80
|
+
}) : req.payload.update({
|
|
81
|
+
collection: collectionSlug,
|
|
82
|
+
context: { disableAutoTranslate: true },
|
|
83
|
+
data,
|
|
84
|
+
depth,
|
|
85
|
+
id,
|
|
86
|
+
locale,
|
|
87
|
+
overrideAccess,
|
|
88
|
+
req
|
|
89
|
+
});
|
|
90
|
+
return promise;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/translate/operation.ts
|
|
94
|
+
var preview = (value) => {
|
|
95
|
+
const flat = (typeof value === "string" ? value : String(value ?? "")).replace(/\s+/g, " ").trim();
|
|
96
|
+
return JSON.stringify(flat.length > 60 ? `${flat.slice(0, 57)}…` : flat);
|
|
97
|
+
};
|
|
98
|
+
var translateOperation2 = async (args) => {
|
|
99
|
+
const req = "req" in args ? args.req : {
|
|
100
|
+
payload: args.payload
|
|
101
|
+
};
|
|
102
|
+
const { collectionSlug, globalSlug, id, locale, localeFrom, overrideAccess } = args;
|
|
103
|
+
const { config, doc: dataFrom } = await findEntityWithConfig({
|
|
104
|
+
collectionSlug,
|
|
105
|
+
globalSlug,
|
|
106
|
+
id,
|
|
107
|
+
locale: localeFrom,
|
|
108
|
+
overrideAccess,
|
|
109
|
+
req
|
|
110
|
+
});
|
|
111
|
+
const resolver = (req.payload.config.custom?.translator?.resolvers ?? []).find((each) => each.key === args.resolver);
|
|
112
|
+
if (!resolver)
|
|
113
|
+
throw new APIError3(`Resolver with the key ${args.resolver} was not found`);
|
|
114
|
+
const valuesToTranslate = [];
|
|
115
|
+
let translatedData = args.data;
|
|
116
|
+
if (!translatedData) {
|
|
117
|
+
const { doc } = await findEntityWithConfig({
|
|
118
|
+
collectionSlug,
|
|
119
|
+
globalSlug,
|
|
120
|
+
id,
|
|
121
|
+
locale,
|
|
122
|
+
overrideAccess,
|
|
123
|
+
req
|
|
124
|
+
});
|
|
125
|
+
translatedData = doc;
|
|
126
|
+
}
|
|
127
|
+
traverseFields({
|
|
128
|
+
dataFrom,
|
|
129
|
+
emptyOnly: args.emptyOnly,
|
|
130
|
+
fields: config.fields,
|
|
131
|
+
translatedData,
|
|
132
|
+
valuesToTranslate,
|
|
133
|
+
_options: req.payload.config.custom?.translator?._options
|
|
134
|
+
});
|
|
135
|
+
const entityLabel = `${collectionSlug || globalSlug}#${id ?? ""}`;
|
|
136
|
+
const direction = `${args.localeFrom}→${args.locale}`;
|
|
137
|
+
req.payload.logger.info({
|
|
138
|
+
msg: `[translate] ${entityLabel} ${direction}: traversed ${valuesToTranslate.length} translatable value(s)`
|
|
139
|
+
});
|
|
140
|
+
const resolveResult = valuesToTranslate.length === 0 ? { success: true, translatedTexts: [] } : await resolver.resolve({
|
|
141
|
+
localeFrom: args.localeFrom,
|
|
142
|
+
localeTo: args.locale,
|
|
143
|
+
req,
|
|
144
|
+
texts: valuesToTranslate.map((each) => each.value)
|
|
145
|
+
});
|
|
146
|
+
let result;
|
|
147
|
+
if (!resolveResult.success) {
|
|
148
|
+
req.payload.logger.warn({
|
|
149
|
+
msg: `[translate] ${entityLabel} ${direction}: resolver failed — ${valuesToTranslate.length} value(s) traversed but NOT translated` + (valuesToTranslate.length ? `
|
|
150
|
+
${valuesToTranslate.map((v) => ` ${v.path ?? "(unknown)"}: ${preview(v.value)}`).join(`
|
|
151
|
+
`)}` : "")
|
|
152
|
+
});
|
|
153
|
+
result = {
|
|
154
|
+
success: false
|
|
155
|
+
};
|
|
156
|
+
} else if (resolveResult.translatedTexts.length !== valuesToTranslate.length) {
|
|
157
|
+
req.payload.logger.error({
|
|
158
|
+
msg: `[translate] ${entityLabel} ${direction}: resolver returned ${resolveResult.translatedTexts.length} value(s) for ${valuesToTranslate.length} — nothing applied`
|
|
159
|
+
});
|
|
160
|
+
result = {
|
|
161
|
+
success: false
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
const summary = [];
|
|
165
|
+
resolveResult.translatedTexts.forEach((translated, index) => {
|
|
166
|
+
const entry = valuesToTranslate[index];
|
|
167
|
+
const formattedValue = typeof entry.value === "string" && /&[#\w]+;/.test(entry.value) ? translated : he.decode(translated);
|
|
168
|
+
if (typeof entry.value === "string" && !samePlaceholders(entry.value, formattedValue)) {
|
|
169
|
+
summary.push(` ${entry.path ?? "(unknown)"}: placeholders changed, kept source ${preview(entry.value)}`);
|
|
170
|
+
entry.onTranslate(entry.value);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
summary.push(` ${entry.path ?? "(unknown)"}: ${preview(entry.value)} → ${preview(formattedValue)}`);
|
|
174
|
+
entry.onTranslate(formattedValue);
|
|
175
|
+
});
|
|
176
|
+
req.payload.logger.info({
|
|
177
|
+
msg: `[translate] ${entityLabel} ${direction}: translated ${resolveResult.translatedTexts.length} value(s)` + (summary.length ? `
|
|
178
|
+
${summary.join(`
|
|
179
|
+
`)}` : "")
|
|
180
|
+
});
|
|
181
|
+
if (args.update) {
|
|
182
|
+
const { _locale, _parent_id, createdAt, updatedAt, ...data } = translatedData;
|
|
183
|
+
await updateEntity({
|
|
184
|
+
collectionSlug,
|
|
185
|
+
data,
|
|
186
|
+
depth: 0,
|
|
187
|
+
globalSlug,
|
|
188
|
+
id,
|
|
189
|
+
locale,
|
|
190
|
+
overrideAccess,
|
|
191
|
+
req
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
result = {
|
|
195
|
+
success: true,
|
|
196
|
+
translatedData,
|
|
197
|
+
dataFrom
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
};
|
|
202
|
+
|
|
1
203
|
// src/jobs/constants.ts
|
|
2
|
-
var
|
|
3
|
-
var
|
|
204
|
+
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
205
|
+
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
4
206
|
|
|
5
207
|
// src/jobs/createAutoTranslateCollectionHook.ts
|
|
6
|
-
function
|
|
208
|
+
function createAutoTranslateCollectionHook2(options) {
|
|
7
209
|
const {
|
|
8
210
|
collectionSlug,
|
|
9
211
|
defaultLocale: defaultLocaleOption,
|
|
10
212
|
targetLocales: targetLocalesOption,
|
|
11
213
|
onlyOnPublished = true,
|
|
12
|
-
workflowSlug =
|
|
214
|
+
workflowSlug = TRANSLATE_WORKFLOW_SLUG2
|
|
13
215
|
} = options;
|
|
14
216
|
const afterChange = async ({ doc, req }) => {
|
|
15
217
|
if (shouldSkipAutoTranslate(req.context))
|
|
@@ -39,6 +241,7 @@ function createAutoTranslateCollectionHook(options) {
|
|
|
39
241
|
return doc;
|
|
40
242
|
try {
|
|
41
243
|
const job = await req.payload.jobs.queue({
|
|
244
|
+
req,
|
|
42
245
|
workflow: workflowSlug,
|
|
43
246
|
input: {
|
|
44
247
|
id: typed.id,
|
|
@@ -103,12 +306,12 @@ function readFirstResolverKey(req) {
|
|
|
103
306
|
}
|
|
104
307
|
|
|
105
308
|
// src/jobs/createAutoTranslateGlobalHook.ts
|
|
106
|
-
function
|
|
309
|
+
function createAutoTranslateGlobalHook2(options) {
|
|
107
310
|
const {
|
|
108
311
|
globalSlug,
|
|
109
312
|
defaultLocale: defaultLocaleOption,
|
|
110
313
|
targetLocales: targetLocalesOption,
|
|
111
|
-
workflowSlug =
|
|
314
|
+
workflowSlug = TRANSLATE_WORKFLOW_SLUG2
|
|
112
315
|
} = options;
|
|
113
316
|
return async ({ doc, req }) => {
|
|
114
317
|
if (shouldSkipAutoTranslate2(req.context))
|
|
@@ -120,6 +323,8 @@ function createAutoTranslateGlobalHook(options) {
|
|
|
120
323
|
return doc;
|
|
121
324
|
if (!req.user)
|
|
122
325
|
return doc;
|
|
326
|
+
if (typed._status && typed._status !== "published")
|
|
327
|
+
return doc;
|
|
123
328
|
const targetLocales = targetLocalesOption ?? readTargetLocales2(req, defaultLocale);
|
|
124
329
|
if (targetLocales.length === 0)
|
|
125
330
|
return doc;
|
|
@@ -136,6 +341,7 @@ function createAutoTranslateGlobalHook(options) {
|
|
|
136
341
|
return doc;
|
|
137
342
|
try {
|
|
138
343
|
const job = await req.payload.jobs.queue({
|
|
344
|
+
req,
|
|
139
345
|
workflow: workflowSlug,
|
|
140
346
|
input: {
|
|
141
347
|
updatedAt,
|
|
@@ -197,496 +403,37 @@ function readFirstResolverKey2(req) {
|
|
|
197
403
|
return typeof first?.key === "string" ? first.key : "";
|
|
198
404
|
}
|
|
199
405
|
|
|
200
|
-
// src/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const { collectionSlug, globalSlug, id, locale, overrideAccess, req } = args;
|
|
209
|
-
if (!collectionSlug && !globalSlug)
|
|
210
|
-
throw new APIError("Bad Request", 400);
|
|
211
|
-
const { payload } = req;
|
|
212
|
-
const { config } = payload;
|
|
213
|
-
const isGlobal = !!globalSlug;
|
|
214
|
-
if (!isGlobal && !id)
|
|
215
|
-
throw new APIError("Bad Request", 400);
|
|
216
|
-
const entityConfig = isGlobal ? findConfigBySlug(globalSlug, config.globals) : findConfigBySlug(collectionSlug, config.collections);
|
|
217
|
-
if (!entityConfig)
|
|
218
|
-
throw new APIError("Bad Request", 400);
|
|
219
|
-
const docPromise = isGlobal ? payload.findGlobal({
|
|
220
|
-
depth: 0,
|
|
221
|
-
fallbackLocale: undefined,
|
|
222
|
-
locale,
|
|
223
|
-
overrideAccess,
|
|
224
|
-
req,
|
|
225
|
-
slug: args.globalSlug
|
|
226
|
-
}) : payload.findByID({
|
|
227
|
-
collection: collectionSlug,
|
|
406
|
+
// src/review/recordTranslationStatus.ts
|
|
407
|
+
var upsertTranslationStatus = async (req, entity, locale, data) => {
|
|
408
|
+
if (!req.payload.collections[TRANSLATION_STATUS_SLUG2])
|
|
409
|
+
return;
|
|
410
|
+
const existing = await req.payload.find({
|
|
411
|
+
collection: TRANSLATION_STATUS_SLUG2,
|
|
412
|
+
where: { and: [{ entity: { equals: entity } }, { locale: { equals: locale } }] },
|
|
413
|
+
limit: 1,
|
|
228
414
|
depth: 0,
|
|
229
|
-
|
|
230
|
-
id,
|
|
231
|
-
locale,
|
|
232
|
-
overrideAccess,
|
|
233
|
-
req
|
|
234
|
-
});
|
|
235
|
-
const doc = await docPromise;
|
|
236
|
-
return {
|
|
237
|
-
config: entityConfig,
|
|
238
|
-
doc
|
|
239
|
-
};
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
// src/translate/traverseFields.ts
|
|
243
|
-
import ObjectID from "bson-objectid";
|
|
244
|
-
import { tabHasName } from "payload/shared";
|
|
245
|
-
|
|
246
|
-
// src/utils/isEmpty.ts
|
|
247
|
-
var isEmpty = (value) => {
|
|
248
|
-
if (Array.isArray(value))
|
|
249
|
-
return value.length === 0;
|
|
250
|
-
if (value === null || typeof value === "undefined")
|
|
251
|
-
return true;
|
|
252
|
-
if (typeof value === "object" && Object.keys(value).length === 0)
|
|
253
|
-
return true;
|
|
254
|
-
return false;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// src/utils/sanitizeSlug.ts
|
|
258
|
-
var sanitizeSlug = (text) => text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
259
|
-
|
|
260
|
-
// src/translate/traverseRichText.ts
|
|
261
|
-
var traverseRichText = ({
|
|
262
|
-
onText,
|
|
263
|
-
root,
|
|
264
|
-
siblingData,
|
|
265
|
-
additionalTraverseRichText
|
|
266
|
-
}) => {
|
|
267
|
-
siblingData = siblingData ?? root;
|
|
268
|
-
if (siblingData.text) {
|
|
269
|
-
onText(siblingData);
|
|
270
|
-
}
|
|
271
|
-
if (Array.isArray(siblingData?.children)) {
|
|
272
|
-
for (const child of siblingData.children) {
|
|
273
|
-
traverseRichText({
|
|
274
|
-
onText,
|
|
275
|
-
root,
|
|
276
|
-
siblingData: child,
|
|
277
|
-
additionalTraverseRichText
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
} else {
|
|
281
|
-
additionalTraverseRichText?.({ onText, root, siblingData });
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// src/translate/traverseFields.ts
|
|
286
|
-
var joinPath = (base, segment) => base ? `${base}.${segment}` : segment;
|
|
287
|
-
var traverseFields = ({
|
|
288
|
-
dataFrom,
|
|
289
|
-
emptyOnly,
|
|
290
|
-
fields,
|
|
291
|
-
localizedParent,
|
|
292
|
-
path,
|
|
293
|
-
siblingDataFrom,
|
|
294
|
-
siblingDataTranslated,
|
|
295
|
-
translatedData,
|
|
296
|
-
valuesToTranslate,
|
|
297
|
-
_options
|
|
298
|
-
}) => {
|
|
299
|
-
const { additionalTraverseRichText } = _options ?? {};
|
|
300
|
-
siblingDataFrom = siblingDataFrom ?? dataFrom;
|
|
301
|
-
siblingDataTranslated = siblingDataTranslated ?? translatedData;
|
|
302
|
-
for (const field of fields) {
|
|
303
|
-
switch (field.type) {
|
|
304
|
-
case "tabs":
|
|
305
|
-
for (const tab of field.tabs) {
|
|
306
|
-
const hasName = tabHasName(tab);
|
|
307
|
-
const tabDataFrom = hasName ? siblingDataFrom[tab.name] : siblingDataFrom;
|
|
308
|
-
if (!tabDataFrom)
|
|
309
|
-
return;
|
|
310
|
-
let tabDataTranslated;
|
|
311
|
-
if (hasName) {
|
|
312
|
-
if (!siblingDataTranslated[tab.name])
|
|
313
|
-
siblingDataTranslated[tab.name] = {};
|
|
314
|
-
tabDataTranslated = siblingDataTranslated[tab.name];
|
|
315
|
-
} else {
|
|
316
|
-
tabDataTranslated = siblingDataTranslated;
|
|
317
|
-
}
|
|
318
|
-
traverseFields({
|
|
319
|
-
dataFrom,
|
|
320
|
-
emptyOnly,
|
|
321
|
-
fields: tab.fields,
|
|
322
|
-
localizedParent: localizedParent ?? tab.localized,
|
|
323
|
-
path: hasName ? joinPath(path, tab.name) : path,
|
|
324
|
-
siblingDataFrom: tabDataFrom,
|
|
325
|
-
siblingDataTranslated: tabDataTranslated,
|
|
326
|
-
translatedData,
|
|
327
|
-
valuesToTranslate,
|
|
328
|
-
_options
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
break;
|
|
332
|
-
case "group": {
|
|
333
|
-
if (!("name" in field))
|
|
334
|
-
break;
|
|
335
|
-
const groupDataFrom = siblingDataFrom[field.name];
|
|
336
|
-
if (!groupDataFrom)
|
|
337
|
-
break;
|
|
338
|
-
if (!siblingDataTranslated[field.name])
|
|
339
|
-
siblingDataTranslated[field.name] = {};
|
|
340
|
-
const groupDataTranslated = siblingDataTranslated[field.name];
|
|
341
|
-
traverseFields({
|
|
342
|
-
dataFrom,
|
|
343
|
-
emptyOnly,
|
|
344
|
-
fields: field.fields,
|
|
345
|
-
localizedParent: localizedParent ?? field.localized,
|
|
346
|
-
path: joinPath(path, field.name),
|
|
347
|
-
siblingDataFrom: groupDataFrom,
|
|
348
|
-
siblingDataTranslated: groupDataTranslated,
|
|
349
|
-
translatedData,
|
|
350
|
-
valuesToTranslate,
|
|
351
|
-
_options
|
|
352
|
-
});
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
case "array": {
|
|
356
|
-
const arrayDataFrom = siblingDataFrom[field.name];
|
|
357
|
-
if (isEmpty(arrayDataFrom))
|
|
358
|
-
break;
|
|
359
|
-
if (!siblingDataTranslated[field.name])
|
|
360
|
-
siblingDataTranslated[field.name] = [];
|
|
361
|
-
let arrayDataTranslated = siblingDataTranslated[field.name];
|
|
362
|
-
if (field.localized || localizedParent) {
|
|
363
|
-
if (arrayDataTranslated.length > 0 && emptyOnly)
|
|
364
|
-
break;
|
|
365
|
-
arrayDataTranslated = arrayDataFrom.map(() => ({
|
|
366
|
-
id: ObjectID().toHexString()
|
|
367
|
-
}));
|
|
368
|
-
}
|
|
369
|
-
arrayDataTranslated.forEach((item, index) => {
|
|
370
|
-
traverseFields({
|
|
371
|
-
dataFrom,
|
|
372
|
-
emptyOnly,
|
|
373
|
-
fields: field.fields,
|
|
374
|
-
localizedParent: localizedParent ?? field.localized,
|
|
375
|
-
path: `${joinPath(path, field.name)}[${index}]`,
|
|
376
|
-
siblingDataFrom: arrayDataFrom[index],
|
|
377
|
-
siblingDataTranslated: item,
|
|
378
|
-
translatedData,
|
|
379
|
-
valuesToTranslate,
|
|
380
|
-
_options
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
siblingDataTranslated[field.name] = arrayDataTranslated;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
case "blocks": {
|
|
387
|
-
const blocksDataFrom = siblingDataFrom[field.name];
|
|
388
|
-
if (isEmpty(blocksDataFrom))
|
|
389
|
-
break;
|
|
390
|
-
if (!siblingDataTranslated[field.name])
|
|
391
|
-
siblingDataTranslated[field.name] = [];
|
|
392
|
-
let blocksDataTranslated = siblingDataTranslated[field.name];
|
|
393
|
-
if (field.localized || localizedParent) {
|
|
394
|
-
if (blocksDataTranslated.length > 0 && emptyOnly)
|
|
395
|
-
break;
|
|
396
|
-
blocksDataTranslated = blocksDataFrom.map(({ blockType }) => ({
|
|
397
|
-
blockType,
|
|
398
|
-
id: ObjectID().toHexString()
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
|
-
blocksDataTranslated.forEach((item, index) => {
|
|
402
|
-
const block = field.blocks.find((each) => each.slug === item.blockType);
|
|
403
|
-
if (!block)
|
|
404
|
-
return;
|
|
405
|
-
traverseFields({
|
|
406
|
-
dataFrom,
|
|
407
|
-
emptyOnly,
|
|
408
|
-
fields: block.fields,
|
|
409
|
-
localizedParent: localizedParent ?? field.localized,
|
|
410
|
-
path: `${joinPath(path, field.name)}[${index}](${item.blockType})`,
|
|
411
|
-
siblingDataFrom: blocksDataFrom[index],
|
|
412
|
-
siblingDataTranslated: item,
|
|
413
|
-
translatedData,
|
|
414
|
-
valuesToTranslate,
|
|
415
|
-
_options
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
siblingDataTranslated[field.name] = blocksDataTranslated;
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
case "collapsible":
|
|
422
|
-
case "row":
|
|
423
|
-
traverseFields({
|
|
424
|
-
dataFrom,
|
|
425
|
-
emptyOnly,
|
|
426
|
-
fields: field.fields,
|
|
427
|
-
localizedParent,
|
|
428
|
-
path,
|
|
429
|
-
siblingDataFrom,
|
|
430
|
-
siblingDataTranslated,
|
|
431
|
-
translatedData,
|
|
432
|
-
valuesToTranslate,
|
|
433
|
-
_options
|
|
434
|
-
});
|
|
435
|
-
break;
|
|
436
|
-
case "date":
|
|
437
|
-
case "checkbox":
|
|
438
|
-
case "code":
|
|
439
|
-
case "email":
|
|
440
|
-
case "number":
|
|
441
|
-
case "point":
|
|
442
|
-
case "radio":
|
|
443
|
-
case "relationship":
|
|
444
|
-
case "select":
|
|
445
|
-
case "upload":
|
|
446
|
-
siblingDataTranslated[field.name] = siblingDataFrom[field.name];
|
|
447
|
-
break;
|
|
448
|
-
case "json":
|
|
449
|
-
let traverseObject = function(obj, objPath) {
|
|
450
|
-
if (!obj || typeof obj !== "object")
|
|
451
|
-
return;
|
|
452
|
-
for (const key in obj) {
|
|
453
|
-
const value = obj[key];
|
|
454
|
-
if (typeof value === "string" && value.trim()) {
|
|
455
|
-
((parentObj, parentKey, parentValue, parentPath) => {
|
|
456
|
-
valuesToTranslate.push({
|
|
457
|
-
onTranslate: (translated) => {
|
|
458
|
-
parentObj[parentKey] = translated;
|
|
459
|
-
},
|
|
460
|
-
value: parentValue,
|
|
461
|
-
path: parentPath
|
|
462
|
-
});
|
|
463
|
-
})(obj, key, value, joinPath(objPath, key));
|
|
464
|
-
} else if (typeof value === "object" && value !== null) {
|
|
465
|
-
traverseObject(value, joinPath(objPath, key));
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
if (!(field.localized || localizedParent))
|
|
470
|
-
break;
|
|
471
|
-
if (isEmpty(siblingDataFrom[field.name]))
|
|
472
|
-
break;
|
|
473
|
-
if (emptyOnly && siblingDataTranslated[field.name])
|
|
474
|
-
break;
|
|
475
|
-
const jsonDataFrom = siblingDataFrom[field.name];
|
|
476
|
-
const jsonDataTranslated = JSON.parse(JSON.stringify(jsonDataFrom));
|
|
477
|
-
siblingDataTranslated[field.name] = jsonDataTranslated;
|
|
478
|
-
traverseObject(jsonDataTranslated, joinPath(path, field.name));
|
|
479
|
-
break;
|
|
480
|
-
case "text":
|
|
481
|
-
case "textarea":
|
|
482
|
-
if (field.custom && typeof field.custom === "object" && field.custom.translatorSkip)
|
|
483
|
-
break;
|
|
484
|
-
if (!(field.localized || localizedParent) || isEmpty(siblingDataFrom[field.name]))
|
|
485
|
-
break;
|
|
486
|
-
if (emptyOnly && siblingDataTranslated[field.name])
|
|
487
|
-
break;
|
|
488
|
-
if (field.name === "blockName" || field.name === "id") {
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
valuesToTranslate.push({
|
|
492
|
-
onTranslate: (translated) => {
|
|
493
|
-
siblingDataTranslated[field.name] = field.name === "slug" ? sanitizeSlug(translated) : translated;
|
|
494
|
-
},
|
|
495
|
-
value: siblingDataFrom[field.name],
|
|
496
|
-
path: joinPath(path, field.name)
|
|
497
|
-
});
|
|
498
|
-
break;
|
|
499
|
-
case "richText": {
|
|
500
|
-
if (!(field.localized || localizedParent) || isEmpty(siblingDataFrom[field.name]))
|
|
501
|
-
break;
|
|
502
|
-
if (emptyOnly && siblingDataTranslated[field.name])
|
|
503
|
-
break;
|
|
504
|
-
const richTextDataFrom = siblingDataFrom[field.name];
|
|
505
|
-
siblingDataTranslated[field.name] = richTextDataFrom;
|
|
506
|
-
if (!richTextDataFrom)
|
|
507
|
-
break;
|
|
508
|
-
const isSlate = Array.isArray(richTextDataFrom);
|
|
509
|
-
const isLexical = "root" in richTextDataFrom;
|
|
510
|
-
if (!isSlate && !isLexical)
|
|
511
|
-
break;
|
|
512
|
-
const richTextPath = joinPath(path, field.name);
|
|
513
|
-
let richTextNodeIndex = 0;
|
|
514
|
-
if (isLexical) {
|
|
515
|
-
const root = siblingDataTranslated[field.name]?.root;
|
|
516
|
-
if (root)
|
|
517
|
-
traverseRichText({
|
|
518
|
-
onText: (siblingData, attribute = "text") => {
|
|
519
|
-
valuesToTranslate.push({
|
|
520
|
-
onTranslate: (translated) => {
|
|
521
|
-
siblingData[attribute] = translated;
|
|
522
|
-
},
|
|
523
|
-
value: siblingData[attribute],
|
|
524
|
-
path: `${richTextPath}#${richTextNodeIndex++}`
|
|
525
|
-
});
|
|
526
|
-
},
|
|
527
|
-
root,
|
|
528
|
-
additionalTraverseRichText
|
|
529
|
-
});
|
|
530
|
-
} else {
|
|
531
|
-
for (const root of siblingDataTranslated[field.name]) {
|
|
532
|
-
traverseRichText({
|
|
533
|
-
onText: (siblingData, attribute = "text") => {
|
|
534
|
-
valuesToTranslate.push({
|
|
535
|
-
onTranslate: (translated) => {
|
|
536
|
-
siblingData[attribute] = translated;
|
|
537
|
-
},
|
|
538
|
-
value: siblingData[attribute],
|
|
539
|
-
path: `${richTextPath}#${richTextNodeIndex++}`
|
|
540
|
-
});
|
|
541
|
-
},
|
|
542
|
-
root,
|
|
543
|
-
additionalTraverseRichText
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
break;
|
|
548
|
-
}
|
|
549
|
-
default:
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
// src/translate/updateEntity.ts
|
|
556
|
-
import { APIError as APIError2 } from "payload";
|
|
557
|
-
var updateEntity = ({
|
|
558
|
-
collectionSlug,
|
|
559
|
-
data,
|
|
560
|
-
depth: incomingDepth,
|
|
561
|
-
globalSlug,
|
|
562
|
-
id,
|
|
563
|
-
locale,
|
|
564
|
-
overrideAccess,
|
|
565
|
-
req
|
|
566
|
-
}) => {
|
|
567
|
-
if (!collectionSlug && !globalSlug)
|
|
568
|
-
throw new APIError2("Bad Request", 400);
|
|
569
|
-
const isGlobal = !!globalSlug;
|
|
570
|
-
if (!isGlobal && !id)
|
|
571
|
-
throw new APIError2("Bad Request", 400);
|
|
572
|
-
const depth = incomingDepth ?? req.payload.config.defaultDepth;
|
|
573
|
-
const promise = isGlobal ? req.payload.updateGlobal({
|
|
574
|
-
data,
|
|
575
|
-
depth,
|
|
576
|
-
locale,
|
|
577
|
-
overrideAccess,
|
|
578
|
-
req,
|
|
579
|
-
slug: globalSlug
|
|
580
|
-
}) : req.payload.update({
|
|
581
|
-
collection: collectionSlug,
|
|
582
|
-
data,
|
|
583
|
-
depth,
|
|
584
|
-
id,
|
|
585
|
-
locale,
|
|
586
|
-
overrideAccess,
|
|
415
|
+
overrideAccess: true,
|
|
587
416
|
req
|
|
588
417
|
});
|
|
589
|
-
|
|
418
|
+
const row = existing.docs[0];
|
|
419
|
+
if (row)
|
|
420
|
+
await req.payload.update({ collection: TRANSLATION_STATUS_SLUG2, id: row.id, data, overrideAccess: true, req });
|
|
421
|
+
else
|
|
422
|
+
await req.payload.create({ collection: TRANSLATION_STATUS_SLUG2, data: { entity, locale, ...data }, overrideAccess: true, req });
|
|
590
423
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const flat = (typeof value === "string" ? value : String(value ?? "")).replace(/\s+/g, " ").trim();
|
|
595
|
-
return JSON.stringify(flat.length > 60 ? `${flat.slice(0, 57)}…` : flat);
|
|
596
|
-
};
|
|
597
|
-
var translateOperation = async (args) => {
|
|
598
|
-
const req = "req" in args ? args.req : {
|
|
599
|
-
payload: args.payload
|
|
600
|
-
};
|
|
601
|
-
const { collectionSlug, globalSlug, id, locale, localeFrom, overrideAccess } = args;
|
|
602
|
-
const { config, doc: dataFrom } = await findEntityWithConfig({
|
|
603
|
-
collectionSlug,
|
|
604
|
-
globalSlug,
|
|
605
|
-
id,
|
|
606
|
-
locale: localeFrom,
|
|
607
|
-
req
|
|
608
|
-
});
|
|
609
|
-
const resolver = (req.payload.config.custom?.translator?.resolvers ?? []).find((each) => each.key === args.resolver);
|
|
610
|
-
if (!resolver)
|
|
611
|
-
throw new APIError3(`Resolver with the key ${args.resolver} was not found`);
|
|
612
|
-
const valuesToTranslate = [];
|
|
613
|
-
let translatedData = args.data;
|
|
614
|
-
if (!translatedData) {
|
|
615
|
-
const { doc } = await findEntityWithConfig({
|
|
616
|
-
collectionSlug,
|
|
617
|
-
globalSlug,
|
|
618
|
-
id,
|
|
619
|
-
locale,
|
|
620
|
-
overrideAccess,
|
|
621
|
-
req
|
|
622
|
-
});
|
|
623
|
-
translatedData = doc;
|
|
624
|
-
}
|
|
625
|
-
traverseFields({
|
|
424
|
+
var recordTranslationStatus = async ({ req, collectionSlug, globalSlug, id, locale, config, dataFrom, reviewedBy }) => {
|
|
425
|
+
const hash = sourceHash(collectTranslatableFields({
|
|
426
|
+
config,
|
|
626
427
|
dataFrom,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
});
|
|
633
|
-
const entityLabel = `${collectionSlug || globalSlug}#${id ?? ""}`;
|
|
634
|
-
const direction = `${args.localeFrom}→${args.locale}`;
|
|
635
|
-
req.payload.logger.info({
|
|
636
|
-
msg: `[translate] ${entityLabel} ${direction}: traversed ${valuesToTranslate.length} translatable value(s)`
|
|
637
|
-
});
|
|
638
|
-
const resolveResult = await resolver.resolve({
|
|
639
|
-
localeFrom: args.localeFrom,
|
|
640
|
-
localeTo: args.locale,
|
|
641
|
-
req,
|
|
642
|
-
texts: valuesToTranslate.map((each) => each.value)
|
|
643
|
-
});
|
|
644
|
-
let result;
|
|
645
|
-
if (!resolveResult.success) {
|
|
646
|
-
req.payload.logger.warn({
|
|
647
|
-
msg: `[translate] ${entityLabel} ${direction}: resolver failed — ${valuesToTranslate.length} value(s) traversed but NOT translated` + (valuesToTranslate.length ? `
|
|
648
|
-
${valuesToTranslate.map((v) => ` ${v.path ?? "(unknown)"}: ${preview(v.value)}`).join(`
|
|
649
|
-
`)}` : "")
|
|
650
|
-
});
|
|
651
|
-
result = {
|
|
652
|
-
success: false
|
|
653
|
-
};
|
|
654
|
-
} else {
|
|
655
|
-
const summary = [];
|
|
656
|
-
resolveResult.translatedTexts.forEach((translated, index) => {
|
|
657
|
-
const formattedValue = he.decode(translated);
|
|
658
|
-
const entry = valuesToTranslate[index];
|
|
659
|
-
summary.push(` ${entry.path ?? "(unknown)"}: ${preview(entry.value)} → ${preview(formattedValue)}`);
|
|
660
|
-
entry.onTranslate(formattedValue);
|
|
661
|
-
});
|
|
662
|
-
req.payload.logger.info({
|
|
663
|
-
msg: `[translate] ${entityLabel} ${direction}: translated ${resolveResult.translatedTexts.length} value(s)` + (summary.length ? `
|
|
664
|
-
${summary.join(`
|
|
665
|
-
`)}` : "")
|
|
666
|
-
});
|
|
667
|
-
if (args.update) {
|
|
668
|
-
await updateEntity({
|
|
669
|
-
collectionSlug,
|
|
670
|
-
data: translatedData,
|
|
671
|
-
depth: 0,
|
|
672
|
-
globalSlug,
|
|
673
|
-
id,
|
|
674
|
-
locale,
|
|
675
|
-
overrideAccess,
|
|
676
|
-
req
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
result = {
|
|
680
|
-
success: true,
|
|
681
|
-
translatedData,
|
|
682
|
-
dataFrom
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
return result;
|
|
428
|
+
dataTarget: {},
|
|
429
|
+
options: req.payload.config.custom?.translator?._options
|
|
430
|
+
}));
|
|
431
|
+
const now = new Date().toISOString();
|
|
432
|
+
await upsertTranslationStatus(req, entityKey({ collectionSlug, globalSlug, id }), locale, reviewedBy ? { reviewedHash: hash, reviewedAt: now, reviewedBy } : { sourceHash: hash, translatedAt: now });
|
|
686
433
|
};
|
|
687
434
|
|
|
688
435
|
// src/jobs/createTranslateTask.ts
|
|
689
|
-
function
|
|
436
|
+
function createTranslateTask2(options = {}) {
|
|
690
437
|
const { slug = "translateEntityToLocale" } = options;
|
|
691
438
|
return {
|
|
692
439
|
slug,
|
|
@@ -696,13 +443,15 @@ function createTranslateTask(options = {}) {
|
|
|
696
443
|
{ name: "global", type: "text", required: false },
|
|
697
444
|
{ name: "fromLocale", type: "text", required: true },
|
|
698
445
|
{ name: "toLocale", type: "text", required: true },
|
|
699
|
-
{ name: "resolver", type: "text", required: false }
|
|
446
|
+
{ name: "resolver", type: "text", required: false },
|
|
447
|
+
{ name: "mode", type: "text", required: false }
|
|
700
448
|
],
|
|
701
449
|
outputSchema: [],
|
|
702
450
|
retries: 3,
|
|
703
451
|
handler: async (args) => {
|
|
704
452
|
const { input, job, req } = args;
|
|
705
453
|
const { id, collection, global, fromLocale, toLocale, resolver: inputResolver } = input;
|
|
454
|
+
const mode = input.mode ?? readAutoTranslateMode(req);
|
|
706
455
|
if (!collection && !global) {
|
|
707
456
|
throw new Error("translateTask: either `collection` or `global` must be provided");
|
|
708
457
|
}
|
|
@@ -717,11 +466,11 @@ function createTranslateTask(options = {}) {
|
|
|
717
466
|
});
|
|
718
467
|
let result;
|
|
719
468
|
try {
|
|
720
|
-
result = await
|
|
469
|
+
result = await translateOperation2({
|
|
721
470
|
req,
|
|
722
471
|
collectionSlug: collection,
|
|
723
472
|
globalSlug: global,
|
|
724
|
-
emptyOnly:
|
|
473
|
+
emptyOnly: mode !== "all",
|
|
725
474
|
id,
|
|
726
475
|
locale: toLocale,
|
|
727
476
|
localeFrom: fromLocale,
|
|
@@ -743,31 +492,47 @@ function createTranslateTask(options = {}) {
|
|
|
743
492
|
});
|
|
744
493
|
throw new Error(`translateTask: resolver returned success=false for ${entityLabel} → ${toLocale}`);
|
|
745
494
|
}
|
|
495
|
+
const latest = await findEntityWithConfig({
|
|
496
|
+
collectionSlug: collection,
|
|
497
|
+
globalSlug: global,
|
|
498
|
+
id,
|
|
499
|
+
locale: fromLocale,
|
|
500
|
+
overrideAccess: true,
|
|
501
|
+
req
|
|
502
|
+
});
|
|
503
|
+
if (String(latest.doc?.updatedAt) !== String(result.dataFrom?.updatedAt)) {
|
|
504
|
+
req.payload.logger.warn({
|
|
505
|
+
jobId: job.id,
|
|
506
|
+
msg: `[translate] ${entityLabel}#${id ?? global} changed during translation — skipping ${toLocale}, the newer save re-queues it`
|
|
507
|
+
});
|
|
508
|
+
return { output: { success: true } };
|
|
509
|
+
}
|
|
746
510
|
const translated = result.translatedData ?? {};
|
|
747
|
-
const { _locale: _dropLocale, _parent_id: _dropParent, ...data } = translated;
|
|
511
|
+
const { _locale: _dropLocale, _parent_id: _dropParent, updatedAt: _dropUpdatedAt, createdAt: _dropCreatedAt, ...data } = translated;
|
|
748
512
|
req.payload.logger.info({
|
|
749
513
|
jobId: job.id,
|
|
750
514
|
msg: `[translate] persisting ${entityLabel}#${id ?? global} → ${toLocale}`
|
|
751
515
|
});
|
|
752
516
|
try {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
517
|
+
await updateEntity({
|
|
518
|
+
collectionSlug: collection,
|
|
519
|
+
data,
|
|
520
|
+
depth: 0,
|
|
521
|
+
globalSlug: global,
|
|
522
|
+
id,
|
|
523
|
+
locale: toLocale,
|
|
524
|
+
overrideAccess: true,
|
|
525
|
+
req
|
|
526
|
+
});
|
|
527
|
+
await recordTranslationStatus({
|
|
528
|
+
req,
|
|
529
|
+
collectionSlug: collection,
|
|
530
|
+
globalSlug: global,
|
|
531
|
+
id,
|
|
532
|
+
locale: toLocale,
|
|
533
|
+
config: latest.config,
|
|
534
|
+
dataFrom: latest.doc
|
|
535
|
+
});
|
|
771
536
|
} catch (error) {
|
|
772
537
|
req.payload.logger.error({
|
|
773
538
|
jobId: job.id,
|
|
@@ -792,9 +557,12 @@ function readFirstResolverKey3(req) {
|
|
|
792
557
|
const first = custom[0];
|
|
793
558
|
return typeof first?.key === "string" ? first.key : "";
|
|
794
559
|
}
|
|
560
|
+
function readAutoTranslateMode(req) {
|
|
561
|
+
return req.payload.config.custom?.translator?.autoTranslateMode === "all" ? "all" : "missing";
|
|
562
|
+
}
|
|
795
563
|
|
|
796
564
|
// src/jobs/createTranslateWorkflow.ts
|
|
797
|
-
function
|
|
565
|
+
function createTranslateWorkflow2(options = {}) {
|
|
798
566
|
const { slug = "translateEntityToLocales", taskSlug = "translateEntityToLocale" } = options;
|
|
799
567
|
return {
|
|
800
568
|
slug,
|
|
@@ -852,4 +620,4 @@ function createTranslateWorkflow(options = {}) {
|
|
|
852
620
|
};
|
|
853
621
|
}
|
|
854
622
|
|
|
855
|
-
export {
|
|
623
|
+
export { findEntityWithConfig, translateOperation2, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, createAutoTranslateCollectionHook2, createAutoTranslateGlobalHook2, recordTranslationStatus, createTranslateTask2, createTranslateWorkflow2 };
|