@justanarthur/payload-plugin-translator 3.1.1 → 3.2.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 +7 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +128 -16
- package/dist/index.d.ts +10 -1
- package/dist/index.js +42 -5
- package/dist/jobs.js +5 -4
- package/dist/rsc.js +15 -3
- package/dist/shared/{chunk-15n5q7wd.js → chunk-4ftfmxgs.js} +110 -24
- package/dist/shared/{chunk-9d433kmv.js → chunk-d0n3f8ad.js} +36 -4
- package/dist/shared/{chunk-p03jz82h.js → chunk-hkx0bzb6.js} +26 -13
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ 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
83
|
| `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
84
|
| `disabled` | `boolean` | no | Skip the plugin entirely (no overrides, no jobs). Useful in tests. |
|
|
85
85
|
| `_options.additionalTraverseRichText` | function | no | Hook to extend rich-text traversal — see below. |
|
|
@@ -128,6 +128,12 @@ With `review` on, **Translations** appears in the admin nav (`/admin/translation
|
|
|
128
128
|
last auto-translate job and its error. Actions: *Translate missing fields*, *Re-translate all*,
|
|
129
129
|
*Mark reviewed*.
|
|
130
130
|
|
|
131
|
+
- **Bulk fix**: *Translate untranslated* queues one translate workflow per incomplete document,
|
|
132
|
+
for all locales or one, in `untranslated` mode (empty fields and fields still holding the source
|
|
133
|
+
copy; existing translations and slugs stay). Locales a pending job already covers are skipped.
|
|
134
|
+
The overview shows queued, running and failed jobs, and *Run a batch now* processes a few without
|
|
135
|
+
waiting for the job runner. Needs `autoTranslate` on, since that registers the jobs.
|
|
136
|
+
|
|
131
137
|
The view only lists collections and globals the signed-in user can read, and the endpoints behind
|
|
132
138
|
the actions require a user.
|
|
133
139
|
|
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 and untranslated fields in ${scope}? Existing 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,7 +65,7 @@ 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;
|
|
62
71
|
_options?: {
|
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-hkx0bzb6.js";
|
|
12
|
+
import"./shared/chunk-d0n3f8ad.js";
|
|
11
13
|
import {
|
|
12
14
|
readLocales,
|
|
13
|
-
parseEntityKey
|
|
14
|
-
|
|
15
|
+
parseEntityKey,
|
|
16
|
+
hasTranslateWorkflow,
|
|
17
|
+
queueBulkTranslation,
|
|
18
|
+
runQueuedTranslations
|
|
19
|
+
} from "./shared/chunk-4ftfmxgs.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-hkx0bzb6.js";
|
|
7
|
+
import {
|
|
8
|
+
TRANSLATE_TASK_SLUG2,
|
|
9
|
+
TRANSLATE_WORKFLOW_SLUG2
|
|
10
|
+
} from "./shared/chunk-d0n3f8ad.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-d0n3f8ad.js";
|
|
2
2
|
import {
|
|
3
3
|
readLocales,
|
|
4
4
|
loadCollectionReview,
|
|
5
5
|
loadGlobalsReview,
|
|
6
|
-
loadEntityLocaleReview
|
|
7
|
-
|
|
6
|
+
loadEntityLocaleReview,
|
|
7
|
+
loadJobsProgress
|
|
8
|
+
} from "./shared/chunk-4ftfmxgs.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";
|
|
@@ -456,6 +458,10 @@ var reviewStyles = `
|
|
|
456
458
|
.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
459
|
.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
460
|
|
|
461
|
+
.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); }
|
|
462
|
+
.tr__bulk-main, .tr__bulk-progress { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
463
|
+
.tr__bulk .btn { margin: 0; }
|
|
464
|
+
.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
465
|
.tr__card { border: 1px solid var(--tr-line); border-radius: var(--style-radius-m); overflow: auto; background: var(--theme-elevation-0); }
|
|
460
466
|
.tr table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
|
|
461
467
|
.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; }
|
|
@@ -628,6 +634,7 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
|
|
|
628
634
|
});
|
|
629
635
|
};
|
|
630
636
|
const adminLocale = req.locale;
|
|
637
|
+
const progress = entity || !req.user ? null : await loadJobsProgress(req);
|
|
631
638
|
const body = await content();
|
|
632
639
|
req.locale = adminLocale;
|
|
633
640
|
return /* @__PURE__ */ jsxs3(DefaultTemplate, {
|
|
@@ -685,6 +692,11 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
|
|
|
685
692
|
children: "Only incomplete"
|
|
686
693
|
})
|
|
687
694
|
]
|
|
695
|
+
}),
|
|
696
|
+
req.user && tab && /* @__PURE__ */ jsx3(BulkActions, {
|
|
697
|
+
locales: targetLocales,
|
|
698
|
+
progress,
|
|
699
|
+
tab
|
|
688
700
|
})
|
|
689
701
|
]
|
|
690
702
|
}),
|
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
samePlaceholders,
|
|
3
|
-
|
|
3
|
+
plainText,
|
|
4
|
+
looksUntranslated,
|
|
5
|
+
TRANSLATE_WORKFLOW_SLUG2,
|
|
4
6
|
collectTranslatableFields,
|
|
5
7
|
entityKey,
|
|
6
8
|
sourceHash
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-d0n3f8ad.js";
|
|
8
10
|
import {
|
|
9
11
|
TRANSLATION_STATUS_SLUG2
|
|
10
12
|
} from "./chunk-31qgv1zk.js";
|
|
11
13
|
|
|
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
14
|
// src/review/computeStatus.ts
|
|
32
15
|
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
33
16
|
var classify = (field, sourceText, targetText) => {
|
|
@@ -35,8 +18,7 @@ var classify = (field, sourceText, targetText) => {
|
|
|
35
18
|
return "missing";
|
|
36
19
|
if (field.type === "json" && !samePlaceholders(sourceText, targetText))
|
|
37
20
|
return "placeholders";
|
|
38
|
-
|
|
39
|
-
if (same && field.type !== "slug" && normalize(sourceText).length > 3 && !isOpaqueText(sourceText) && /\s/.test(normalize(sourceText)))
|
|
21
|
+
if (field.type !== "slug" && looksUntranslated(sourceText, targetText))
|
|
40
22
|
return "identical";
|
|
41
23
|
return "ok";
|
|
42
24
|
};
|
|
@@ -250,4 +232,108 @@ var loadLastJob = async (req, { collectionSlug, globalSlug, id }, locale) => {
|
|
|
250
232
|
}
|
|
251
233
|
};
|
|
252
234
|
|
|
253
|
-
|
|
235
|
+
// src/review/bulk.ts
|
|
236
|
+
var JOBS_SLUG = "payload-jobs";
|
|
237
|
+
var pendingWhere = {
|
|
238
|
+
and: [
|
|
239
|
+
{ workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } },
|
|
240
|
+
{ completedAt: { exists: false } },
|
|
241
|
+
{ hasError: { not_equals: true } }
|
|
242
|
+
]
|
|
243
|
+
};
|
|
244
|
+
var hasTranslateWorkflow = (req) => Boolean(req.payload.config.jobs?.workflows?.some((workflow) => workflow.slug === TRANSLATE_WORKFLOW_SLUG2)) && Boolean(req.payload.collections[JOBS_SLUG]);
|
|
245
|
+
var loadJobsProgress = async (req) => {
|
|
246
|
+
if (!hasTranslateWorkflow(req))
|
|
247
|
+
return null;
|
|
248
|
+
const count = (where) => req.payload.count({ collection: JOBS_SLUG, where, overrideAccess: true, req }).then(({ totalDocs }) => totalDocs);
|
|
249
|
+
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
250
|
+
const [pending, running, failed] = await Promise.all([
|
|
251
|
+
count(pendingWhere),
|
|
252
|
+
count({ and: [...pendingWhere.and ?? [], { processing: { equals: true } }] }),
|
|
253
|
+
count({ and: [{ workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } }, { hasError: { equals: true } }, { updatedAt: { greater_than: dayAgo } }] })
|
|
254
|
+
]);
|
|
255
|
+
return { pending, running, failed };
|
|
256
|
+
};
|
|
257
|
+
var loadPendingPairs = async (req) => {
|
|
258
|
+
const { docs } = await req.payload.find({
|
|
259
|
+
collection: JOBS_SLUG,
|
|
260
|
+
where: pendingWhere,
|
|
261
|
+
pagination: false,
|
|
262
|
+
depth: 0,
|
|
263
|
+
overrideAccess: true,
|
|
264
|
+
req
|
|
265
|
+
});
|
|
266
|
+
const pairs = new Set;
|
|
267
|
+
for (const { input } of docs) {
|
|
268
|
+
if (!input)
|
|
269
|
+
continue;
|
|
270
|
+
const key = entityKey({ collectionSlug: input.collection, globalSlug: input.global, id: input.id });
|
|
271
|
+
for (const locale of input.toLocales ?? (input.toLocale ? [input.toLocale] : []))
|
|
272
|
+
pairs.add(`${key}|${locale}`);
|
|
273
|
+
}
|
|
274
|
+
return pairs;
|
|
275
|
+
};
|
|
276
|
+
var incompleteLocales = (entity, locales) => locales.filter((locale) => {
|
|
277
|
+
const review = entity.locales[locale];
|
|
278
|
+
return review && (review.summary.coverage < 100 || review.summary.slugMissing);
|
|
279
|
+
});
|
|
280
|
+
var queueBulkTranslation = async ({ req, tab, locale, mode }) => {
|
|
281
|
+
const pluginConfig = req.payload.config.custom?.translator;
|
|
282
|
+
const { defaultLocale, targetLocales } = readLocales(req);
|
|
283
|
+
const locales = locale ? [locale] : targetLocales;
|
|
284
|
+
const resolver = pluginConfig.resolvers[0]?.key;
|
|
285
|
+
const pending = await loadPendingPairs(req);
|
|
286
|
+
let queuedDocuments = 0;
|
|
287
|
+
let queuedLocales = 0;
|
|
288
|
+
let skippedPending = 0;
|
|
289
|
+
const queue = async (entity) => {
|
|
290
|
+
const toLocales = incompleteLocales(entity, locales).filter((each) => {
|
|
291
|
+
const already = pending.has(`${entity.key}|${each}`);
|
|
292
|
+
if (already)
|
|
293
|
+
skippedPending++;
|
|
294
|
+
return !already;
|
|
295
|
+
});
|
|
296
|
+
if (!toLocales.length)
|
|
297
|
+
return;
|
|
298
|
+
await req.payload.jobs.queue({
|
|
299
|
+
req,
|
|
300
|
+
workflow: TRANSLATE_WORKFLOW_SLUG2,
|
|
301
|
+
input: {
|
|
302
|
+
id: entity.id,
|
|
303
|
+
collection: entity.collectionSlug,
|
|
304
|
+
global: entity.globalSlug,
|
|
305
|
+
updatedAt: new Date().toISOString(),
|
|
306
|
+
fromLocale: defaultLocale,
|
|
307
|
+
toLocales,
|
|
308
|
+
resolver,
|
|
309
|
+
mode
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
queuedDocuments++;
|
|
313
|
+
queuedLocales += toLocales.length;
|
|
314
|
+
};
|
|
315
|
+
if (tab === "globals") {
|
|
316
|
+
const { entities } = await loadGlobalsReview({ req, globalSlugs: pluginConfig.globals });
|
|
317
|
+
for (const entity of entities)
|
|
318
|
+
await queue(entity);
|
|
319
|
+
} else {
|
|
320
|
+
for (let page = 1;; page++) {
|
|
321
|
+
const review = await loadCollectionReview({ req, collectionSlug: tab, page, limit: 50 });
|
|
322
|
+
for (const entity of review.entities)
|
|
323
|
+
await queue(entity);
|
|
324
|
+
if (page >= review.totalPages)
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { queuedDocuments, queuedLocales, skippedPending };
|
|
329
|
+
};
|
|
330
|
+
var runQueuedTranslations = async (req, limit = 3) => {
|
|
331
|
+
await req.payload.jobs.run({
|
|
332
|
+
limit,
|
|
333
|
+
where: { workflowSlug: { equals: TRANSLATE_WORKFLOW_SLUG2 } },
|
|
334
|
+
req
|
|
335
|
+
});
|
|
336
|
+
return loadJobsProgress(req);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export { readLocales, loadCollectionReview, loadGlobalsReview, parseEntityKey, loadEntityLocaleReview, hasTranslateWorkflow, loadJobsProgress, queueBulkTranslation, runQueuedTranslations };
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/jobs/constants.ts
|
|
2
|
+
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
3
|
+
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
4
|
+
|
|
1
5
|
// src/utils/placeholders.ts
|
|
2
6
|
var placeholdersOf = (value) => (value.match(/\{\s*[\w.]+\s*(?:,[^{}]*)?\}/g) ?? []).map((each) => each.replace(/\s+/g, "")).sort();
|
|
3
7
|
var samePlaceholders = (source, translated) => placeholdersOf(source).join("|") === placeholdersOf(translated).join("|");
|
|
@@ -46,6 +50,32 @@ var isOpaqueText = (value) => {
|
|
|
46
50
|
return /^([a-z][a-z0-9+.-]*:|\/\/|\/|#|www\.)/i.test(text) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) || !/\p{L}/u.test(text);
|
|
47
51
|
};
|
|
48
52
|
|
|
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
|
+
// src/utils/looksUntranslated.ts
|
|
73
|
+
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
74
|
+
var looksUntranslated = (source, target) => {
|
|
75
|
+
const sourceText = normalize(plainText(source));
|
|
76
|
+
return sourceText.length > 3 && /\s/.test(sourceText) && !isOpaqueText(sourceText) && sourceText === normalize(plainText(target));
|
|
77
|
+
};
|
|
78
|
+
|
|
49
79
|
// src/utils/sanitizeSlug.ts
|
|
50
80
|
var sanitizeSlug = (text) => text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
51
81
|
|
|
@@ -95,6 +125,7 @@ var traverseFields = (args) => {
|
|
|
95
125
|
const {
|
|
96
126
|
dataFrom,
|
|
97
127
|
emptyOnly,
|
|
128
|
+
retranslateIdentical,
|
|
98
129
|
fields,
|
|
99
130
|
localizedParent,
|
|
100
131
|
path,
|
|
@@ -106,6 +137,7 @@ var traverseFields = (args) => {
|
|
|
106
137
|
const { additionalTraverseRichText } = _options ?? {};
|
|
107
138
|
const siblingDataFrom = args.siblingDataFrom ?? dataFrom;
|
|
108
139
|
const siblingDataTranslated = args.siblingDataTranslated ?? translatedData;
|
|
140
|
+
const keepsTarget = (source, target) => Boolean(emptyOnly) && hasText(target) && !(retranslateIdentical && looksUntranslated(source, target));
|
|
109
141
|
const recurse = (overrides) => traverseFields({ ...args, siblingDataFrom, siblingDataTranslated, ...overrides });
|
|
110
142
|
for (const field of fields) {
|
|
111
143
|
switch (field.type) {
|
|
@@ -226,7 +258,7 @@ var traverseFields = (args) => {
|
|
|
226
258
|
onField?.({ path: keyPath, type: "json", source: value, target: previous });
|
|
227
259
|
if (!value.trim())
|
|
228
260
|
continue;
|
|
229
|
-
if (
|
|
261
|
+
if (keepsTarget(value, previous))
|
|
230
262
|
continue;
|
|
231
263
|
target[key] = value;
|
|
232
264
|
if (isOpaqueText(value))
|
|
@@ -266,7 +298,7 @@ var traverseFields = (args) => {
|
|
|
266
298
|
if (typeof value !== "string" || !value.trim())
|
|
267
299
|
break;
|
|
268
300
|
const hasCurrent = typeof current === "string" && current.trim().length > 0;
|
|
269
|
-
if (hasCurrent
|
|
301
|
+
if (isSlug ? hasCurrent : keepsTarget(value, current))
|
|
270
302
|
break;
|
|
271
303
|
if (isOpaqueText(value)) {
|
|
272
304
|
siblingDataTranslated[field.name] = value;
|
|
@@ -304,7 +336,7 @@ var traverseFields = (args) => {
|
|
|
304
336
|
onField?.({ path: richTextPath, type: "richText", source: richTextDataFrom, target: current });
|
|
305
337
|
if (!richTextDataFrom || !hasText(richTextDataFrom))
|
|
306
338
|
break;
|
|
307
|
-
if (
|
|
339
|
+
if (keepsTarget(richTextDataFrom, current))
|
|
308
340
|
break;
|
|
309
341
|
const isSlate = Array.isArray(richTextDataFrom);
|
|
310
342
|
const isLexical = typeof richTextDataFrom === "object" && "root" in richTextDataFrom;
|
|
@@ -356,4 +388,4 @@ var entityKey = ({ collectionSlug, globalSlug, id }) => globalSlug ? `global:${g
|
|
|
356
388
|
import { createHash } from "node:crypto";
|
|
357
389
|
var sourceHash = (fields) => createHash("sha1").update(JSON.stringify(fields.map(({ path, source }) => [path, source]))).digest("hex");
|
|
358
390
|
|
|
359
|
-
export { samePlaceholders,
|
|
391
|
+
export { samePlaceholders, plainText, looksUntranslated, traverseFields, TRANSLATE_TASK_SLUG2, TRANSLATE_WORKFLOW_SLUG2, collectTranslatableFields, entityKey, sourceHash };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
samePlaceholders,
|
|
3
3
|
traverseFields,
|
|
4
|
+
TRANSLATE_WORKFLOW_SLUG2,
|
|
4
5
|
collectTranslatableFields,
|
|
5
6
|
entityKey,
|
|
6
7
|
sourceHash
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-d0n3f8ad.js";
|
|
8
9
|
import {
|
|
9
10
|
TRANSLATION_STATUS_SLUG2
|
|
10
11
|
} from "./chunk-31qgv1zk.js";
|
|
@@ -127,6 +128,7 @@ var translateOperation2 = async (args) => {
|
|
|
127
128
|
traverseFields({
|
|
128
129
|
dataFrom,
|
|
129
130
|
emptyOnly: args.emptyOnly,
|
|
131
|
+
retranslateIdentical: args.retranslateIdentical,
|
|
130
132
|
fields: config.fields,
|
|
131
133
|
translatedData,
|
|
132
134
|
valuesToTranslate,
|
|
@@ -178,7 +180,7 @@ ${valuesToTranslate.map((v) => ` ${v.path ?? "(unknown)"}: ${preview(v.value)}`
|
|
|
178
180
|
${summary.join(`
|
|
179
181
|
`)}` : "")
|
|
180
182
|
});
|
|
181
|
-
if (args.update) {
|
|
183
|
+
if (args.update && valuesToTranslate.length > 0) {
|
|
182
184
|
const { _locale, _parent_id, createdAt, updatedAt, ...data } = translatedData;
|
|
183
185
|
await updateEntity({
|
|
184
186
|
collectionSlug,
|
|
@@ -194,16 +196,13 @@ ${summary.join(`
|
|
|
194
196
|
result = {
|
|
195
197
|
success: true,
|
|
196
198
|
translatedData,
|
|
199
|
+
translatedCount: valuesToTranslate.length,
|
|
197
200
|
dataFrom
|
|
198
201
|
};
|
|
199
202
|
}
|
|
200
203
|
return result;
|
|
201
204
|
};
|
|
202
205
|
|
|
203
|
-
// src/jobs/constants.ts
|
|
204
|
-
var TRANSLATE_TASK_SLUG2 = "translateEntityToLocale";
|
|
205
|
-
var TRANSLATE_WORKFLOW_SLUG2 = "translateEntityToLocales";
|
|
206
|
-
|
|
207
206
|
// src/jobs/createAutoTranslateCollectionHook.ts
|
|
208
207
|
function createAutoTranslateCollectionHook2(options) {
|
|
209
208
|
const {
|
|
@@ -432,6 +431,13 @@ var recordTranslationStatus = async ({ req, collectionSlug, globalSlug, id, loca
|
|
|
432
431
|
await upsertTranslationStatus(req, entityKey({ collectionSlug, globalSlug, id }), locale, reviewedBy ? { reviewedHash: hash, reviewedAt: now, reviewedBy } : { sourceHash: hash, translatedAt: now });
|
|
433
432
|
};
|
|
434
433
|
|
|
434
|
+
// src/utils/translateMode.ts
|
|
435
|
+
var translateModeArgs = (mode) => ({
|
|
436
|
+
emptyOnly: mode !== "all",
|
|
437
|
+
retranslateIdentical: mode === "untranslated"
|
|
438
|
+
});
|
|
439
|
+
var isTranslateMode = (value) => value === "all" || value === "missing" || value === "untranslated";
|
|
440
|
+
|
|
435
441
|
// src/jobs/createTranslateTask.ts
|
|
436
442
|
function createTranslateTask2(options = {}) {
|
|
437
443
|
const { slug = "translateEntityToLocale" } = options;
|
|
@@ -451,7 +457,7 @@ function createTranslateTask2(options = {}) {
|
|
|
451
457
|
handler: async (args) => {
|
|
452
458
|
const { input, job, req } = args;
|
|
453
459
|
const { id, collection, global, fromLocale, toLocale, resolver: inputResolver } = input;
|
|
454
|
-
const mode = input.mode
|
|
460
|
+
const mode = isTranslateMode(input.mode) ? input.mode : readAutoTranslateMode(req);
|
|
455
461
|
if (!collection && !global) {
|
|
456
462
|
throw new Error("translateTask: either `collection` or `global` must be provided");
|
|
457
463
|
}
|
|
@@ -470,7 +476,7 @@ function createTranslateTask2(options = {}) {
|
|
|
470
476
|
req,
|
|
471
477
|
collectionSlug: collection,
|
|
472
478
|
globalSlug: global,
|
|
473
|
-
|
|
479
|
+
...translateModeArgs(mode),
|
|
474
480
|
id,
|
|
475
481
|
locale: toLocale,
|
|
476
482
|
localeFrom: fromLocale,
|
|
@@ -507,6 +513,10 @@ function createTranslateTask2(options = {}) {
|
|
|
507
513
|
});
|
|
508
514
|
return { output: { success: true } };
|
|
509
515
|
}
|
|
516
|
+
if (result.translatedCount === 0) {
|
|
517
|
+
req.payload.logger.info({ jobId: job.id, msg: `[translate] ${entityLabel}#${id ?? global} → ${toLocale}: nothing to translate` });
|
|
518
|
+
return { output: { success: true } };
|
|
519
|
+
}
|
|
510
520
|
const translated = result.translatedData ?? {};
|
|
511
521
|
const { _locale: _dropLocale, _parent_id: _dropParent, updatedAt: _dropUpdatedAt, createdAt: _dropCreatedAt, ...data } = translated;
|
|
512
522
|
req.payload.logger.info({
|
|
@@ -558,7 +568,8 @@ function readFirstResolverKey3(req) {
|
|
|
558
568
|
return typeof first?.key === "string" ? first.key : "";
|
|
559
569
|
}
|
|
560
570
|
function readAutoTranslateMode(req) {
|
|
561
|
-
|
|
571
|
+
const mode = req.payload.config.custom?.translator?.autoTranslateMode;
|
|
572
|
+
return isTranslateMode(mode) ? mode : "missing";
|
|
562
573
|
}
|
|
563
574
|
|
|
564
575
|
// src/jobs/createTranslateWorkflow.ts
|
|
@@ -574,11 +585,12 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
574
585
|
{ name: "fromLocale", type: "text", required: true },
|
|
575
586
|
{ name: "toLocales", type: "json", required: false },
|
|
576
587
|
{ name: "toLocale", type: "text", required: false },
|
|
577
|
-
{ name: "resolver", type: "text", required: false }
|
|
588
|
+
{ name: "resolver", type: "text", required: false },
|
|
589
|
+
{ name: "mode", type: "text", required: false }
|
|
578
590
|
],
|
|
579
591
|
handler: async (args) => {
|
|
580
592
|
const { job, req, tasks } = args;
|
|
581
|
-
const { id, collection, global, fromLocale, toLocales, toLocale, updatedAt, resolver } = job.input;
|
|
593
|
+
const { id, collection, global, fromLocale, toLocales, toLocale, updatedAt, resolver, mode } = job.input;
|
|
582
594
|
if (!collection && !global) {
|
|
583
595
|
throw new Error("translateWorkflow: either `collection` or `global` must be provided");
|
|
584
596
|
}
|
|
@@ -612,7 +624,8 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
612
624
|
global,
|
|
613
625
|
fromLocale,
|
|
614
626
|
toLocale: target,
|
|
615
|
-
resolver
|
|
627
|
+
resolver,
|
|
628
|
+
mode
|
|
616
629
|
}
|
|
617
630
|
});
|
|
618
631
|
}
|
|
@@ -620,4 +633,4 @@ function createTranslateWorkflow2(options = {}) {
|
|
|
620
633
|
};
|
|
621
634
|
}
|
|
622
635
|
|
|
623
|
-
export { findEntityWithConfig, translateOperation2,
|
|
636
|
+
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,7 +32,7 @@ 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;
|
|
33
38
|
_options?: {
|
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.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|