@justanarthur/payload-plugin-translator 3.2.0 → 3.3.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 +8 -0
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -3
- package/dist/jobs.js +2 -2
- package/dist/rsc.js +30 -6
- package/dist/shared/{chunk-d0n3f8ad.js → chunk-3rz1t943.js} +64 -21
- package/dist/shared/{chunk-4ftfmxgs.js → chunk-gd0rak8x.js} +21 -8
- package/dist/shared/{chunk-hkx0bzb6.js → chunk-z55qxmj2.js} +7 -1
- package/dist/types.d.ts +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ export const myResolver: TranslateResolver = {
|
|
|
80
80
|
| `resolvers` | `TranslateResolver[]` | yes | Tried in order; first to succeed wins per chunk. |
|
|
81
81
|
| `autoTranslate` | `boolean` | no | Default `true`. Set `false` to opt out of the auto-translate afterChange hook — useful when you drive translation manually via the `translateOperation` export. |
|
|
82
82
|
| `autoTranslateMode` | `'missing' \| 'untranslated' \| 'all'` | no | Default `'missing'`: a publish in the default locale fills empty fields in other locales and keeps existing translations and slugs. `'untranslated'` also replaces fields that still hold the source copy. `'all'` re-translates everything. |
|
|
83
|
+
| `languageDetection` | `boolean` | no | Default `true`. The review view flags translated fields written in another locale's language, and `untranslated` mode re-translates them. |
|
|
83
84
|
| `review` | `boolean` | no | Default `true`. Adds the `/admin/translations` review view, its endpoints and the hidden `translation-status` collection (needs a migration on postgres). |
|
|
84
85
|
| `disabled` | `boolean` | no | Skip the plugin entirely (no overrides, no jobs). Useful in tests. |
|
|
85
86
|
| `_options.additionalTraverseRichText` | function | no | Hook to extend rich-text traversal — see below. |
|
|
@@ -128,6 +129,13 @@ With `review` on, **Translations** appears in the admin nav (`/admin/translation
|
|
|
128
129
|
last auto-translate job and its error. Actions: *Translate missing fields*, *Re-translate all*,
|
|
129
130
|
*Mark reviewed*.
|
|
130
131
|
|
|
132
|
+
- **Wrong language**: translated fields are checked with [eld](https://github.com/nitotm/efficient-language-detector-js)
|
|
133
|
+
(small n-gram set, loaded lazily on the server, limited to the configured locales). A field is
|
|
134
|
+
flagged, shown as `⚑` in the overview and `xx detected` in the detail, only when it has at least 40
|
|
135
|
+
characters and 5 words after dropping emails, urls, numbers and slashed loanwords, is not a
|
|
136
|
+
list of short lines, and the detected language leads the expected one by a margin (wider for close
|
|
137
|
+
pairs such as cs/sk and es/pt, and for text under 80 characters). Flagged fields count against
|
|
138
|
+
coverage, so bulk and per-document fixes re-translate them.
|
|
131
139
|
- **Bulk fix**: *Translate untranslated* queues one translate workflow per incomplete document,
|
|
132
140
|
for all locales or one, in `untranslated` mode (empty fields and fields still holding the source
|
|
133
141
|
copy; existing translations and slugs stay). Locales a pending job already covers are skipped.
|
package/dist/client.js
CHANGED
|
@@ -338,7 +338,7 @@ var BulkActions = ({ tab, locales, progress }) => {
|
|
|
338
338
|
};
|
|
339
339
|
const queue = async () => {
|
|
340
340
|
const scope = `${locale ? locale.toUpperCase() : "every locale"} of all ${tab}`;
|
|
341
|
-
if (!window.confirm(`Queue translation of empty and
|
|
341
|
+
if (!window.confirm(`Queue translation of empty, untranslated and wrong-language fields in ${scope}? Correct translations are kept.`))
|
|
342
342
|
return;
|
|
343
343
|
setPending("bulk");
|
|
344
344
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ type TranslatorConfig = {
|
|
|
68
68
|
autoTranslateMode?: TranslateMode;
|
|
69
69
|
/** adds the /admin/translations review view, its endpoints and the status collection (default true) */
|
|
70
70
|
review?: boolean;
|
|
71
|
+
/** flag translated fields written in another locale's language (default true) */
|
|
72
|
+
languageDetection?: boolean;
|
|
71
73
|
_options?: {
|
|
72
74
|
additionalTraverseRichText?: (args: {
|
|
73
75
|
onText: (siblingData: Record<string, unknown>, attribute?: string) => void;
|
package/dist/index.js
CHANGED
|
@@ -8,15 +8,15 @@ import {
|
|
|
8
8
|
isTranslateMode,
|
|
9
9
|
createTranslateTask2,
|
|
10
10
|
createTranslateWorkflow2
|
|
11
|
-
} from "./shared/chunk-
|
|
12
|
-
import"./shared/chunk-
|
|
11
|
+
} from "./shared/chunk-z55qxmj2.js";
|
|
12
|
+
import"./shared/chunk-3rz1t943.js";
|
|
13
13
|
import {
|
|
14
14
|
readLocales,
|
|
15
15
|
parseEntityKey,
|
|
16
16
|
hasTranslateWorkflow,
|
|
17
17
|
queueBulkTranslation,
|
|
18
18
|
runQueuedTranslations
|
|
19
|
-
} from "./shared/chunk-
|
|
19
|
+
} from "./shared/chunk-gd0rak8x.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-
|
|
6
|
+
} from "./shared/chunk-z55qxmj2.js";
|
|
7
7
|
import {
|
|
8
8
|
TRANSLATE_TASK_SLUG2,
|
|
9
9
|
TRANSLATE_WORKFLOW_SLUG2
|
|
10
|
-
} from "./shared/chunk-
|
|
10
|
+
} from "./shared/chunk-3rz1t943.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-
|
|
1
|
+
import"./shared/chunk-3rz1t943.js";
|
|
2
2
|
import {
|
|
3
3
|
readLocales,
|
|
4
4
|
loadCollectionReview,
|
|
5
5
|
loadGlobalsReview,
|
|
6
6
|
loadEntityLocaleReview,
|
|
7
7
|
loadJobsProgress
|
|
8
|
-
} from "./shared/chunk-
|
|
8
|
+
} from "./shared/chunk-gd0rak8x.js";
|
|
9
9
|
import {
|
|
10
10
|
REVIEW_VIEW_PATH
|
|
11
11
|
} from "./shared/chunk-31qgv1zk.js";
|
|
@@ -35,7 +35,8 @@ var STATE = {
|
|
|
35
35
|
identical: { label: "same as source", tone: "warn" },
|
|
36
36
|
missing: { label: "missing", tone: "bad" },
|
|
37
37
|
ok: { label: "translated", tone: "ok" },
|
|
38
|
-
placeholders: { label: "placeholders differ", tone: "warn" }
|
|
38
|
+
placeholders: { label: "placeholders differ", tone: "warn" },
|
|
39
|
+
wrongLanguage: { label: "wrong language", tone: "bad" }
|
|
39
40
|
};
|
|
40
41
|
var preview = (text) => text.length > 600 ? `${text.slice(0, 597)}…` : text;
|
|
41
42
|
var readablePath = (path) => path.replace(/\[(\d+)\]/g, (_, index) => ` ${Number(index) + 1}`).replace(/\(([^)]+)\)/g, " · $1").replace(/#(\d+)$/, (_, index) => ` (part ${Number(index) + 1})`).split(".").join(" › ");
|
|
@@ -47,6 +48,7 @@ var Detail = ({ adminRoute, review, onlyIssues, tab, page }) => {
|
|
|
47
48
|
{ label: "translated", value: `${summary.coverage}%`, note: `${summary.ok} of ${summary.total} fields`, bar: true },
|
|
48
49
|
{ label: "missing", value: summary.missing, note: "empty in this locale" },
|
|
49
50
|
{ label: "same as source", value: summary.identical, note: `identical to ${review.defaultLocale}` },
|
|
51
|
+
{ label: "wrong language", value: summary.wrongLanguage, note: `not written in ${review.locale}` },
|
|
50
52
|
{ label: "placeholders", value: summary.placeholders, note: "differ from source" }
|
|
51
53
|
];
|
|
52
54
|
return /* @__PURE__ */ jsxs(Fragment, {
|
|
@@ -219,7 +221,7 @@ var Detail = ({ adminRoute, review, onlyIssues, tab, page }) => {
|
|
|
219
221
|
children: /* @__PURE__ */ jsx("span", {
|
|
220
222
|
className: "tr__pill",
|
|
221
223
|
"data-tone": STATE[field.state].tone,
|
|
222
|
-
children: STATE[field.state].label
|
|
224
|
+
children: field.detectedLanguage ? `${field.detectedLanguage} detected` : STATE[field.state].label
|
|
223
225
|
})
|
|
224
226
|
}),
|
|
225
227
|
/* @__PURE__ */ jsx("td", {
|
|
@@ -262,14 +264,15 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
262
264
|
return {
|
|
263
265
|
locale,
|
|
264
266
|
coverage: fields ? Math.floor(ok / fields * 100) : 100,
|
|
265
|
-
incomplete: reviews.filter(isIncomplete).length
|
|
267
|
+
incomplete: reviews.filter(isIncomplete).length,
|
|
268
|
+
wrongLanguage: reviews.filter((each) => each.summary.wrongLanguage > 0).length
|
|
266
269
|
};
|
|
267
270
|
});
|
|
268
271
|
return /* @__PURE__ */ jsxs2(Fragment2, {
|
|
269
272
|
children: [
|
|
270
273
|
/* @__PURE__ */ jsx2("div", {
|
|
271
274
|
className: "tr__stats",
|
|
272
|
-
children: stats.map(({ locale, coverage, incomplete }) => /* @__PURE__ */ jsxs2("div", {
|
|
275
|
+
children: stats.map(({ locale, coverage, incomplete, wrongLanguage }) => /* @__PURE__ */ jsxs2("div", {
|
|
273
276
|
className: "tr__stat",
|
|
274
277
|
children: [
|
|
275
278
|
/* @__PURE__ */ jsx2("span", {
|
|
@@ -293,6 +296,14 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
293
296
|
/* @__PURE__ */ jsx2("span", {
|
|
294
297
|
className: "tr__stat-note",
|
|
295
298
|
children: incomplete ? `${incomplete} incomplete` : "all complete"
|
|
299
|
+
}),
|
|
300
|
+
wrongLanguage > 0 && /* @__PURE__ */ jsxs2("span", {
|
|
301
|
+
className: "tr__stat-note tr__lang",
|
|
302
|
+
children: [
|
|
303
|
+
"⚑ ",
|
|
304
|
+
wrongLanguage,
|
|
305
|
+
" in another language"
|
|
306
|
+
]
|
|
296
307
|
})
|
|
297
308
|
]
|
|
298
309
|
}, locale))
|
|
@@ -339,6 +350,7 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
339
350
|
summary.missing && `${summary.missing} missing`,
|
|
340
351
|
summary.identical && `${summary.identical} same as ${defaultLocale}`,
|
|
341
352
|
summary.placeholders && `${summary.placeholders} broken placeholders`,
|
|
353
|
+
summary.wrongLanguage && `${summary.wrongLanguage} written in another language`,
|
|
342
354
|
summary.slugMissing && "no slug: the page 404s",
|
|
343
355
|
review.stale && "source changed since translation",
|
|
344
356
|
review.reviewed && "reviewed"
|
|
@@ -360,6 +372,13 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
360
372
|
" ",
|
|
361
373
|
[review.stale && "↻", review.reviewed && "✓"].filter(Boolean).join(" ")
|
|
362
374
|
]
|
|
375
|
+
}),
|
|
376
|
+
summary.wrongLanguage > 0 && /* @__PURE__ */ jsxs2("span", {
|
|
377
|
+
className: "tr__lang",
|
|
378
|
+
children: [
|
|
379
|
+
" ⚑",
|
|
380
|
+
summary.wrongLanguage
|
|
381
|
+
]
|
|
363
382
|
})
|
|
364
383
|
]
|
|
365
384
|
}),
|
|
@@ -417,6 +436,10 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
417
436
|
"under half, or 404 without a slug"
|
|
418
437
|
]
|
|
419
438
|
}),
|
|
439
|
+
/* @__PURE__ */ jsx2("span", {
|
|
440
|
+
className: "tr__lang",
|
|
441
|
+
children: "⚑ fields written in another language"
|
|
442
|
+
}),
|
|
420
443
|
/* @__PURE__ */ jsx2("span", {
|
|
421
444
|
children: "↻ source changed since the last translation"
|
|
422
445
|
}),
|
|
@@ -481,6 +504,7 @@ var reviewStyles = `
|
|
|
481
504
|
.tr__cell[data-tone='warn'] { --tr-tone: var(--tr-warn); color: var(--tr-warn); }
|
|
482
505
|
.tr__cell[data-tone='bad'] { --tr-tone: var(--tr-bad); color: var(--tr-bad); }
|
|
483
506
|
.tr__marks { font-size: 11px; color: var(--tr-muted); }
|
|
507
|
+
.tr__lang { color: var(--tr-bad); font-weight: 500; }
|
|
484
508
|
|
|
485
509
|
.tr__legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px; font-size: 12px; color: var(--tr-muted); }
|
|
486
510
|
.tr__legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
@@ -2,10 +2,71 @@
|
|
|
2
2
|
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
3
3
|
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
4
4
|
|
|
5
|
+
// src/utils/languageDetector.ts
|
|
6
|
+
var CLOSE_PAIRS = new Set(["cs|sk", "sk|cs", "es|pt", "pt|es", "es|it", "it|es", "nl|de", "de|nl"]);
|
|
7
|
+
var MIN_LENGTH = 40;
|
|
8
|
+
var MIN_WORDS = 5;
|
|
9
|
+
var MARGIN = 0.03;
|
|
10
|
+
var CLOSE_MARGIN = 0.06;
|
|
11
|
+
var SHORT_LENGTH = 80;
|
|
12
|
+
var SHORT_EXTRA_MARGIN = 0.03;
|
|
13
|
+
var base = (locale) => locale.split(/[-_]/)[0].toLowerCase();
|
|
14
|
+
var stripNonWords = (text) => text.replace(/\S+@\S+|https?:\/\/\S+|www\.\S+/gi, " ").replace(/\S*[/\d]\S*/g, " ").replace(/\s+/g, " ").trim();
|
|
15
|
+
var looksLikeList = (text) => {
|
|
16
|
+
const lines = text.split(`
|
|
17
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
18
|
+
if (lines.length < 3)
|
|
19
|
+
return false;
|
|
20
|
+
const lengths = lines.map((line) => line.length).sort((a, b) => a - b);
|
|
21
|
+
return lengths[Math.floor(lengths.length / 2)] < MIN_LENGTH;
|
|
22
|
+
};
|
|
23
|
+
var detector;
|
|
24
|
+
var loadWrongLanguageCheck = (locales) => {
|
|
25
|
+
detector ??= import("eld/small").then(({ eld }) => {
|
|
26
|
+
const languages = [...new Set(locales.map(base))];
|
|
27
|
+
eld.setLanguageSubset(languages);
|
|
28
|
+
return (text, expectedLocale) => {
|
|
29
|
+
const expected = base(expectedLocale);
|
|
30
|
+
if (looksLikeList(text))
|
|
31
|
+
return null;
|
|
32
|
+
const normalized = stripNonWords(text);
|
|
33
|
+
if (normalized.length < MIN_LENGTH || normalized.split(" ").length < MIN_WORDS)
|
|
34
|
+
return null;
|
|
35
|
+
const result = eld.detect(normalized.slice(0, 1000));
|
|
36
|
+
if (!result.language || result.language === expected || !result.isReliable())
|
|
37
|
+
return null;
|
|
38
|
+
const scores = result.getScores();
|
|
39
|
+
const lead = (scores[result.language] ?? 0) - (scores[expected] ?? 0);
|
|
40
|
+
const margin = (CLOSE_PAIRS.has(`${expected}|${result.language}`) ? CLOSE_MARGIN : MARGIN) + (normalized.length < SHORT_LENGTH ? SHORT_EXTRA_MARGIN : 0);
|
|
41
|
+
return lead >= margin ? result.language : null;
|
|
42
|
+
};
|
|
43
|
+
}).catch(() => null);
|
|
44
|
+
return detector;
|
|
45
|
+
};
|
|
46
|
+
|
|
5
47
|
// src/utils/placeholders.ts
|
|
6
48
|
var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
|
|
7
49
|
var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
|
|
8
50
|
|
|
51
|
+
// src/utils/plainText.ts
|
|
52
|
+
var plainText = (value) => {
|
|
53
|
+
if (typeof value === "string")
|
|
54
|
+
return value;
|
|
55
|
+
if (!value || typeof value !== "object")
|
|
56
|
+
return "";
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.map(plainText).filter(Boolean).join(" ");
|
|
59
|
+
const node = value;
|
|
60
|
+
if (typeof node.text === "string")
|
|
61
|
+
return node.text;
|
|
62
|
+
if ("root" in node)
|
|
63
|
+
return plainText(node.root);
|
|
64
|
+
if (Array.isArray(node.children))
|
|
65
|
+
return node.children.map(plainText).filter(Boolean).join(node.type === "root" ? `
|
|
66
|
+
` : "");
|
|
67
|
+
return "";
|
|
68
|
+
};
|
|
69
|
+
|
|
9
70
|
// src/translate/traverseFields.ts
|
|
10
71
|
import ObjectID from "bson-objectid";
|
|
11
72
|
import { tabHasName } from "payload/shared";
|
|
@@ -50,25 +111,6 @@ var isOpaqueText = (value) => {
|
|
|
50
111
|
return /^([a-z][a-z0-9+.-]*:|\/\/|\/|#|www\.)/i.test(text) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) || !/\p{L}/u.test(text);
|
|
51
112
|
};
|
|
52
113
|
|
|
53
|
-
// src/utils/plainText.ts
|
|
54
|
-
var plainText = (value) => {
|
|
55
|
-
if (typeof value === "string")
|
|
56
|
-
return value;
|
|
57
|
-
if (!value || typeof value !== "object")
|
|
58
|
-
return "";
|
|
59
|
-
if (Array.isArray(value))
|
|
60
|
-
return value.map(plainText).filter(Boolean).join(" ");
|
|
61
|
-
const node = value;
|
|
62
|
-
if (typeof node.text === "string")
|
|
63
|
-
return node.text;
|
|
64
|
-
if ("root" in node)
|
|
65
|
-
return plainText(node.root);
|
|
66
|
-
if (Array.isArray(node.children))
|
|
67
|
-
return node.children.map(plainText).filter(Boolean).join(node.type === "root" ? `
|
|
68
|
-
` : "");
|
|
69
|
-
return "";
|
|
70
|
-
};
|
|
71
|
-
|
|
72
114
|
// src/utils/looksUntranslated.ts
|
|
73
115
|
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
74
116
|
var looksUntranslated = (source, target) => {
|
|
@@ -126,6 +168,7 @@ var traverseFields = (args) => {
|
|
|
126
168
|
dataFrom,
|
|
127
169
|
emptyOnly,
|
|
128
170
|
retranslateIdentical,
|
|
171
|
+
isWrongLanguage,
|
|
129
172
|
fields,
|
|
130
173
|
localizedParent,
|
|
131
174
|
path,
|
|
@@ -137,7 +180,7 @@ var traverseFields = (args) => {
|
|
|
137
180
|
const { additionalTraverseRichText } = _options ?? {};
|
|
138
181
|
const siblingDataFrom = args.siblingDataFrom ?? dataFrom;
|
|
139
182
|
const siblingDataTranslated = args.siblingDataTranslated ?? translatedData;
|
|
140
|
-
const keepsTarget = (source, target) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && looksUntranslated(source, target));
|
|
183
|
+
const keepsTarget = (source, target) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && (looksUntranslated(source, target) || Boolean(isWrongLanguage?.(target))));
|
|
141
184
|
const recurse = (overrides) => traverseFields({ ...args, siblingDataFrom, siblingDataTranslated, ...overrides });
|
|
142
185
|
for (const field of fields) {
|
|
143
186
|
switch (field.type) {
|
|
@@ -388,4 +431,4 @@ var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${g
|
|
|
388
431
|
import { createHash } from "node:crypto";
|
|
389
432
|
var sourceHash = (fields) => createHash("sha1").update(JSON.stringify(fields.map(({ path, source }) => [path, source]))).digest("hex");
|
|
390
433
|
|
|
391
|
-
export { samePlaceholders, plainText, looksUntranslated, traverseFields, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, collectTranslatableFields, entityKey, sourceHash };
|
|
434
|
+
export { loadWrongLanguageCheck, samePlaceholders, plainText, looksUntranslated, traverseFields, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, collectTranslatableFields, entityKey, sourceHash };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
loadWrongLanguageCheck,
|
|
2
3
|
samePlaceholders,
|
|
3
4
|
plainText,
|
|
4
5
|
looksUntranslated,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
collectTranslatableFields,
|
|
7
8
|
entityKey,
|
|
8
9
|
sourceHash
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3rz1t943.js";
|
|
10
11
|
import {
|
|
11
12
|
TRANSLATION_STATUS_SLUG2
|
|
12
13
|
} from "./chunk-31qgv1zk.js";
|
|
@@ -22,14 +23,22 @@ var classify = (field, sourceText, targetText) => {
|
|
|
22
23
|
return "identical";
|
|
23
24
|
return "ok";
|
|
24
25
|
};
|
|
25
|
-
var computeStatus = (fields) => {
|
|
26
|
+
var computeStatus = (fields, language) => {
|
|
26
27
|
const statuses = [];
|
|
27
28
|
for (const field of fields) {
|
|
28
29
|
const sourceText = plainText(field.source);
|
|
29
30
|
if (!normalize(sourceText))
|
|
30
31
|
continue;
|
|
31
32
|
const targetText = plainText(field.target);
|
|
32
|
-
|
|
33
|
+
const state = classify(field, sourceText, targetText);
|
|
34
|
+
const detectedLanguage = state === "ok" && field.type !== "slug" && language?.check ? language.check(targetText, language.locale) ?? undefined : undefined;
|
|
35
|
+
statuses.push({
|
|
36
|
+
...field,
|
|
37
|
+
sourceText,
|
|
38
|
+
targetText,
|
|
39
|
+
state: detectedLanguage ? "wrongLanguage" : state,
|
|
40
|
+
...detectedLanguage ? { detectedLanguage } : {}
|
|
41
|
+
});
|
|
33
42
|
}
|
|
34
43
|
const count = (state) => statuses.filter((each) => each.state === state).length;
|
|
35
44
|
const ok = count("ok");
|
|
@@ -41,6 +50,7 @@ var computeStatus = (fields) => {
|
|
|
41
50
|
missing: count("missing"),
|
|
42
51
|
identical: count("identical"),
|
|
43
52
|
placeholders: count("placeholders"),
|
|
53
|
+
wrongLanguage: count("wrongLanguage"),
|
|
44
54
|
coverage: statuses.length ? Math.floor(ok / statuses.length * 100) : 100,
|
|
45
55
|
slugMissing: statuses.some((each) => each.type === "slug" && each.state === "missing")
|
|
46
56
|
}
|
|
@@ -69,13 +79,14 @@ var loadStatusRows = async (req, keys) => {
|
|
|
69
79
|
});
|
|
70
80
|
return docs;
|
|
71
81
|
};
|
|
72
|
-
var
|
|
82
|
+
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) => {
|
|
73
84
|
const { fields, summary } = computeStatus(collectTranslatableFields({
|
|
74
85
|
config,
|
|
75
86
|
dataFrom: source,
|
|
76
87
|
dataTarget: target ?? {},
|
|
77
88
|
options: pluginOptions(req)?._options
|
|
78
|
-
}));
|
|
89
|
+
}), { locale, check });
|
|
79
90
|
return {
|
|
80
91
|
fields,
|
|
81
92
|
summary,
|
|
@@ -94,6 +105,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
94
105
|
if (!config)
|
|
95
106
|
throw new Error(`unknown collection ${collectionSlug}`);
|
|
96
107
|
const { defaultLocale, targetLocales } = readLocales(req);
|
|
108
|
+
const check = await loadLanguageCheck(req);
|
|
97
109
|
const sources = await req.payload.find({
|
|
98
110
|
collection: collectionSlug,
|
|
99
111
|
locale: defaultLocale,
|
|
@@ -132,7 +144,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
132
144
|
collectionSlug,
|
|
133
145
|
id: source.id,
|
|
134
146
|
locales: Object.fromEntries(targetLocales.map((locale) => {
|
|
135
|
-
const { fields: _fields, ...review } = reviewLocale(config, source, targets[locale].get(String(source.id)), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
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);
|
|
136
148
|
return [locale, review];
|
|
137
149
|
}))
|
|
138
150
|
};
|
|
@@ -147,6 +159,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
147
159
|
};
|
|
148
160
|
var loadGlobalsReview = async ({ req, globalSlugs }) => {
|
|
149
161
|
const { defaultLocale, targetLocales } = readLocales(req);
|
|
162
|
+
const check = await loadLanguageCheck(req);
|
|
150
163
|
const rows = await loadStatusRows(req, globalSlugs.map((globalSlug) => entityKey({ globalSlug })));
|
|
151
164
|
const entities = [];
|
|
152
165
|
for (const globalSlug of globalSlugs) {
|
|
@@ -166,7 +179,7 @@ var loadGlobalsReview = async ({ req, globalSlugs }) => {
|
|
|
166
179
|
const key = entityKey({ globalSlug });
|
|
167
180
|
const locales = {};
|
|
168
181
|
for (const locale of targetLocales) {
|
|
169
|
-
const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
182
|
+
const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req, locale, check);
|
|
170
183
|
locales[locale] = review;
|
|
171
184
|
}
|
|
172
185
|
entities.push({ key, label: globalLabel(config), globalSlug, locales });
|
|
@@ -189,7 +202,7 @@ var loadEntityLocaleReview = async ({ req, entity, locale }) => {
|
|
|
189
202
|
const target = await read(locale);
|
|
190
203
|
const rows = await loadStatusRows(req, [entity]);
|
|
191
204
|
const row = rows.find((each) => each.locale === locale);
|
|
192
|
-
const review = reviewLocale(config, source, target, hashOf(config, source, req), row, req);
|
|
205
|
+
const review = reviewLocale(config, source, target, hashOf(config, source, req), row, req, locale, await loadLanguageCheck(req));
|
|
193
206
|
return {
|
|
194
207
|
...review,
|
|
195
208
|
entity,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
loadWrongLanguageCheck,
|
|
2
3
|
samePlaceholders,
|
|
4
|
+
plainText,
|
|
3
5
|
traverseFields,
|
|
4
6
|
TRANSLATE_WORKFLOW_SLUG2,
|
|
5
7
|
collectTranslatableFields,
|
|
6
8
|
entityKey,
|
|
7
9
|
sourceHash
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3rz1t943.js";
|
|
9
11
|
import {
|
|
10
12
|
TRANSLATION_STATUS_SLUG2
|
|
11
13
|
} from "./chunk-31qgv1zk.js";
|
|
@@ -96,6 +98,7 @@ var preview = (value) => {
|
|
|
96
98
|
const flat = (typeof value === "string" ? value : String(value ?? "")).replace(/\s+/g, " ").trim();
|
|
97
99
|
return JSON.stringify(flat.length > 60 ? `${flat.slice(0, 57)}…` : flat);
|
|
98
100
|
};
|
|
101
|
+
var localeCodes = (req) => (req.payload.config.localization ? req.payload.config.localization.locales : []).map((each) => typeof each === "string" ? each : each.code);
|
|
99
102
|
var translateOperation2 = async (args) => {
|
|
100
103
|
const req = "req" in args ? args.req : {
|
|
101
104
|
payload: args.payload
|
|
@@ -125,10 +128,13 @@ var translateOperation2 = async (args) => {
|
|
|
125
128
|
});
|
|
126
129
|
translatedData = doc;
|
|
127
130
|
}
|
|
131
|
+
const wrongLanguageCheck = args.retranslateIdentical && req.payload.config.custom?.translator?.languageDetection !== false ? await loadWrongLanguageCheck(localeCodes(req)) : null;
|
|
132
|
+
const isWrongLanguage = wrongLanguageCheck ? (target) => Boolean(wrongLanguageCheck(plainText(target), args.locale)) : undefined;
|
|
128
133
|
traverseFields({
|
|
129
134
|
dataFrom,
|
|
130
135
|
emptyOnly: args.emptyOnly,
|
|
131
136
|
retranslateIdentical: args.retranslateIdentical,
|
|
137
|
+
isWrongLanguage,
|
|
132
138
|
fields: config.fields,
|
|
133
139
|
translatedData,
|
|
134
140
|
valuesToTranslate,
|
package/dist/types.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ type TranslatorConfig = {
|
|
|
35
35
|
autoTranslateMode?: TranslateMode;
|
|
36
36
|
/** adds the /admin/translations review view, its endpoints and the status collection (default true) */
|
|
37
37
|
review?: boolean;
|
|
38
|
+
/** flag translated fields written in another locale's language (default true) */
|
|
39
|
+
languageDetection?: boolean;
|
|
38
40
|
_options?: {
|
|
39
41
|
additionalTraverseRichText?: (args: {
|
|
40
42
|
onText: (siblingData: Record<string, unknown>, attribute?: string) => void;
|
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.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"bson-objectid": "^2.0.4",
|
|
92
|
+
"eld": "^2.1.0",
|
|
92
93
|
"he": "^1.2.0"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|