@justanarthur/payload-plugin-translator 3.1.1 → 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 +15 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +128 -16
- package/dist/index.d.ts +12 -1
- package/dist/index.js +42 -5
- package/dist/jobs.js +5 -4
- package/dist/rsc.js +43 -7
- package/dist/shared/{chunk-9d433kmv.js → chunk-3rz1t943.js} +79 -4
- package/dist/shared/{chunk-15n5q7wd.js → chunk-gd0rak8x.js} +130 -31
- package/dist/shared/{chunk-p03jz82h.js → chunk-z55qxmj2.js} +32 -13
- package/dist/types.d.ts +8 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -79,7 +79,8 @@ export const myResolver: TranslateResolver = {
|
|
|
79
79
|
| `globals` | `GlobalSlug[]` | yes | Globals to auto-translate. |
|
|
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
|
-
| `autoTranslateMode` | `'missing' \| 'all'` | no | Default `'missing'`: a publish in the default locale fills empty fields in other locales and keeps existing translations and slugs. `'all'` re-translates everything. |
|
|
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,19 @@ 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.
|
|
139
|
+
- **Bulk fix**: *Translate untranslated* queues one translate workflow per incomplete document,
|
|
140
|
+
for all locales or one, in `untranslated` mode (empty fields and fields still holding the source
|
|
141
|
+
copy; existing translations and slugs stay). Locales a pending job already covers are skipped.
|
|
142
|
+
The overview shows queued, running and failed jobs, and *Run a batch now* processes a few without
|
|
143
|
+
waiting for the job runner. Needs `autoTranslate` on, since that registers the jobs.
|
|
144
|
+
|
|
131
145
|
The view only lists collections and globals the signed-in user can read, and the endpoints behind
|
|
132
146
|
the actions require a user.
|
|
133
147
|
|
package/dist/client.d.ts
CHANGED
|
@@ -4,13 +4,25 @@ declare const CustomButtonWithTranslator: ({ type }: {
|
|
|
4
4
|
type: "publish" | "save";
|
|
5
5
|
}) => Element_12xgc;
|
|
6
6
|
import { JSX as JSX_1kxb2 } from "react";
|
|
7
|
+
type Progress = {
|
|
8
|
+
pending: number;
|
|
9
|
+
running: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
} | null;
|
|
7
12
|
type Element_12xgc2 = JSX_1kxb2["Element"];
|
|
13
|
+
declare const BulkActions: ({ tab, locales, progress }: {
|
|
14
|
+
tab: string;
|
|
15
|
+
locales: string[];
|
|
16
|
+
progress: Progress;
|
|
17
|
+
}) => Element_12xgc2;
|
|
18
|
+
import { JSX as JSX_1kxb3 } from "react";
|
|
19
|
+
type Element_12xgc3 = JSX_1kxb3["Element"];
|
|
8
20
|
declare const ReviewActions: ({ entity, locale, reviewed }: {
|
|
9
21
|
entity: string;
|
|
10
22
|
locale: string;
|
|
11
23
|
reviewed: boolean;
|
|
12
|
-
}) =>
|
|
13
|
-
import { JSX as
|
|
14
|
-
type
|
|
15
|
-
declare const TranslationsNavLink: () =>
|
|
16
|
-
export { CustomButtonWithTranslator, ReviewActions, TranslationsNavLink, CustomButtonWithTranslator as default };
|
|
24
|
+
}) => Element_12xgc3;
|
|
25
|
+
import { JSX as JSX_1kxb4 } from "react";
|
|
26
|
+
type Element_12xgc4 = JSX_1kxb4["Element"];
|
|
27
|
+
declare const TranslationsNavLink: () => Element_12xgc4;
|
|
28
|
+
export { BulkActions, CustomButtonWithTranslator, ReviewActions, TranslationsNavLink, CustomButtonWithTranslator as default };
|
package/dist/client.js
CHANGED
|
@@ -315,14 +315,125 @@ var CustomButtonWithTranslator = ({ type }) => {
|
|
|
315
315
|
});
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
-
// src/client/components/Review/
|
|
318
|
+
// src/client/components/Review/BulkActions.tsx
|
|
319
319
|
import { Button as Button3, toast as toast2, useConfig as useConfig3 } from "@payloadcms/ui";
|
|
320
320
|
import { useState as useState2 } from "react";
|
|
321
321
|
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
322
322
|
|
|
323
|
-
var
|
|
323
|
+
var BulkActions = ({ tab, locales, progress }) => {
|
|
324
324
|
const { config: { routes: { api }, serverURL } } = useConfig3();
|
|
325
|
+
const [locale, setLocale] = useState2("");
|
|
325
326
|
const [pending, setPending] = useState2(null);
|
|
327
|
+
const post = async (path, body) => {
|
|
328
|
+
const res = await fetch(`${serverURL ?? ""}${api}/translator/review/${path}`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
credentials: "include",
|
|
331
|
+
headers: { "Content-Type": "application/json" },
|
|
332
|
+
body: JSON.stringify(body)
|
|
333
|
+
});
|
|
334
|
+
const result = await res.json().catch(() => ({}));
|
|
335
|
+
if (!res.ok || !result.success)
|
|
336
|
+
throw new Error(result?.errors?.[0]?.message ?? "request failed");
|
|
337
|
+
return result;
|
|
338
|
+
};
|
|
339
|
+
const queue = async () => {
|
|
340
|
+
const scope = `${locale ? locale.toUpperCase() : "every locale"} of all ${tab}`;
|
|
341
|
+
if (!window.confirm(`Queue translation of empty, untranslated and wrong-language fields in ${scope}? Correct translations are kept.`))
|
|
342
|
+
return;
|
|
343
|
+
setPending("bulk");
|
|
344
|
+
try {
|
|
345
|
+
const { queuedDocuments, queuedLocales, skippedPending } = await post("bulk", { tab, locale, mode: "untranslated" });
|
|
346
|
+
toast2.success(queuedDocuments ? `Queued ${queuedDocuments} documents (${queuedLocales} locales)${skippedPending ? `, ${skippedPending} already queued` : ""}` : skippedPending ? "Everything incomplete is already queued" : "Nothing to translate");
|
|
347
|
+
window.location.reload();
|
|
348
|
+
} catch (error) {
|
|
349
|
+
toast2.error(`Could not queue: ${error instanceof Error ? error.message : String(error)}`);
|
|
350
|
+
} finally {
|
|
351
|
+
setPending(null);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const runNow = async () => {
|
|
355
|
+
setPending("run");
|
|
356
|
+
try {
|
|
357
|
+
await post("run-jobs", {});
|
|
358
|
+
toast2.success("Ran a batch of queued translations");
|
|
359
|
+
window.location.reload();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
toast2.error(`Could not run jobs: ${error instanceof Error ? error.message : String(error)}`);
|
|
362
|
+
} finally {
|
|
363
|
+
setPending(null);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
return /* @__PURE__ */ jsxs5("div", {
|
|
367
|
+
className: "tr__bulk",
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ jsxs5("div", {
|
|
370
|
+
className: "tr__bulk-main",
|
|
371
|
+
children: [
|
|
372
|
+
/* @__PURE__ */ jsxs5("select", {
|
|
373
|
+
"aria-label": "Locale",
|
|
374
|
+
className: "tr__select",
|
|
375
|
+
onChange: (event) => setLocale(event.target.value),
|
|
376
|
+
value: locale,
|
|
377
|
+
children: [
|
|
378
|
+
/* @__PURE__ */ jsx7("option", {
|
|
379
|
+
value: "",
|
|
380
|
+
children: "All locales"
|
|
381
|
+
}),
|
|
382
|
+
locales.map((each) => /* @__PURE__ */ jsx7("option", {
|
|
383
|
+
value: each,
|
|
384
|
+
children: each.toUpperCase()
|
|
385
|
+
}, each))
|
|
386
|
+
]
|
|
387
|
+
}),
|
|
388
|
+
/* @__PURE__ */ jsx7(Button3, {
|
|
389
|
+
buttonStyle: "primary",
|
|
390
|
+
disabled: Boolean(pending),
|
|
391
|
+
onClick: queue,
|
|
392
|
+
size: "small",
|
|
393
|
+
children: pending === "bulk" ? "Queuing…" : "Translate untranslated"
|
|
394
|
+
})
|
|
395
|
+
]
|
|
396
|
+
}),
|
|
397
|
+
progress && (progress.pending > 0 || progress.failed > 0) && /* @__PURE__ */ jsxs5("div", {
|
|
398
|
+
className: "tr__bulk-progress",
|
|
399
|
+
children: [
|
|
400
|
+
progress.pending > 0 && /* @__PURE__ */ jsxs5("span", {
|
|
401
|
+
className: "tr__pill",
|
|
402
|
+
"data-tone": "warn",
|
|
403
|
+
children: [
|
|
404
|
+
progress.pending,
|
|
405
|
+
" queued",
|
|
406
|
+
progress.running ? `, ${progress.running} running` : ""
|
|
407
|
+
]
|
|
408
|
+
}),
|
|
409
|
+
progress.failed > 0 && /* @__PURE__ */ jsxs5("span", {
|
|
410
|
+
className: "tr__pill",
|
|
411
|
+
"data-tone": "bad",
|
|
412
|
+
children: [
|
|
413
|
+
progress.failed,
|
|
414
|
+
" failed today"
|
|
415
|
+
]
|
|
416
|
+
}),
|
|
417
|
+
progress.pending > 0 && /* @__PURE__ */ jsx7(Button3, {
|
|
418
|
+
buttonStyle: "secondary",
|
|
419
|
+
disabled: Boolean(pending),
|
|
420
|
+
onClick: runNow,
|
|
421
|
+
size: "small",
|
|
422
|
+
children: pending === "run" ? "Running…" : "Run a batch now"
|
|
423
|
+
})
|
|
424
|
+
]
|
|
425
|
+
})
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
// src/client/components/Review/ReviewActions.tsx
|
|
430
|
+
import { Button as Button4, toast as toast3, useConfig as useConfig4 } from "@payloadcms/ui";
|
|
431
|
+
import { useState as useState3 } from "react";
|
|
432
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
433
|
+
|
|
434
|
+
var ReviewActions = ({ entity, locale, reviewed }) => {
|
|
435
|
+
const { config: { routes: { api }, serverURL } } = useConfig4();
|
|
436
|
+
const [pending, setPending] = useState3(null);
|
|
326
437
|
const run = async (action) => {
|
|
327
438
|
if (action === "translate-all" && !window.confirm(`Re-translate every field in ${locale}? Existing ${locale} copy is replaced.`))
|
|
328
439
|
return;
|
|
@@ -333,37 +444,37 @@ var ReviewActions = ({ entity, locale, reviewed }) => {
|
|
|
333
444
|
method: "POST",
|
|
334
445
|
credentials: "include",
|
|
335
446
|
headers: { "Content-Type": "application/json" },
|
|
336
|
-
body: JSON.stringify({ entity, locale, mode: action === "translate-all" ? "all" : "
|
|
447
|
+
body: JSON.stringify({ entity, locale, mode: action === "translate-all" ? "all" : "untranslated" })
|
|
337
448
|
});
|
|
338
449
|
const result = await res.json().catch(() => ({}));
|
|
339
450
|
if (!res.ok || !result.success)
|
|
340
451
|
throw new Error(result?.errors?.[0]?.message ?? "request failed");
|
|
341
|
-
|
|
452
|
+
toast3.success(action === "mark-reviewed" ? `${locale} marked as reviewed` : `${locale} translated`);
|
|
342
453
|
window.location.reload();
|
|
343
454
|
} catch (error) {
|
|
344
|
-
|
|
455
|
+
toast3.error(`Could not ${action.replace("-", " ")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
456
|
} finally {
|
|
346
457
|
setPending(null);
|
|
347
458
|
}
|
|
348
459
|
};
|
|
349
|
-
return /* @__PURE__ */
|
|
460
|
+
return /* @__PURE__ */ jsxs6("div", {
|
|
350
461
|
className: "translator-review__actions",
|
|
351
462
|
children: [
|
|
352
|
-
/* @__PURE__ */
|
|
463
|
+
/* @__PURE__ */ jsx8(Button4, {
|
|
353
464
|
buttonStyle: "primary",
|
|
354
465
|
disabled: Boolean(pending),
|
|
355
|
-
onClick: () => run("translate-
|
|
466
|
+
onClick: () => run("translate-untranslated"),
|
|
356
467
|
size: "small",
|
|
357
|
-
children: pending === "translate-
|
|
468
|
+
children: pending === "translate-untranslated" ? "Translating…" : "Translate untranslated fields"
|
|
358
469
|
}),
|
|
359
|
-
/* @__PURE__ */
|
|
470
|
+
/* @__PURE__ */ jsx8(Button4, {
|
|
360
471
|
buttonStyle: "secondary",
|
|
361
472
|
disabled: Boolean(pending),
|
|
362
473
|
onClick: () => run("translate-all"),
|
|
363
474
|
size: "small",
|
|
364
475
|
children: pending === "translate-all" ? "Translating…" : "Re-translate all"
|
|
365
476
|
}),
|
|
366
|
-
/* @__PURE__ */
|
|
477
|
+
/* @__PURE__ */ jsx8(Button4, {
|
|
367
478
|
buttonStyle: "secondary",
|
|
368
479
|
disabled: Boolean(pending) || reviewed,
|
|
369
480
|
onClick: () => run("mark-reviewed"),
|
|
@@ -374,18 +485,18 @@ var ReviewActions = ({ entity, locale, reviewed }) => {
|
|
|
374
485
|
});
|
|
375
486
|
};
|
|
376
487
|
// src/client/components/Review/TranslationsNavLink.tsx
|
|
377
|
-
import { useConfig as
|
|
488
|
+
import { useConfig as useConfig5 } from "@payloadcms/ui";
|
|
378
489
|
import { formatAdminURL } from "payload/shared";
|
|
379
|
-
import { jsx as
|
|
490
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
380
491
|
|
|
381
492
|
var TranslationsNavLink = () => {
|
|
382
|
-
const { config: { routes: { admin } } } =
|
|
493
|
+
const { config: { routes: { admin } } } = useConfig5();
|
|
383
494
|
const href = formatAdminURL({ adminRoute: admin, path: REVIEW_VIEW_PATH });
|
|
384
|
-
return /* @__PURE__ */
|
|
495
|
+
return /* @__PURE__ */ jsx9("a", {
|
|
385
496
|
className: "nav__link",
|
|
386
497
|
href,
|
|
387
498
|
id: "nav-translations",
|
|
388
|
-
children: /* @__PURE__ */
|
|
499
|
+
children: /* @__PURE__ */ jsx9("span", {
|
|
389
500
|
className: "nav__link-label",
|
|
390
501
|
children: "Translations"
|
|
391
502
|
})
|
|
@@ -395,6 +506,7 @@ var TranslationsNavLink = () => {
|
|
|
395
506
|
// src/exports/client.ts
|
|
396
507
|
var client_default = CustomButtonWithTranslator;
|
|
397
508
|
export {
|
|
509
|
+
BulkActions,
|
|
398
510
|
CustomButtonWithTranslator,
|
|
399
511
|
ReviewActions,
|
|
400
512
|
TranslationsNavLink,
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ type TranslateArgs = {
|
|
|
6
6
|
data?: Record<string, any>;
|
|
7
7
|
emptyOnly?: boolean;
|
|
8
8
|
globalSlug?: string;
|
|
9
|
+
/** with `emptyOnly`, also replace fields that still hold the source copy */
|
|
10
|
+
retranslateIdentical?: boolean;
|
|
9
11
|
id?: number | string;
|
|
10
12
|
locale: string;
|
|
11
13
|
localeFrom: string;
|
|
@@ -18,6 +20,8 @@ type TranslateResult = {
|
|
|
18
20
|
} | {
|
|
19
21
|
success: true;
|
|
20
22
|
translatedData: Record<string, any>;
|
|
23
|
+
/** values sent to the resolver; 0 means the target needed nothing */
|
|
24
|
+
translatedCount: number;
|
|
21
25
|
dataFrom: Doc;
|
|
22
26
|
};
|
|
23
27
|
type TranslateOperationArgs = ({
|
|
@@ -45,6 +49,11 @@ type TranslateResolver = {
|
|
|
45
49
|
key: string;
|
|
46
50
|
resolve: (args: TranslateResolverArgs) => Promise<TranslateResolverResponse> | TranslateResolverResponse;
|
|
47
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* `missing` fills empty fields, `untranslated` also replaces fields that still hold the
|
|
54
|
+
* source copy, `all` re-translates everything.
|
|
55
|
+
*/
|
|
56
|
+
type TranslateMode = "all" | "missing" | "untranslated";
|
|
48
57
|
type TranslatorConfig = {
|
|
49
58
|
collections: CollectionSlug[];
|
|
50
59
|
disabled?: boolean;
|
|
@@ -56,9 +65,11 @@ type TranslatorConfig = {
|
|
|
56
65
|
* `missing` (default) fills empty fields and keeps existing translations and slugs;
|
|
57
66
|
* `all` re-translates every field.
|
|
58
67
|
*/
|
|
59
|
-
autoTranslateMode?:
|
|
68
|
+
autoTranslateMode?: TranslateMode;
|
|
60
69
|
/** adds the /admin/translations review view, its endpoints and the status collection (default true) */
|
|
61
70
|
review?: boolean;
|
|
71
|
+
/** flag translated fields written in another locale's language (default true) */
|
|
72
|
+
languageDetection?: boolean;
|
|
62
73
|
_options?: {
|
|
63
74
|
additionalTraverseRichText?: (args: {
|
|
64
75
|
onText: (siblingData: Record<string, unknown>, attribute?: string) => void;
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,19 @@ import {
|
|
|
4
4
|
createAutoTranslateCollectionHook2,
|
|
5
5
|
createAutoTranslateGlobalHook2,
|
|
6
6
|
recordTranslationStatus,
|
|
7
|
+
translateModeArgs,
|
|
8
|
+
isTranslateMode,
|
|
7
9
|
createTranslateTask2,
|
|
8
10
|
createTranslateWorkflow2
|
|
9
|
-
} from "./shared/chunk-
|
|
10
|
-
import"./shared/chunk-
|
|
11
|
+
} from "./shared/chunk-z55qxmj2.js";
|
|
12
|
+
import"./shared/chunk-3rz1t943.js";
|
|
11
13
|
import {
|
|
12
14
|
readLocales,
|
|
13
|
-
parseEntityKey
|
|
14
|
-
|
|
15
|
+
parseEntityKey,
|
|
16
|
+
hasTranslateWorkflow,
|
|
17
|
+
queueBulkTranslation,
|
|
18
|
+
runQueuedTranslations
|
|
19
|
+
} from "./shared/chunk-gd0rak8x.js";
|
|
15
20
|
import {
|
|
16
21
|
TRANSLATION_STATUS_SLUG2,
|
|
17
22
|
REVIEW_VIEW_PATH
|
|
@@ -171,7 +176,7 @@ var translateHandler = async (req) => {
|
|
|
171
176
|
collectionSlug,
|
|
172
177
|
globalSlug,
|
|
173
178
|
id,
|
|
174
|
-
|
|
179
|
+
...translateModeArgs(isTranslateMode(mode) ? mode : "untranslated"),
|
|
175
180
|
locale,
|
|
176
181
|
localeFrom: defaultLocale,
|
|
177
182
|
overrideAccess: false,
|
|
@@ -208,7 +213,39 @@ var markReviewedHandler = async (req) => {
|
|
|
208
213
|
});
|
|
209
214
|
return Response.json({ success: true });
|
|
210
215
|
};
|
|
216
|
+
var bulkHandler = async (req) => {
|
|
217
|
+
if (!req.user)
|
|
218
|
+
throw new APIError("Unauthorized", 401);
|
|
219
|
+
if (!req.json)
|
|
220
|
+
throw new APIError("Content-Type should be json");
|
|
221
|
+
if (!hasTranslateWorkflow(req))
|
|
222
|
+
throw new APIError("Bulk translation needs autoTranslate enabled, which registers the translate jobs", 400);
|
|
223
|
+
const body = await req.json();
|
|
224
|
+
const pluginConfig = req.payload.config.custom?.translator;
|
|
225
|
+
const { targetLocales } = readLocales(req);
|
|
226
|
+
const tabAllowed = body.tab === "globals" ? Boolean(pluginConfig?.globals.length) : pluginConfig?.collections.includes(body.tab);
|
|
227
|
+
if (!body.tab || !tabAllowed)
|
|
228
|
+
throw new APIError("Forbidden", 403);
|
|
229
|
+
if (body.locale && !targetLocales.includes(body.locale))
|
|
230
|
+
throw new APIError("Bad Request", 400);
|
|
231
|
+
const result = await queueBulkTranslation({
|
|
232
|
+
req,
|
|
233
|
+
tab: body.tab,
|
|
234
|
+
locale: body.locale || undefined,
|
|
235
|
+
mode: isTranslateMode(body.mode) ? body.mode : "untranslated"
|
|
236
|
+
});
|
|
237
|
+
return Response.json({ success: true, ...result });
|
|
238
|
+
};
|
|
239
|
+
var runJobsHandler = async (req) => {
|
|
240
|
+
if (!req.user)
|
|
241
|
+
throw new APIError("Unauthorized", 401);
|
|
242
|
+
if (!hasTranslateWorkflow(req))
|
|
243
|
+
throw new APIError("Translate jobs are not registered", 400);
|
|
244
|
+
return Response.json({ success: true, progress: await runQueuedTranslations(req) });
|
|
245
|
+
};
|
|
211
246
|
var reviewEndpoints = [
|
|
247
|
+
{ path: "/translator/review/bulk", method: "post", handler: bulkHandler },
|
|
248
|
+
{ path: "/translator/review/run-jobs", method: "post", handler: runJobsHandler },
|
|
212
249
|
{ path: "/translator/review/translate", method: "post", handler: translateHandler },
|
|
213
250
|
{ path: "/translator/review/mark-reviewed", method: "post", handler: markReviewedHandler }
|
|
214
251
|
];
|
package/dist/jobs.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
TRANSLATE_TASK_SLUG2,
|
|
3
|
-
TRANSLATE_WORKFLOW_SLUG2,
|
|
4
2
|
createAutoTranslateCollectionHook2,
|
|
5
3
|
createAutoTranslateGlobalHook2,
|
|
6
4
|
createTranslateTask2,
|
|
7
5
|
createTranslateWorkflow2
|
|
8
|
-
} from "./shared/chunk-
|
|
9
|
-
import
|
|
6
|
+
} from "./shared/chunk-z55qxmj2.js";
|
|
7
|
+
import {
|
|
8
|
+
TRANSLATE_TASK_SLUG2,
|
|
9
|
+
TRANSLATE_WORKFLOW_SLUG2
|
|
10
|
+
} from "./shared/chunk-3rz1t943.js";
|
|
10
11
|
|
|
11
12
|
// src/exports/jobs.ts
|
|
12
13
|
var jobs = {
|
package/dist/rsc.js
CHANGED
|
@@ -1,10 +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
|
-
loadEntityLocaleReview
|
|
7
|
-
|
|
6
|
+
loadEntityLocaleReview,
|
|
7
|
+
loadJobsProgress
|
|
8
|
+
} from "./shared/chunk-gd0rak8x.js";
|
|
8
9
|
import {
|
|
9
10
|
REVIEW_VIEW_PATH
|
|
10
11
|
} from "./shared/chunk-31qgv1zk.js";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
// src/review/view/TranslationsView.tsx
|
|
13
14
|
import { DefaultTemplate } from "@payloadcms/next/templates";
|
|
14
15
|
import { Gutter } from "@payloadcms/ui";
|
|
16
|
+
import { BulkActions } from "@justanarthur/payload-plugin-translator/client";
|
|
15
17
|
|
|
16
18
|
// src/review/view/Detail.tsx
|
|
17
19
|
import { ReviewActions } from "@justanarthur/payload-plugin-translator/client";
|
|
@@ -33,7 +35,8 @@ var STATE = {
|
|
|
33
35
|
identical: { label: "same as source", tone: "warn" },
|
|
34
36
|
missing: { label: "missing", tone: "bad" },
|
|
35
37
|
ok: { label: "translated", tone: "ok" },
|
|
36
|
-
placeholders: { label: "placeholders differ", tone: "warn" }
|
|
38
|
+
placeholders: { label: "placeholders differ", tone: "warn" },
|
|
39
|
+
wrongLanguage: { label: "wrong language", tone: "bad" }
|
|
37
40
|
};
|
|
38
41
|
var preview = (text) => text.length > 600 ? `${text.slice(0, 597)}…` : text;
|
|
39
42
|
var readablePath = (path) => path.replace(/\[(\d+)\]/g, (_, index) => ` ${Number(index) + 1}`).replace(/\(([^)]+)\)/g, " · $1").replace(/#(\d+)$/, (_, index) => ` (part ${Number(index) + 1})`).split(".").join(" › ");
|
|
@@ -45,6 +48,7 @@ var Detail = ({ adminRoute, review, onlyIssues, tab, page }) => {
|
|
|
45
48
|
{ label: "translated", value: `${summary.coverage}%`, note: `${summary.ok} of ${summary.total} fields`, bar: true },
|
|
46
49
|
{ label: "missing", value: summary.missing, note: "empty in this locale" },
|
|
47
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}` },
|
|
48
52
|
{ label: "placeholders", value: summary.placeholders, note: "differ from source" }
|
|
49
53
|
];
|
|
50
54
|
return /* @__PURE__ */ jsxs(Fragment, {
|
|
@@ -217,7 +221,7 @@ var Detail = ({ adminRoute, review, onlyIssues, tab, page }) => {
|
|
|
217
221
|
children: /* @__PURE__ */ jsx("span", {
|
|
218
222
|
className: "tr__pill",
|
|
219
223
|
"data-tone": STATE[field.state].tone,
|
|
220
|
-
children: STATE[field.state].label
|
|
224
|
+
children: field.detectedLanguage ? `${field.detectedLanguage} detected` : STATE[field.state].label
|
|
221
225
|
})
|
|
222
226
|
}),
|
|
223
227
|
/* @__PURE__ */ jsx("td", {
|
|
@@ -260,14 +264,15 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
260
264
|
return {
|
|
261
265
|
locale,
|
|
262
266
|
coverage: fields ? Math.floor(ok / fields * 100) : 100,
|
|
263
|
-
incomplete: reviews.filter(isIncomplete).length
|
|
267
|
+
incomplete: reviews.filter(isIncomplete).length,
|
|
268
|
+
wrongLanguage: reviews.filter((each) => each.summary.wrongLanguage > 0).length
|
|
264
269
|
};
|
|
265
270
|
});
|
|
266
271
|
return /* @__PURE__ */ jsxs2(Fragment2, {
|
|
267
272
|
children: [
|
|
268
273
|
/* @__PURE__ */ jsx2("div", {
|
|
269
274
|
className: "tr__stats",
|
|
270
|
-
children: stats.map(({ locale, coverage, incomplete }) => /* @__PURE__ */ jsxs2("div", {
|
|
275
|
+
children: stats.map(({ locale, coverage, incomplete, wrongLanguage }) => /* @__PURE__ */ jsxs2("div", {
|
|
271
276
|
className: "tr__stat",
|
|
272
277
|
children: [
|
|
273
278
|
/* @__PURE__ */ jsx2("span", {
|
|
@@ -291,6 +296,14 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
291
296
|
/* @__PURE__ */ jsx2("span", {
|
|
292
297
|
className: "tr__stat-note",
|
|
293
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
|
+
]
|
|
294
307
|
})
|
|
295
308
|
]
|
|
296
309
|
}, locale))
|
|
@@ -337,6 +350,7 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
337
350
|
summary.missing && `${summary.missing} missing`,
|
|
338
351
|
summary.identical && `${summary.identical} same as ${defaultLocale}`,
|
|
339
352
|
summary.placeholders && `${summary.placeholders} broken placeholders`,
|
|
353
|
+
summary.wrongLanguage && `${summary.wrongLanguage} written in another language`,
|
|
340
354
|
summary.slugMissing && "no slug: the page 404s",
|
|
341
355
|
review.stale && "source changed since translation",
|
|
342
356
|
review.reviewed && "reviewed"
|
|
@@ -358,6 +372,13 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
358
372
|
" ",
|
|
359
373
|
[review.stale && "↻", review.reviewed && "✓"].filter(Boolean).join(" ")
|
|
360
374
|
]
|
|
375
|
+
}),
|
|
376
|
+
summary.wrongLanguage > 0 && /* @__PURE__ */ jsxs2("span", {
|
|
377
|
+
className: "tr__lang",
|
|
378
|
+
children: [
|
|
379
|
+
" ⚑",
|
|
380
|
+
summary.wrongLanguage
|
|
381
|
+
]
|
|
361
382
|
})
|
|
362
383
|
]
|
|
363
384
|
}),
|
|
@@ -415,6 +436,10 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab,
|
|
|
415
436
|
"under half, or 404 without a slug"
|
|
416
437
|
]
|
|
417
438
|
}),
|
|
439
|
+
/* @__PURE__ */ jsx2("span", {
|
|
440
|
+
className: "tr__lang",
|
|
441
|
+
children: "⚑ fields written in another language"
|
|
442
|
+
}),
|
|
418
443
|
/* @__PURE__ */ jsx2("span", {
|
|
419
444
|
children: "↻ source changed since the last translation"
|
|
420
445
|
}),
|
|
@@ -456,6 +481,10 @@ var reviewStyles = `
|
|
|
456
481
|
.tr__toggle::before { content: ''; width: 26px; height: 14px; border-radius: 999px; background: var(--theme-elevation-200) radial-gradient(circle at 7px 50%, var(--theme-elevation-0) 4px, transparent 4.5px); }
|
|
457
482
|
.tr__toggle[aria-pressed='true']::before { background: var(--tr-ok) radial-gradient(circle at 19px 50%, var(--theme-elevation-0) 4px, transparent 4.5px); }
|
|
458
483
|
|
|
484
|
+
.tr__bulk { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: calc(var(--base) * 1.25); padding: 10px 12px; border: 1px dashed var(--tr-line); border-radius: var(--style-radius-m); }
|
|
485
|
+
.tr__bulk-main, .tr__bulk-progress { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
486
|
+
.tr__bulk .btn { margin: 0; }
|
|
487
|
+
.tr__select { height: 28px; padding: 0 8px; border: 1px solid var(--tr-line); border-radius: var(--style-radius-s, 4px); background: var(--theme-input-bg, var(--theme-elevation-0)); color: var(--theme-text); font: inherit; font-size: 13px; }
|
|
459
488
|
.tr__card { border: 1px solid var(--tr-line); border-radius: var(--style-radius-m); overflow: auto; background: var(--theme-elevation-0); }
|
|
460
489
|
.tr table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
|
|
461
490
|
.tr th { position: sticky; top: 0; z-index: 1; background: var(--tr-card); text-align: left; font-weight: 500; color: var(--tr-muted); padding: 10px 12px; border-bottom: 1px solid var(--tr-line); white-space: nowrap; }
|
|
@@ -475,6 +504,7 @@ var reviewStyles = `
|
|
|
475
504
|
.tr__cell[data-tone='warn'] { --tr-tone: var(--tr-warn); color: var(--tr-warn); }
|
|
476
505
|
.tr__cell[data-tone='bad'] { --tr-tone: var(--tr-bad); color: var(--tr-bad); }
|
|
477
506
|
.tr__marks { font-size: 11px; color: var(--tr-muted); }
|
|
507
|
+
.tr__lang { color: var(--tr-bad); font-weight: 500; }
|
|
478
508
|
|
|
479
509
|
.tr__legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px; font-size: 12px; color: var(--tr-muted); }
|
|
480
510
|
.tr__legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
@@ -628,6 +658,7 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
|
|
|
628
658
|
});
|
|
629
659
|
};
|
|
630
660
|
const adminLocale = req.locale;
|
|
661
|
+
const progress = entity || !req.user ? null : await loadJobsProgress(req);
|
|
631
662
|
const body = await content();
|
|
632
663
|
req.locale = adminLocale;
|
|
633
664
|
return /* @__PURE__ */ jsxs3(DefaultTemplate, {
|
|
@@ -685,6 +716,11 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
|
|
|
685
716
|
children: "Only incomplete"
|
|
686
717
|
})
|
|
687
718
|
]
|
|
719
|
+
}),
|
|
720
|
+
req.user && tab && /* @__PURE__ */ jsx3(BulkActions, {
|
|
721
|
+
locales: targetLocales,
|
|
722
|
+
progress,
|
|
723
|
+
tab
|
|
688
724
|
})
|
|
689
725
|
]
|
|
690
726
|
}),
|
|
@@ -1,7 +1,72 @@
|
|
|
1
|
+
// src/jobs/constants.ts
|
|
2
|
+
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
3
|
+
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
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
|
+
|
|
1
47
|
// src/utils/placeholders.ts
|
|
2
48
|
var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
|
|
3
49
|
var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
|
|
4
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
|
+
|
|
5
70
|
// src/translate/traverseFields.ts
|
|
6
71
|
import ObjectID from "bson-objectid";
|
|
7
72
|
import { tabHasName } from "payload/shared";
|
|
@@ -46,6 +111,13 @@ var isOpaqueText = (value) => {
|
|
|
46
111
|
return /^([a-z][a-z0-9+.-]*:|\/\/|\/|#|www\.)/i.test(text) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) || !/\p{L}/u.test(text);
|
|
47
112
|
};
|
|
48
113
|
|
|
114
|
+
// src/utils/looksUntranslated.ts
|
|
115
|
+
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
116
|
+
var looksUntranslated = (source, target) => {
|
|
117
|
+
const sourceText = normalize(plainText(source));
|
|
118
|
+
return sourceText.length > 3 && /\s/.test(sourceText) && !isOpaqueText(sourceText) && sourceText === normalize(plainText(target));
|
|
119
|
+
};
|
|
120
|
+
|
|
49
121
|
// src/utils/sanitizeSlug.ts
|
|
50
122
|
var sanitizeSlug = (text) => text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
51
123
|
|
|
@@ -95,6 +167,8 @@ var traverseFields = (args) => {
|
|
|
95
167
|
const {
|
|
96
168
|
dataFrom,
|
|
97
169
|
emptyOnly,
|
|
170
|
+
retranslateIdentical,
|
|
171
|
+
isWrongLanguage,
|
|
98
172
|
fields,
|
|
99
173
|
localizedParent,
|
|
100
174
|
path,
|
|
@@ -106,6 +180,7 @@ var traverseFields = (args) => {
|
|
|
106
180
|
const { additionalTraverseRichText } = _options ?? {};
|
|
107
181
|
const siblingDataFrom = args.siblingDataFrom ?? dataFrom;
|
|
108
182
|
const siblingDataTranslated = args.siblingDataTranslated ?? translatedData;
|
|
183
|
+
const keepsTarget = (source, target) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && (looksUntranslated(source, target) || Boolean(isWrongLanguage?.(target))));
|
|
109
184
|
const recurse = (overrides) => traverseFields({ ...args, siblingDataFrom, siblingDataTranslated, ...overrides });
|
|
110
185
|
for (const field of fields) {
|
|
111
186
|
switch (field.type) {
|
|
@@ -226,7 +301,7 @@ var traverseFields = (args) => {
|
|
|
226
301
|
onField?.({ path: keyPath, type: "json", source: value, target: previous });
|
|
227
302
|
if (!value.trim())
|
|
228
303
|
continue;
|
|
229
|
-
if (
|
|
304
|
+
if (keepsTarget(value, previous))
|
|
230
305
|
continue;
|
|
231
306
|
target[key] = value;
|
|
232
307
|
if (isOpaqueText(value))
|
|
@@ -266,7 +341,7 @@ var traverseFields = (args) => {
|
|
|
266
341
|
if (typeof value !== "string" || !value.trim())
|
|
267
342
|
break;
|
|
268
343
|
const hasCurrent = typeof current === "string" && current.trim().length > 0;
|
|
269
|
-
if (hasCurrent
|
|
344
|
+
if (isSlug ? hasCurrent : keepsTarget(value, current))
|
|
270
345
|
break;
|
|
271
346
|
if (isOpaqueText(value)) {
|
|
272
347
|
siblingDataTranslated[field.name] = value;
|
|
@@ -304,7 +379,7 @@ var traverseFields = (args) => {
|
|
|
304
379
|
onField?.({ path: richTextPath, type: "richText", source: richTextDataFrom, target: current });
|
|
305
380
|
if (!richTextDataFrom || !hasText(richTextDataFrom))
|
|
306
381
|
break;
|
|
307
|
-
if (
|
|
382
|
+
if (keepsTarget(richTextDataFrom, current))
|
|
308
383
|
break;
|
|
309
384
|
const isSlate = Array.isArray(richTextDataFrom);
|
|
310
385
|
const isLexical = typeof richTextDataFrom === "object" && "root" in richTextDataFrom;
|
|
@@ -356,4 +431,4 @@ var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${g
|
|
|
356
431
|
import { createHash } from "node:crypto";
|
|
357
432
|
var sourceHash = (fields) => createHash("sha1").update(JSON.stringify(fields.map(({ path, source }) => [path, source]))).digest("hex");
|
|
358
433
|
|
|
359
|
-
export { samePlaceholders,
|
|
434
|
+
export { loadWrongLanguageCheck, samePlaceholders, plainText, looksUntranslated, traverseFields, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, collectTranslatableFields, entityKey, sourceHash };
|
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
loadWrongLanguageCheck,
|
|
2
3
|
samePlaceholders,
|
|
3
|
-
|
|
4
|
+
plainText,
|
|
5
|
+
looksUntranslated,
|
|
6
|
+
TRANSLATE_WORKFLOW_SLUG2,
|
|
4
7
|
collectTranslatableFields,
|
|
5
8
|
entityKey,
|
|
6
9
|
sourceHash
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3rz1t943.js";
|
|
8
11
|
import {
|
|
9
12
|
TRANSLATION_STATUS_SLUG2
|
|
10
13
|
} from "./chunk-31qgv1zk.js";
|
|
11
14
|
|
|
12
|
-
// src/review/plainText.ts
|
|
13
|
-
var plainText = (value) => {
|
|
14
|
-
if (typeof value === "string")
|
|
15
|
-
return value;
|
|
16
|
-
if (!value || typeof value !== "object")
|
|
17
|
-
return "";
|
|
18
|
-
if (Array.isArray(value))
|
|
19
|
-
return value.map(plainText).filter(Boolean).join(" ");
|
|
20
|
-
const node = value;
|
|
21
|
-
if (typeof node.text === "string")
|
|
22
|
-
return node.text;
|
|
23
|
-
if ("root" in node)
|
|
24
|
-
return plainText(node.root);
|
|
25
|
-
if (Array.isArray(node.children))
|
|
26
|
-
return node.children.map(plainText).filter(Boolean).join(node.type === "root" ? `
|
|
27
|
-
` : "");
|
|
28
|
-
return "";
|
|
29
|
-
};
|
|
30
|
-
|
|
31
15
|
// src/review/computeStatus.ts
|
|
32
16
|
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
33
17
|
var classify = (field, sourceText, targetText) => {
|
|
@@ -35,19 +19,26 @@ var classify = (field, sourceText, targetText) => {
|
|
|
35
19
|
return "missing";
|
|
36
20
|
if (field.type === "json" && !samePlaceholders(sourceText, targetText))
|
|
37
21
|
return "placeholders";
|
|
38
|
-
|
|
39
|
-
if (same && field.type !== "slug" && normalize(sourceText).length > 3 && !isOpaqueText(sourceText) && /\s/.test(normalize(sourceText)))
|
|
22
|
+
if (field.type !== "slug" && looksUntranslated(sourceText, targetText))
|
|
40
23
|
return "identical";
|
|
41
24
|
return "ok";
|
|
42
25
|
};
|
|
43
|
-
var computeStatus = (fields) => {
|
|
26
|
+
var computeStatus = (fields, language) => {
|
|
44
27
|
const statuses = [];
|
|
45
28
|
for (const field of fields) {
|
|
46
29
|
const sourceText = plainText(field.source);
|
|
47
30
|
if (!normalize(sourceText))
|
|
48
31
|
continue;
|
|
49
32
|
const targetText = plainText(field.target);
|
|
50
|
-
|
|
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
|
+
});
|
|
51
42
|
}
|
|
52
43
|
const count = (state) => statuses.filter((each) => each.state === state).length;
|
|
53
44
|
const ok = count("ok");
|
|
@@ -59,6 +50,7 @@ var computeStatus = (fields) => {
|
|
|
59
50
|
missing: count("missing"),
|
|
60
51
|
identical: count("identical"),
|
|
61
52
|
placeholders: count("placeholders"),
|
|
53
|
+
wrongLanguage: count("wrongLanguage"),
|
|
62
54
|
coverage: statuses.length ? Math.floor(ok / statuses.length * 100) : 100,
|
|
63
55
|
slugMissing: statuses.some((each) => each.type === "slug" && each.state === "missing")
|
|
64
56
|
}
|
|
@@ -87,13 +79,14 @@ var loadStatusRows = async (req, keys) => {
|
|
|
87
79
|
});
|
|
88
80
|
return docs;
|
|
89
81
|
};
|
|
90
|
-
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) => {
|
|
91
84
|
const { fields, summary } = computeStatus(collectTranslatableFields({
|
|
92
85
|
config,
|
|
93
86
|
dataFrom: source,
|
|
94
87
|
dataTarget: target ?? {},
|
|
95
88
|
options: pluginOptions(req)?._options
|
|
96
|
-
}));
|
|
89
|
+
}), { locale, check });
|
|
97
90
|
return {
|
|
98
91
|
fields,
|
|
99
92
|
summary,
|
|
@@ -112,6 +105,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
112
105
|
if (!config)
|
|
113
106
|
throw new Error(`unknown collection ${collectionSlug}`);
|
|
114
107
|
const { defaultLocale, targetLocales } = readLocales(req);
|
|
108
|
+
const check = await loadLanguageCheck(req);
|
|
115
109
|
const sources = await req.payload.find({
|
|
116
110
|
collection: collectionSlug,
|
|
117
111
|
locale: defaultLocale,
|
|
@@ -150,7 +144,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
150
144
|
collectionSlug,
|
|
151
145
|
id: source.id,
|
|
152
146
|
locales: Object.fromEntries(targetLocales.map((locale) => {
|
|
153
|
-
const { fields: _fields, ...review } = reviewLocale(config, source, targets[locale].get(String(source.id)), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
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);
|
|
154
148
|
return [locale, review];
|
|
155
149
|
}))
|
|
156
150
|
};
|
|
@@ -165,6 +159,7 @@ var loadCollectionReview = async ({ req, collectionSlug, page = 1, limit = 25 })
|
|
|
165
159
|
};
|
|
166
160
|
var loadGlobalsReview = async ({ req, globalSlugs }) => {
|
|
167
161
|
const { defaultLocale, targetLocales } = readLocales(req);
|
|
162
|
+
const check = await loadLanguageCheck(req);
|
|
168
163
|
const rows = await loadStatusRows(req, globalSlugs.map((globalSlug) => entityKey({ globalSlug })));
|
|
169
164
|
const entities = [];
|
|
170
165
|
for (const globalSlug of globalSlugs) {
|
|
@@ -184,7 +179,7 @@ var loadGlobalsReview = async ({ req, globalSlugs }) => {
|
|
|
184
179
|
const key = entityKey({ globalSlug });
|
|
185
180
|
const locales = {};
|
|
186
181
|
for (const locale of targetLocales) {
|
|
187
|
-
const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req);
|
|
182
|
+
const { fields: _fields, ...review } = reviewLocale(config, source, await read(locale), hash, rows.find((row) => row.entity === key && row.locale === locale), req, locale, check);
|
|
188
183
|
locales[locale] = review;
|
|
189
184
|
}
|
|
190
185
|
entities.push({ key, label: globalLabel(config), globalSlug, locales });
|
|
@@ -207,7 +202,7 @@ var loadEntityLocaleReview = async ({ req, entity, locale }) => {
|
|
|
207
202
|
const target = await read(locale);
|
|
208
203
|
const rows = await loadStatusRows(req, [entity]);
|
|
209
204
|
const row = rows.find((each) => each.locale === locale);
|
|
210
|
-
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));
|
|
211
206
|
return {
|
|
212
207
|
...review,
|
|
213
208
|
entity,
|
|
@@ -250,4 +245,108 @@ var loadLastJob = async (req, { collectionSlug, globalSlug, id }, locale) => {
|
|
|
250
245
|
}
|
|
251
246
|
};
|
|
252
247
|
|
|
253
|
-
|
|
248
|
+
// src/review/bulk.ts
|
|
249
|
+
var JOBS_SLUG = "payload-jobs";
|
|
250
|
+
var pendingWhere = {
|
|
251
|
+
and: [
|
|
252
|
+
{ workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } },
|
|
253
|
+
{ completedAt: { exists: false } },
|
|
254
|
+
{ hasError: { not_equals: true } }
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
var hasTranslateWorkflow = (req) => Boolean(req.payload.config.jobs?.workflows?.some((workflow) => workflow.slug === TRANSLATE_WORKFLOW_SLUG2)) && Boolean(req.payload.collections[JOBS_SLUG]);
|
|
258
|
+
var loadJobsProgress = async (req) => {
|
|
259
|
+
if (!hasTranslateWorkflow(req))
|
|
260
|
+
return null;
|
|
261
|
+
const count = (where) => req.payload.count({ collection: JOBS_SLUG, where, overrideAccess: true, req }).then(({ totalDocs }) => totalDocs);
|
|
262
|
+
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
263
|
+
const [pending, running, failed] = await Promise.all([
|
|
264
|
+
count(pendingWhere),
|
|
265
|
+
count({ and: [...pendingWhere.and ?? [], { processing: { equals: true } }] }),
|
|
266
|
+
count({ and: [{ workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } }, { hasError: { equals: true } }, { updatedAt: { greater_than: dayAgo } }] })
|
|
267
|
+
]);
|
|
268
|
+
return { pending, running, failed };
|
|
269
|
+
};
|
|
270
|
+
var loadPendingPairs = async (req) => {
|
|
271
|
+
const { docs } = await req.payload.find({
|
|
272
|
+
collection: JOBS_SLUG,
|
|
273
|
+
where: pendingWhere,
|
|
274
|
+
pagination: false,
|
|
275
|
+
depth: 0,
|
|
276
|
+
overrideAccess: true,
|
|
277
|
+
req
|
|
278
|
+
});
|
|
279
|
+
const pairs = new Set;
|
|
280
|
+
for (const { input } of docs) {
|
|
281
|
+
if (!input)
|
|
282
|
+
continue;
|
|
283
|
+
const key = entityKey({ collectionSlug: input.collection, globalSlug: input.global, id: input.id });
|
|
284
|
+
for (const locale of input.toLocales ?? (input.toLocale ? [input.toLocale] : []))
|
|
285
|
+
pairs.add(`${key}|${locale}`);
|
|
286
|
+
}
|
|
287
|
+
return pairs;
|
|
288
|
+
};
|
|
289
|
+
var incompleteLocales = (entity, locales) => locales.filter((locale) => {
|
|
290
|
+
const review = entity.locales[locale];
|
|
291
|
+
return review && (review.summary.coverage < 100 || review.summary.slugMissing);
|
|
292
|
+
});
|
|
293
|
+
var queueBulkTranslation = async ({ req, tab, locale, mode }) => {
|
|
294
|
+
const pluginConfig = req.payload.config.custom?.translator;
|
|
295
|
+
const { defaultLocale, targetLocales } = readLocales(req);
|
|
296
|
+
const locales = locale ? [locale] : targetLocales;
|
|
297
|
+
const resolver = pluginConfig.resolvers[0]?.key;
|
|
298
|
+
const pending = await loadPendingPairs(req);
|
|
299
|
+
let queuedDocuments = 0;
|
|
300
|
+
let queuedLocales = 0;
|
|
301
|
+
let skippedPending = 0;
|
|
302
|
+
const queue = async (entity) => {
|
|
303
|
+
const toLocales = incompleteLocales(entity, locales).filter((each) => {
|
|
304
|
+
const already = pending.has(`${entity.key}|${each}`);
|
|
305
|
+
if (already)
|
|
306
|
+
skippedPending++;
|
|
307
|
+
return !already;
|
|
308
|
+
});
|
|
309
|
+
if (!toLocales.length)
|
|
310
|
+
return;
|
|
311
|
+
await req.payload.jobs.queue({
|
|
312
|
+
req,
|
|
313
|
+
workflow: TRANSLATE_WORKFLOW_SLUG2,
|
|
314
|
+
input: {
|
|
315
|
+
id: entity.id,
|
|
316
|
+
collection: entity.collectionSlug,
|
|
317
|
+
global: entity.globalSlug,
|
|
318
|
+
updatedAt: new Date().toISOString(),
|
|
319
|
+
fromLocale: defaultLocale,
|
|
320
|
+
toLocales,
|
|
321
|
+
resolver,
|
|
322
|
+
mode
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
queuedDocuments++;
|
|
326
|
+
queuedLocales += toLocales.length;
|
|
327
|
+
};
|
|
328
|
+
if (tab === "globals") {
|
|
329
|
+
const { entities } = await loadGlobalsReview({ req, globalSlugs: pluginConfig.globals });
|
|
330
|
+
for (const entity of entities)
|
|
331
|
+
await queue(entity);
|
|
332
|
+
} else {
|
|
333
|
+
for (let page = 1;; page++) {
|
|
334
|
+
const review = await loadCollectionReview({ req, collectionSlug: tab, page, limit: 50 });
|
|
335
|
+
for (const entity of review.entities)
|
|
336
|
+
await queue(entity);
|
|
337
|
+
if (page >= review.totalPages)
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { queuedDocuments, queuedLocales, skippedPending };
|
|
342
|
+
};
|
|
343
|
+
var runQueuedTranslations = async (req, limit = 3) => {
|
|
344
|
+
await req.payload.jobs.run({
|
|
345
|
+
limit,
|
|
346
|
+
where: { workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } },
|
|
347
|
+
req
|
|
348
|
+
});
|
|
349
|
+
return loadJobsProgress(req);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export { readLocales, loadCollectionReview, loadGlobalsReview, parseEntityKey, loadEntityLocaleReview, hasTranslateWorkflow, loadJobsProgress, queueBulkTranslation, runQueuedTranslations };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
loadWrongLanguageCheck,
|
|
2
3
|
samePlaceholders,
|
|
4
|
+
plainText,
|
|
3
5
|
traverseFields,
|
|
6
|
+
TRANSLATE_WORKFLOW_SLUG2,
|
|
4
7
|
collectTranslatableFields,
|
|
5
8
|
entityKey,
|
|
6
9
|
sourceHash
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3rz1t943.js";
|
|
8
11
|
import {
|
|
9
12
|
TRANSLATION_STATUS_SLUG2
|
|
10
13
|
} from "./chunk-31qgv1zk.js";
|
|
@@ -95,6 +98,7 @@ var preview = (value) => {
|
|
|
95
98
|
const flat = (typeof value === "string" ? value : String(value ?? "")).replace(/\s+/g, " ").trim();
|
|
96
99
|
return JSON.stringify(flat.length > 60 ? `${flat.slice(0, 57)}…` : flat);
|
|
97
100
|
};
|
|
101
|
+
var localeCodes = (req) => (req.payload.config.localization ? req.payload.config.localization.locales : []).map((each) => typeof each === "string" ? each : each.code);
|
|
98
102
|
var translateOperation2 = async (args) => {
|
|
99
103
|
const req = "req" in args ? args.req : {
|
|
100
104
|
payload: args.payload
|
|
@@ -124,9 +128,13 @@ var translateOperation2 = async (args) => {
|
|
|
124
128
|
});
|
|
125
129
|
translatedData = doc;
|
|
126
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;
|
|
127
133
|
traverseFields({
|
|
128
134
|
dataFrom,
|
|
129
135
|
emptyOnly: args.emptyOnly,
|
|
136
|
+
retranslateIdentical: args.retranslateIdentical,
|
|
137
|
+
isWrongLanguage,
|
|
130
138
|
fields: config.fields,
|
|
131
139
|
translatedData,
|
|
132
140
|
valuesToTranslate,
|
|
@@ -178,7 +186,7 @@ ${valuesToTranslate.map((v) => ` ${v.path ?? "(unknown)"}: ${preview(v.value)}`
|
|
|
178
186
|
${summary.join(`
|
|
179
187
|
`)}` : "")
|
|
180
188
|
});
|
|
181
|
-
if (args.update) {
|
|
189
|
+
if (args.update && valuesToTranslate.length > 0) {
|
|
182
190
|
const { _locale, _parent_id, createdAt, updatedAt, ...data } = translatedData;
|
|
183
191
|
await updateEntity({
|
|
184
192
|
collectionSlug,
|
|
@@ -194,16 +202,13 @@ ${summary.join(`
|
|
|
194
202
|
result = {
|
|
195
203
|
success: true,
|
|
196
204
|
translatedData,
|
|
205
|
+
translatedCount: valuesToTranslate.length,
|
|
197
206
|
dataFrom
|
|
198
207
|
};
|
|
199
208
|
}
|
|
200
209
|
return result;
|
|
201
210
|
};
|
|
202
211
|
|
|
203
|
-
// src/jobs/constants.ts
|
|
204
|
-
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
205
|
-
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
206
|
-
|
|
207
212
|
// src/jobs/createAutoTranslateCollectionHook.ts
|
|
208
213
|
function createAutoTranslateCollectionHook2(options) {
|
|
209
214
|
const {
|
|
@@ -432,6 +437,13 @@ var recordTranslationStatus = async ({ req, collectionSlug, globalSlug, id, loca
|
|
|
432
437
|
await upsertTranslationStatus(req, entityKey({ collectionSlug, globalSlug, id }), locale, reviewedBy ? { reviewedHash: hash, reviewedAt: now, reviewedBy } : { sourceHash: hash, translatedAt: now });
|
|
433
438
|
};
|
|
434
439
|
|
|
440
|
+
// src/utils/translateMode.ts
|
|
441
|
+
var translateModeArgs = (mode) => ({
|
|
442
|
+
emptyOnly: mode !== "all",
|
|
443
|
+
retranslateIdentical: mode === "untranslated"
|
|
444
|
+
});
|
|
445
|
+
var isTranslateMode = (value) => value === "all" || value === "missing" || value === "untranslated";
|
|
446
|
+
|
|
435
447
|
// src/jobs/createTranslateTask.ts
|
|
436
448
|
function createTranslateTask2(options = {}) {
|
|
437
449
|
const { slug = "translateEntityToLocale" } = options;
|
|
@@ -451,7 +463,7 @@ function createTranslateTask2(options = {}) {
|
|
|
451
463
|
handler: async (args) => {
|
|
452
464
|
const { input, job, req } = args;
|
|
453
465
|
const { id, collection, global, fromLocale, toLocale, resolver: inputResolver } = input;
|
|
454
|
-
const mode = input.mode
|
|
466
|
+
const mode = isTranslateMode(input.mode) ? input.mode : readAutoTranslateMode(req);
|
|
455
467
|
if (!collection && !global) {
|
|
456
468
|
throw new Error("translateTask: either `collection` or `global` must be provided");
|
|
457
469
|
}
|
|
@@ -470,7 +482,7 @@ function createTranslateTask2(options = {}) {
|
|
|
470
482
|
req,
|
|
471
483
|
collectionSlug: collection,
|
|
472
484
|
globalSlug: global,
|
|
473
|
-
|
|
485
|
+
...translateModeArgs(mode),
|
|
474
486
|
id,
|
|
475
487
|
locale: toLocale,
|
|
476
488
|
localeFrom: fromLocale,
|
|
@@ -507,6 +519,10 @@ function createTranslateTask2(options = {}) {
|
|
|
507
519
|
});
|
|
508
520
|
return { output: { success: true } };
|
|
509
521
|
}
|
|
522
|
+
if (result.translatedCount === 0) {
|
|
523
|
+
req.payload.logger.info({ jobId: job.id, msg: `[translate] ${entityLabel}#${id ?? global} → ${toLocale}: nothing to translate` });
|
|
524
|
+
return { output: { success: true } };
|
|
525
|
+
}
|
|
510
526
|
const translated = result.translatedData ?? {};
|
|
511
527
|
const { _locale: _dropLocale, _parent_id: _dropParent, updatedAt: _dropUpdatedAt, createdAt: _dropCreatedAt, ...data } = translated;
|
|
512
528
|
req.payload.logger.info({
|
|
@@ -558,7 +574,8 @@ function readFirstResolverKey3(req) {
|
|
|
558
574
|
return typeof first?.key === "string" ? first.key : "";
|
|
559
575
|
}
|
|
560
576
|
function readAutoTranslateMode(req) {
|
|
561
|
-
|
|
577
|
+
const mode = req.payload.config.custom?.translator?.autoTranslateMode;
|
|
578
|
+
return isTranslateMode(mode) ? mode : "missing";
|
|
562
579
|
}
|
|
563
580
|
|
|
564
581
|
// src/jobs/createTranslateWorkflow.ts
|
|
@@ -574,11 +591,12 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
574
591
|
{ name: "fromLocale", type: "text", required: true },
|
|
575
592
|
{ name: "toLocales", type: "json", required: false },
|
|
576
593
|
{ name: "toLocale", type: "text", required: false },
|
|
577
|
-
{ name: "resolver", type: "text", required: false }
|
|
594
|
+
{ name: "resolver", type: "text", required: false },
|
|
595
|
+
{ name: "mode", type: "text", required: false }
|
|
578
596
|
],
|
|
579
597
|
handler: async (args) => {
|
|
580
598
|
const { job, req, tasks } = args;
|
|
581
|
-
const { id, collection, global, fromLocale, toLocales, toLocale, updatedAt, resolver } = job.input;
|
|
599
|
+
const { id, collection, global, fromLocale, toLocales, toLocale, updatedAt, resolver, mode } = job.input;
|
|
582
600
|
if (!collection && !global) {
|
|
583
601
|
throw new Error("translateWorkflow: either `collection` or `global` must be provided");
|
|
584
602
|
}
|
|
@@ -612,7 +630,8 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
612
630
|
global,
|
|
613
631
|
fromLocale,
|
|
614
632
|
toLocale: target,
|
|
615
|
-
resolver
|
|
633
|
+
resolver,
|
|
634
|
+
mode
|
|
616
635
|
}
|
|
617
636
|
});
|
|
618
637
|
}
|
|
@@ -620,4 +639,4 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
620
639
|
};
|
|
621
640
|
}
|
|
622
641
|
|
|
623
|
-
export { findEntityWithConfig, translateOperation2,
|
|
642
|
+
export { findEntityWithConfig, translateOperation2, createAutoTranslateCollectionHook2, createAutoTranslateGlobalHook2, recordTranslationStatus, translateModeArgs, isTranslateMode, createTranslateTask2, createTranslateWorkflow2 };
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ type TranslateResolver = {
|
|
|
16
16
|
key: string;
|
|
17
17
|
resolve: (args: TranslateResolverArgs) => Promise<TranslateResolverResponse> | TranslateResolverResponse;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* `missing` fills empty fields, `untranslated` also replaces fields that still hold the
|
|
21
|
+
* source copy, `all` re-translates everything.
|
|
22
|
+
*/
|
|
23
|
+
type TranslateMode = "all" | "missing" | "untranslated";
|
|
19
24
|
type TranslatorConfig = {
|
|
20
25
|
collections: CollectionSlug[];
|
|
21
26
|
disabled?: boolean;
|
|
@@ -27,9 +32,11 @@ type TranslatorConfig = {
|
|
|
27
32
|
* `missing` (default) fills empty fields and keeps existing translations and slugs;
|
|
28
33
|
* `all` re-translates every field.
|
|
29
34
|
*/
|
|
30
|
-
autoTranslateMode?:
|
|
35
|
+
autoTranslateMode?: TranslateMode;
|
|
31
36
|
/** adds the /admin/translations review view, its endpoints and the status collection (default true) */
|
|
32
37
|
review?: boolean;
|
|
38
|
+
/** flag translated fields written in another locale's language (default true) */
|
|
39
|
+
languageDetection?: boolean;
|
|
33
40
|
_options?: {
|
|
34
41
|
additionalTraverseRichText?: (args: {
|
|
35
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": {
|