@justanarthur/payload-plugin-translator 3.3.4 → 3.4.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/dist/index.js CHANGED
@@ -8,15 +8,15 @@ import {
8
8
  isTranslateMode,
9
9
  createTranslateTask2,
10
10
  createTranslateWorkflow2
11
- } from "./shared/chunk-g3rrecgk.js";
12
- import"./shared/chunk-8hqdxnxy.js";
11
+ } from "./shared/chunk-k3q5eq41.js";
12
+ import"./shared/chunk-m5jdmn2w.js";
13
13
  import {
14
14
  readLocales,
15
15
  parseEntityKey,
16
16
  hasTranslateWorkflow,
17
17
  queueBulkTranslation,
18
18
  runQueuedTranslations
19
- } from "./shared/chunk-8hm2r1wg.js";
19
+ } from "./shared/chunk-9ky9w8k3.js";
20
20
  import {
21
21
  TRANSLATION_STATUS_SLUG2,
22
22
  REVIEW_VIEW_PATH
package/dist/jobs.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  createAutoTranslateGlobalHook2,
4
4
  createTranslateTask2,
5
5
  createTranslateWorkflow2
6
- } from "./shared/chunk-g3rrecgk.js";
6
+ } from "./shared/chunk-k3q5eq41.js";
7
7
  import {
8
8
  TRANSLATE_TASK_SLUG2,
9
9
  TRANSLATE_WORKFLOW_SLUG2
10
- } from "./shared/chunk-8hqdxnxy.js";
10
+ } from "./shared/chunk-m5jdmn2w.js";
11
11
 
12
12
  // src/exports/jobs.ts
13
13
  var jobs = {
package/dist/rsc.js CHANGED
@@ -1,11 +1,11 @@
1
- import"./shared/chunk-8hqdxnxy.js";
1
+ import"./shared/chunk-m5jdmn2w.js";
2
2
  import {
3
3
  readLocales,
4
4
  loadCollectionReview,
5
5
  loadGlobalsReview,
6
6
  loadEntityLocaleReview,
7
7
  loadJobsProgress
8
- } from "./shared/chunk-8hm2r1wg.js";
8
+ } from "./shared/chunk-9ky9w8k3.js";
9
9
  import {
10
10
  REVIEW_VIEW_PATH
11
11
  } from "./shared/chunk-31qgv1zk.js";
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  loadWrongLanguageCheck,
3
- samePlaceholders,
4
3
  plainText,
4
+ buildCrossLocaleTexts,
5
+ crossLocaleMatch,
6
+ isOpaqueText,
5
7
  looksUntranslated,
6
- TRANSLATE_WORKFLOW_SLUG2,
7
8
  collectTranslatableFields,
9
+ samePlaceholders,
10
+ TRANSLATE_WORKFLOW_SLUG2,
8
11
  entityKey,
9
12
  sourceHash
10
- } from "./chunk-8hqdxnxy.js";
13
+ } from "./chunk-m5jdmn2w.js";
11
14
  import {
12
15
  TRANSLATION_STATUS_SLUG2
13
16
  } from "./chunk-31qgv1zk.js";
@@ -23,15 +26,15 @@ var classify = (field, sourceText, targetText) => {
23
26
  return "identical";
24
27
  return "ok";
25
28
  };
26
- var computeStatus = (fields, language) => {
29
+ var computeStatus = (fields, options) => {
27
30
  const statuses = [];
28
31
  for (const field of fields) {
29
32
  const sourceText = plainText(field.source);
30
33
  if (!normalize(sourceText))
31
34
  continue;
32
- const targetText = plainText(field.target);
35
+ const targetText = normalize(plainText(field.target));
33
36
  const state = classify(field, sourceText, targetText);
34
- const detectedLanguage = state === "ok" && field.type !== "slug" && language?.check ? language.check(targetText, language.locale) ?? undefined : undefined;
37
+ const detectedLanguage = state === "ok" && field.type !== "slug" && !isOpaqueText(targetText) ? options?.check?.(targetText, options.locale) ?? (options?.crossLocale ? crossLocaleMatch(options.crossLocale, field.path, options.locale, targetText) : undefined) : undefined;
35
38
  statuses.push({
36
39
  ...field,
37
40
  sourceText,
@@ -80,19 +83,27 @@ var loadStatusRows = async (req, keys) => {
80
83
  return docs;
81
84
  };
82
85
  var loadLanguageCheck = (req) => pluginOptions(req)?.languageDetection === false ? Promise.resolve(null) : loadWrongLanguageCheck(readLocales(req).targetLocales.concat(readLocales(req).defaultLocale));
83
- var reviewLocale = (config, source, target, hash, row, req, locale, check) => {
84
- const { fields, summary } = computeStatus(collectTranslatableFields({
85
- config,
86
- dataFrom: source,
87
- dataTarget: target ?? {},
88
- options: pluginOptions(req)?._options
89
- }), { locale, check });
90
- return {
91
- fields,
92
- summary,
93
- stale: Boolean(row?.sourceHash) && row?.sourceHash !== hash,
94
- reviewed: Boolean(row?.reviewedHash) && row?.reviewedHash === hash
95
- };
86
+ var reviewEntity = (config, source, targetByLocale, targetLocales, hash, rowByLocale, req, check) => {
87
+ const options = pluginOptions(req)?._options;
88
+ const fieldsPerLocale = targetLocales.map((locale) => ({
89
+ locale,
90
+ fields: collectTranslatableFields({
91
+ config,
92
+ dataFrom: source,
93
+ dataTarget: targetByLocale[locale] ?? {},
94
+ options
95
+ })
96
+ }));
97
+ const crossLocale = buildCrossLocaleTexts(fieldsPerLocale);
98
+ return Object.fromEntries(fieldsPerLocale.map(({ locale, fields }) => {
99
+ const row = rowByLocale(locale);
100
+ const { fields: _fields, ...review } = {
101
+ ...computeStatus(fields, { locale, check, crossLocale }),
102
+ stale: Boolean(row?.sourceHash) && row?.sourceHash !== hash,
103
+ reviewed: Boolean(row?.reviewedHash) && row?.reviewedHash === hash
104
+ };
105
+ return [locale, review];
106
+ }));
96
107
  };
97
108
  var labelOf = (config, doc) => {
98
109
  const title = doc[config.admin?.useAsTitle ?? "id"];
@@ -143,10 +154,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
143
154
  label: labelOf(config, source),
144
155
  collectionSlug,
145
156
  id: source.id,
146
- locales: Object.fromEntries(targetLocales.map((locale) => {
147
- const { fields: _fields, ...review } = reviewLocale(config, source, targets[locale].get(String(source.id)), hash, rows.find((row) => row.entity === key && row.locale === locale), req, locale, check);
148
- return [locale, review];
149
- }))
157
+ locales: reviewEntity(config, source, Object.fromEntries(targetLocales.map((locale) => [locale, targets[locale].get(String(source.id))])), targetLocales, hash, (locale) => rows.find((row) => row.entity === key && row.locale === locale), req, check)
150
158
  };
151
159
  });
152
160
  return {
@@ -177,11 +185,11 @@ var loadGlobalsReview = async ({ req, globalSlugs }) => {
177
185
  const source = await read(defaultLocale);
178
186
  const hash = hashOf(config, source, req);
179
187
  const key = entityKey({ globalSlug });
180
- const locales = {};
188
+ const targetByLocale = {};
181
189
  for (const locale of targetLocales) {
182
- const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req, locale, check);
183
- locales[locale] = review;
190
+ targetByLocale[locale] = await read(locale);
184
191
  }
192
+ const locales = reviewEntity(config, source, targetByLocale, targetLocales, hash, (locale) => rows.find((row) => row.entity === key && row.locale === locale), req, check);
185
193
  entities.push({ key, label: globalLabel(config), globalSlug, locales });
186
194
  }
187
195
  return { entities, locales: targetLocales };
@@ -199,10 +207,14 @@ var loadEntityLocaleReview = async ({ req, entity, locale }) => {
199
207
  throw new Error(`unknown entity ${entity}`);
200
208
  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 });
201
209
  const source = await read(defaultLocale);
202
- const target = await read(locale);
203
210
  const rows = await loadStatusRows(req, [entity]);
204
211
  const row = rows.find((each) => each.locale === locale);
205
- const review = reviewLocale(config, source, target, hashOf(config, source, req), row, req, locale, await loadLanguageCheck(req));
212
+ const { targetLocales } = readLocales(req);
213
+ const targetByLocale = {};
214
+ for (const each of targetLocales) {
215
+ targetByLocale[each] = await read(each);
216
+ }
217
+ const review = reviewEntity(config, source, targetByLocale, targetLocales, hashOf(config, source, req), (each) => rows.find((row2) => row2.locale === each), req, await loadLanguageCheck(req))[locale];
206
218
  return {
207
219
  ...review,
208
220
  entity,
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  loadWrongLanguageCheck,
3
- samePlaceholders,
4
3
  plainText,
4
+ buildCrossLocaleTexts,
5
+ crossLocaleMatch,
5
6
  traverseFields,
6
- TRANSLATE_WORKFLOW_SLUG2,
7
7
  collectTranslatableFields,
8
+ samePlaceholders,
9
+ TRANSLATE_WORKFLOW_SLUG2,
8
10
  entityKey,
9
11
  sourceHash
10
- } from "./chunk-8hqdxnxy.js";
12
+ } from "./chunk-m5jdmn2w.js";
11
13
  import {
12
14
  TRANSLATION_STATUS_SLUG2
13
15
  } from "./chunk-31qgv1zk.js";
@@ -102,6 +104,35 @@ var preview = (value) => {
102
104
  return JSON.stringify(flat.length > 60 ? `${flat.slice(0, 57)}…` : flat);
103
105
  };
104
106
  var localeCodes = (req) => (req.payload.config.localization ? req.payload.config.localization.locales : []).map((each) => typeof each === "string" ? each : each.code);
107
+ var buildCrossLocaleTextsFor = async ({
108
+ config,
109
+ dataFrom,
110
+ req,
111
+ args
112
+ }) => {
113
+ const others = localeCodes(req).filter((each) => each !== args.locale && each !== args.localeFrom);
114
+ const fieldsPerLocale = [];
115
+ for (const locale of others) {
116
+ const { doc } = await findEntityWithConfig({
117
+ collectionSlug: args.collectionSlug,
118
+ globalSlug: args.globalSlug,
119
+ id: args.id,
120
+ locale,
121
+ overrideAccess: args.overrideAccess,
122
+ req
123
+ });
124
+ fieldsPerLocale.push({
125
+ locale,
126
+ fields: collectTranslatableFields({
127
+ config,
128
+ dataFrom,
129
+ dataTarget: doc,
130
+ options: req.payload.config.custom?.translator?._options
131
+ })
132
+ });
133
+ }
134
+ return buildCrossLocaleTexts(fieldsPerLocale);
135
+ };
105
136
  var translateOperation2 = async (args) => {
106
137
  const req = "req" in args ? args.req : {
107
138
  payload: args.payload
@@ -136,8 +167,10 @@ var translateOperation2 = async (args) => {
136
167
  });
137
168
  translatedData = doc;
138
169
  }
139
- const wrongLanguageCheck = args.retranslateIdentical && req.payload.config.custom?.translator?.languageDetection !== false ? await loadWrongLanguageCheck(localeCodes(req)) : null;
140
- const isWrongLanguage = wrongLanguageCheck ? (target) => Boolean(wrongLanguageCheck(plainText(target), args.locale)) : undefined;
170
+ const detectionOn = args.retranslateIdentical && req.payload.config.custom?.translator?.languageDetection !== false;
171
+ const wrongLanguageCheck = detectionOn ? await loadWrongLanguageCheck(localeCodes(req)) : null;
172
+ const crossLocale = detectionOn ? await buildCrossLocaleTextsFor({ config, dataFrom, req, args }) : undefined;
173
+ const isWrongLanguage = wrongLanguageCheck || crossLocale ? (target, path) => Boolean(wrongLanguageCheck?.(plainText(target), args.locale)) || (path !== undefined && crossLocale !== undefined ? crossLocaleMatch(crossLocale, path, args.locale, plainText(target)) !== undefined : false) : undefined;
141
174
  traverseFields({
142
175
  dataFrom,
143
176
  emptyOnly: args.emptyOnly,
@@ -20,9 +20,10 @@ var looksLikeList = (text) => {
20
20
  const lengths = lines.map((line) => line.length).sort((a, b) => a - b);
21
21
  return lengths[Math.floor(lengths.length / 2)] < MIN_LENGTH;
22
22
  };
23
- var detector;
23
+ var eldModule;
24
24
  var loadWrongLanguageCheck = (locales) => {
25
- detector ??= import("eld/small").then(({ eld }) => {
25
+ eldModule ??= import("eld/small");
26
+ return eldModule.then(({ eld }) => {
26
27
  const languages = [...new Set(locales.map(base))];
27
28
  eld.setLanguageSubset(languages);
28
29
  return (text, expectedLocale) => {
@@ -41,13 +42,8 @@ var loadWrongLanguageCheck = (locales) => {
41
42
  return lead >= margin ? result.language : null;
42
43
  };
43
44
  }).catch(() => null);
44
- return detector;
45
45
  };
46
46
 
47
- // src/utils/placeholders.ts
48
- var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
49
- var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
50
-
51
47
  // src/utils/plainText.ts
52
48
  var plainText = (value) => {
53
49
  if (typeof value === "string")
@@ -67,6 +63,34 @@ var plainText = (value) => {
67
63
  return "";
68
64
  };
69
65
 
66
+ // src/utils/crossLocale.ts
67
+ var normalize = (text) => text.replace(/\s+/g, " ").trim();
68
+ var buildCrossLocaleTexts = (fieldsPerLocale) => {
69
+ const texts = new Map;
70
+ for (const { locale, fields } of fieldsPerLocale) {
71
+ for (const field of fields) {
72
+ const value = normalize(plainText(field.target));
73
+ if (!value || value === normalize(plainText(field.source)))
74
+ continue;
75
+ const byLocale = texts.get(field.path) ?? new Map;
76
+ byLocale.set(locale, value);
77
+ texts.set(field.path, byLocale);
78
+ }
79
+ }
80
+ return texts;
81
+ };
82
+ var crossLocaleMatch = (texts, path, locale, text) => {
83
+ const value = normalize(text);
84
+ if (!value)
85
+ return;
86
+ for (const [other, otherValue] of texts.get(path) ?? []) {
87
+ if (other === locale || otherValue !== value || CLOSE_PAIRS.has(`${locale}|${other}`))
88
+ continue;
89
+ return other;
90
+ }
91
+ return;
92
+ };
93
+
70
94
  // src/translate/traverseFields.ts
71
95
  import ObjectID from "bson-objectid";
72
96
  import { tabHasName } from "payload/shared";
@@ -112,10 +136,10 @@ var isOpaqueText = (value) => {
112
136
  };
113
137
 
114
138
  // src/utils/looksUntranslated.ts
115
- var normalize = (text) => text.replace(/\s+/g, " ").trim();
139
+ var normalize2 = (text) => text.replace(/\s+/g, " ").trim();
116
140
  var looksUntranslated = (source, target) => {
117
- const sourceText = normalize(plainText(source));
118
- return sourceText.length > 3 && /\s/.test(sourceText) && !isOpaqueText(sourceText) && sourceText === normalize(plainText(target));
141
+ const sourceText = normalize2(plainText(source));
142
+ return sourceText.length > 3 && /\s/.test(sourceText) && !isOpaqueText(sourceText) && sourceText === normalize2(plainText(target));
119
143
  };
120
144
 
121
145
  // src/utils/sanitizeSlug.ts
@@ -182,7 +206,7 @@ var traverseFields = (args) => {
182
206
  const { additionalTraverseRichText } = _options ?? {};
183
207
  const siblingDataFrom = args.siblingDataFrom ?? dataFrom;
184
208
  const siblingDataTranslated = args.siblingDataTranslated ?? translatedData;
185
- const keepsTarget = (source, target) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && (looksUntranslated(source, target) || Boolean(isWrongLanguage?.(target))));
209
+ const keepsTarget = (source, target, path) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && (looksUntranslated(source, target) || Boolean(isWrongLanguage?.(target, path))));
186
210
  const recurse = (overrides) => traverseFields({ ...args, siblingDataFrom, siblingDataTranslated, ...overrides });
187
211
  for (const field of fields) {
188
212
  switch (field.type) {
@@ -307,7 +331,7 @@ var traverseFields = (args) => {
307
331
  onField?.({ path: keyPath, type: "json", source: value, target: previous });
308
332
  if (!value.trim())
309
333
  continue;
310
- if (keepsTarget(value, previous))
334
+ if (keepsTarget(value, previous, keyPath))
311
335
  continue;
312
336
  target[key] = value;
313
337
  if (isOpaqueText(value))
@@ -347,7 +371,7 @@ var traverseFields = (args) => {
347
371
  if (typeof value !== "string" || !value.trim())
348
372
  break;
349
373
  const hasCurrent = typeof current === "string" && current.trim().length > 0;
350
- if (isSlug ? hasCurrent : keepsTarget(value, current))
374
+ if (isSlug ? hasCurrent : keepsTarget(value, current, fieldPath))
351
375
  break;
352
376
  if (isOpaqueText(value)) {
353
377
  siblingDataTranslated[field.name] = value;
@@ -389,7 +413,7 @@ var traverseFields = (args) => {
389
413
  }
390
414
  break;
391
415
  }
392
- if (keepsTarget(richTextDataFrom, current))
416
+ if (keepsTarget(richTextDataFrom, current, richTextPath))
393
417
  break;
394
418
  const isSlate = Array.isArray(richTextDataFrom);
395
419
  const isLexical = typeof richTextDataFrom === "object" && "root" in richTextDataFrom;
@@ -435,6 +459,10 @@ var collectTranslatableFields = ({ config, dataFrom, dataTarget, options }) => {
435
459
  return fields;
436
460
  };
437
461
 
462
+ // src/utils/placeholders.ts
463
+ var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
464
+ var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
465
+
438
466
  // src/review/entityKey.ts
439
467
  var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${globalSlug}` : `${collectionSlug}:${id}`;
440
468
 
@@ -442,4 +470,4 @@ var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${g
442
470
  import { createHash } from "node:crypto";
443
471
  var sourceHash = (fields) => createHash("sha1").update(JSON.stringify(fields.map(({ path, source }) => [path, source]))).digest("hex");
444
472
 
445
- export { loadWrongLanguageCheck, samePlaceholders, plainText, looksUntranslated, traverseFields, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, collectTranslatableFields, entityKey, sourceHash };
473
+ export { loadWrongLanguageCheck, plainText, buildCrossLocaleTexts, crossLocaleMatch, isOpaqueText, looksUntranslated, traverseFields, collectTranslatableFields, samePlaceholders, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, entityKey, sourceHash };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justanarthur/payload-plugin-translator",
3
3
  "description": "Translator plugin for Payload CMS — automatic localization via Google, OpenAI, LibreTranslate, or custom resolvers.",
4
- "version": "3.3.4",
4
+ "version": "3.4.0",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "files": [