@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.
@@ -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 TRANSLATE_TASK_SLUG = "translateEntityToLocale";
3
- var TRANSLATE_WORKFLOW_SLUG = "translateEntityToLocales";
204
+ var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
205
+ var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
4
206
 
5
207
  // src/jobs/createAutoTranslateCollectionHook.ts
6
- function createAutoTranslateCollectionHook(options) {
208
+ function createAutoTranslateCollectionHook2(options) {
7
209
  const {
8
210
  collectionSlug,
9
211
  defaultLocale: defaultLocaleOption,
10
212
  targetLocales: targetLocalesOption,
11
213
  onlyOnPublished = true,
12
- workflowSlug = TRANSLATE_WORKFLOW_SLUG
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 createAutoTranslateGlobalHook(options) {
309
+ function createAutoTranslateGlobalHook2(options) {
107
310
  const {
108
311
  globalSlug,
109
312
  defaultLocale: defaultLocaleOption,
110
313
  targetLocales: targetLocalesOption,
111
- workflowSlug = TRANSLATE_WORKFLOW_SLUG
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/translate/operation.ts
201
- import he from "he";
202
- import { APIError as APIError3 } from "payload";
203
-
204
- // src/translate/findEntityWithConfig.ts
205
- import { APIError } from "payload";
206
- var findConfigBySlug = (slug, enities) => enities.find((entity) => entity.slug === slug);
207
- var findEntityWithConfig = async (args) => {
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
- fallbackLocale: undefined,
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
- return promise;
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
- // src/translate/operation.ts
593
- var preview = (value) => {
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
- emptyOnly: args.emptyOnly,
628
- fields: config.fields,
629
- translatedData,
630
- valuesToTranslate,
631
- _options: req.payload.config.custom?.translator?._options
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 createTranslateTask(options = {}) {
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 translateOperation({
469
+ result = await translateOperation2({
721
470
  req,
722
471
  collectionSlug: collection,
723
472
  globalSlug: global,
724
- emptyOnly: false,
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
- if (collection) {
754
- await req.payload.update({
755
- collection,
756
- data,
757
- id,
758
- locale: toLocale,
759
- overrideAccess: true,
760
- req
761
- });
762
- } else {
763
- await req.payload.updateGlobal({
764
- slug: global,
765
- data,
766
- locale: toLocale,
767
- overrideAccess: true,
768
- req
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 createTranslateWorkflow(options = {}) {
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 { TRANSLATE_TASK_SLUG, TRANSLATE_WORKFLOW_SLUG, createAutoTranslateCollectionHook, createAutoTranslateGlobalHook, translateOperation, createTranslateTask, createTranslateWorkflow };
623
+ export { findEntityWithConfig, translateOperation2, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, createAutoTranslateCollectionHook2, createAutoTranslateGlobalHook2, recordTranslationStatus, createTranslateTask2, createTranslateWorkflow2 };