@focus-reactive/payload-plugin-translator 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/shared/ui/StatusCounter/StatusCounter.js +4 -4
- package/dist/server/features/enqueue-translation/handler.js +9 -6
- package/dist/server/features/enqueue-translation/resolveTargetLocales.d.ts +6 -5
- package/dist/server/features/enqueue-translation/resolveTargetLocales.js +4 -10
- package/dist/server/features/translate-field/handler.js +3 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +6 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +8 -4
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +11 -1
- package/package.json +1 -1
|
@@ -9,10 +9,10 @@ const statusColorMap = {
|
|
|
9
9
|
failed: "red"
|
|
10
10
|
};
|
|
11
11
|
const statusLabelMap = {
|
|
12
|
-
pending: "
|
|
13
|
-
running: "
|
|
14
|
-
completed: "
|
|
15
|
-
failed: "
|
|
12
|
+
pending: "Queued",
|
|
13
|
+
running: "Translating",
|
|
14
|
+
completed: "Translated",
|
|
15
|
+
failed: "Failed"
|
|
16
16
|
};
|
|
17
17
|
export default function StatusCounter({ status, count, label, animated = false, size = "medium", className }) {
|
|
18
18
|
const displayLabel = label || statusLabelMap[status];
|
|
@@ -18,20 +18,23 @@ import { resolveTargetLocales } from "./resolveTargetLocales";
|
|
|
18
18
|
const { source_lng, target_lng, collection_slug, collection_id, select_all, strategy, publish_on_translation } = validationResult.data;
|
|
19
19
|
const collectionSlug = isCollectionAvailable(collection_slug, this.config.availableCollections);
|
|
20
20
|
if (!collectionSlug) return ServerResponse.badRequest("Content of this collection is not available for translation");
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
21
|
+
// A localization-less config has no valid target locale: a phantom locale would burn a provider
|
|
22
|
+
// call and corrupt data — orphaned rows on Mongo/SQLite, a locale-enum error on Postgres, or (with
|
|
23
|
+
// no localization at all) overwrite the single unlocalized field and wipe the source. Reject before
|
|
24
|
+
// anything is enqueued.
|
|
25
25
|
const knownLocales = extractLocaleCodes(req.payload.config?.localization);
|
|
26
|
+
if (!knownLocales) return ServerResponse.badRequest("Localization is not enabled in this Payload config; there are no target locales to translate into");
|
|
27
|
+
// Normalize the scalar-or-array target into the concrete locales to fan out to: de-dup, exclude the
|
|
28
|
+
// source, and drop locales that are not configured.
|
|
26
29
|
const { targets, droppedUnknown } = resolveTargetLocales({
|
|
27
30
|
target_lng,
|
|
28
31
|
source_lng,
|
|
29
32
|
knownLocales
|
|
30
33
|
});
|
|
31
34
|
if (droppedUnknown.length > 0) {
|
|
32
|
-
req.payload.logger?.warn(`[payload-plugin-translator] enqueue on "${collectionSlug}": ignoring unknown target locale(s) ${droppedUnknown.join(", ")} (configured locales: ${
|
|
35
|
+
req.payload.logger?.warn(`[payload-plugin-translator] enqueue on "${collectionSlug}": ignoring unknown target locale(s) ${droppedUnknown.join(", ")} (configured locales: ${[
|
|
33
36
|
...knownLocales
|
|
34
|
-
].join(", ")
|
|
37
|
+
].join(", ")}).`);
|
|
35
38
|
}
|
|
36
39
|
if (targets.length === 0) return ServerResponse.badRequest("No valid target locales to translate into (all requested locales were the source or unknown)");
|
|
37
40
|
const collectionIds = select_all ? await getAllCollectionIds(req.payload, collectionSlug) : collection_id;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export type ResolvedTargetLocales = {
|
|
5
5
|
/** The concrete locales to fan out to — de-duplicated, source excluded, unknown removed. */
|
|
6
6
|
targets: string[];
|
|
7
|
-
/** Requested locales that are not configured (dropped)
|
|
7
|
+
/** Requested locales that are not configured (dropped). */
|
|
8
8
|
droppedUnknown: string[];
|
|
9
9
|
/** Whether the source locale was requested as a target and excluded. */
|
|
10
10
|
droppedSource: boolean;
|
|
@@ -12,18 +12,19 @@ export type ResolvedTargetLocales = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Normalize the enqueue `target_lng` input (scalar or array) into the concrete list of target locales
|
|
14
14
|
* to translate into. Applies, in order: array-coercion, de-duplication (first-seen order preserved),
|
|
15
|
-
* source-locale exclusion, and
|
|
15
|
+
* source-locale exclusion, and dropping unknown (unconfigured) locales.
|
|
16
16
|
*
|
|
17
17
|
* De-dup and unknown-dropping are the manual-enqueue counterparts of the auto-translate policy filter:
|
|
18
18
|
* the runner only supersedes against already-stored jobs, so duplicates within one enqueue must be
|
|
19
19
|
* collapsed here; and an unknown locale must never reach the pipeline — it burns a provider call and
|
|
20
20
|
* either errors on a Postgres locale enum or writes orphaned, invisible data on Mongo/SQLite.
|
|
21
21
|
*
|
|
22
|
-
* @param knownLocales - the configured locale codes
|
|
23
|
-
*
|
|
22
|
+
* @param knownLocales - the configured locale codes. Never null: the caller must reject a
|
|
23
|
+
* localization-less config before reaching here (translating without localization has no valid
|
|
24
|
+
* target and would corrupt data), so "no localization" is not representable as an input.
|
|
24
25
|
*/
|
|
25
26
|
export declare function resolveTargetLocales(args: {
|
|
26
27
|
target_lng: string | string[];
|
|
27
28
|
source_lng: string;
|
|
28
|
-
knownLocales: Set<string
|
|
29
|
+
knownLocales: Set<string>;
|
|
29
30
|
}): ResolvedTargetLocales;
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
*/ /**
|
|
4
4
|
* Normalize the enqueue `target_lng` input (scalar or array) into the concrete list of target locales
|
|
5
5
|
* to translate into. Applies, in order: array-coercion, de-duplication (first-seen order preserved),
|
|
6
|
-
* source-locale exclusion, and
|
|
6
|
+
* source-locale exclusion, and dropping unknown (unconfigured) locales.
|
|
7
7
|
*
|
|
8
8
|
* De-dup and unknown-dropping are the manual-enqueue counterparts of the auto-translate policy filter:
|
|
9
9
|
* the runner only supersedes against already-stored jobs, so duplicates within one enqueue must be
|
|
10
10
|
* collapsed here; and an unknown locale must never reach the pipeline — it burns a provider call and
|
|
11
11
|
* either errors on a Postgres locale enum or writes orphaned, invisible data on Mongo/SQLite.
|
|
12
12
|
*
|
|
13
|
-
* @param knownLocales - the configured locale codes
|
|
14
|
-
*
|
|
13
|
+
* @param knownLocales - the configured locale codes. Never null: the caller must reject a
|
|
14
|
+
* localization-less config before reaching here (translating without localization has no valid
|
|
15
|
+
* target and would corrupt data), so "no localization" is not representable as an input.
|
|
15
16
|
*/ export function resolveTargetLocales(args) {
|
|
16
17
|
const { target_lng, source_lng, knownLocales } = args;
|
|
17
18
|
const requested = Array.isArray(target_lng) ? target_lng : [
|
|
@@ -22,13 +23,6 @@
|
|
|
22
23
|
];
|
|
23
24
|
const droppedSource = deduped.includes(source_lng);
|
|
24
25
|
const withoutSource = deduped.filter((target)=>target !== source_lng);
|
|
25
|
-
if (!knownLocales) {
|
|
26
|
-
return {
|
|
27
|
-
targets: withoutSource,
|
|
28
|
-
droppedUnknown: [],
|
|
29
|
-
droppedSource
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
26
|
const droppedUnknown = withoutSource.filter((target)=>!knownLocales.has(target));
|
|
33
27
|
const targets = withoutSource.filter((target)=>knownLocales.has(target));
|
|
34
28
|
return {
|
|
@@ -62,6 +62,9 @@ const noop = (value, level, message)=>({
|
|
|
62
62
|
if (resolution.status === "not-translatable") {
|
|
63
63
|
return ServerResponse.success(noop(sourceValue, "info", "Nothing to translate in this field"));
|
|
64
64
|
}
|
|
65
|
+
if (resolution.status === "excluded") {
|
|
66
|
+
return ServerResponse.success(noop(sourceValue, "info", "This field is excluded from translation"));
|
|
67
|
+
}
|
|
65
68
|
// No `strategy`/`targetData`: a per-field translate is an explicit "translate this field now",
|
|
66
69
|
// so `translateContent` always overwrites (its default). skip_existing has no meaning here.
|
|
67
70
|
const translated = await translateContent({
|
|
@@ -6,6 +6,10 @@ import type { FieldLike } from "../../../core/kernel/field-traversal";
|
|
|
6
6
|
* are ready for `translateContent`, and `fieldName` is the key to unwrap the result.
|
|
7
7
|
* - `not-found` — the path resolves to no field at all (a typo) → caller returns 400.
|
|
8
8
|
* - `not-translatable` — resolves to a field that isn't a text-like leaf → caller no-ops.
|
|
9
|
+
* - `excluded` — resolves to a translatable leaf that opted out via `withFieldTranslation({ exclude })`
|
|
10
|
+
* → caller no-ops. Kept distinct from `not-translatable` so the notice can say *why* (a deliberate
|
|
11
|
+
* opt-out, not a wrong type), and because this route must honor the same exclude the whole-document
|
|
12
|
+
* path honors via `isTranslatableLeaf` — otherwise naming the path directly bypasses the opt-out.
|
|
9
13
|
* - `inside-blocks` — the path descends through a polymorphic `blocks` field that couldn't be
|
|
10
14
|
* resolved: no `doc` was supplied to read the element's `blockType` from → caller no-ops.
|
|
11
15
|
* - `localized-list-ancestor` — the path descends through a `localized` `blocks`/`array` field,
|
|
@@ -21,6 +25,8 @@ export type FieldSubtreeResolution = {
|
|
|
21
25
|
status: "not-found";
|
|
22
26
|
} | {
|
|
23
27
|
status: "not-translatable";
|
|
28
|
+
} | {
|
|
29
|
+
status: "excluded";
|
|
24
30
|
} | {
|
|
25
31
|
status: "inside-blocks";
|
|
26
32
|
} | {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTranslatableField } from "../../shared";
|
|
1
|
+
import { isFieldExcludedFromTranslation, isTranslatableField } from "../../shared";
|
|
2
2
|
import { findFieldByPath } from "../../../core/kernel/field-traversal";
|
|
3
3
|
/**
|
|
4
4
|
* Map a declared `fieldPath` to the `{ schema, sourceData }` pair `translateContent` expects.
|
|
@@ -17,7 +17,13 @@ import { findFieldByPath } from "../../../core/kernel/field-traversal";
|
|
|
17
17
|
const result = findFieldByPath(rootFields, segments, doc);
|
|
18
18
|
switch(result.status){
|
|
19
19
|
case "leaf":
|
|
20
|
-
|
|
20
|
+
if (!isTranslatableField(result.field)) return {
|
|
21
|
+
status: "not-translatable"
|
|
22
|
+
};
|
|
23
|
+
if (isFieldExcludedFromTranslation(result.field)) return {
|
|
24
|
+
status: "excluded"
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
21
27
|
status: "resolved",
|
|
22
28
|
schema: [
|
|
23
29
|
result.field
|
|
@@ -26,8 +32,6 @@ import { findFieldByPath } from "../../../core/kernel/field-traversal";
|
|
|
26
32
|
[result.field.name]: value
|
|
27
33
|
},
|
|
28
34
|
fieldName: result.field.name
|
|
29
|
-
} : {
|
|
30
|
-
status: "not-translatable"
|
|
31
35
|
};
|
|
32
36
|
case "container":
|
|
33
37
|
return {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const endpointKey = (endpoint)=>`${endpoint.method} ${endpoint.path}`;
|
|
2
|
+
/**
|
|
3
|
+
* Identity of a collection admin component for dedup: its slot plus its module path (+ export name).
|
|
4
|
+
* `serverProps` (per-collection data) are excluded, so the same control registered by a level declared
|
|
5
|
+
* twice collapses to one — while genuinely different controls (distinct path or slot) do not.
|
|
6
|
+
*/ const componentKey = (slot, component)=>`${slot} ${component.path}#${component.exportName ?? ""}`;
|
|
2
7
|
function attachToSlot(collection, slot, component) {
|
|
3
8
|
if (!collection.admin) collection.admin = {};
|
|
4
9
|
if (!collection.admin.components) collection.admin.components = {};
|
|
@@ -92,8 +97,13 @@ function attachToSlot(collection, slot, component) {
|
|
|
92
97
|
const managed = new Set(this.collections.map((collection)=>collection.slug));
|
|
93
98
|
config.collections?.forEach((collection)=>{
|
|
94
99
|
if (!managed.has(collection.slug)) return;
|
|
100
|
+
const seen = new Set();
|
|
95
101
|
for (const { slot, make } of this.collectionComponents){
|
|
96
|
-
|
|
102
|
+
const component = make(collection);
|
|
103
|
+
const key = componentKey(slot, component);
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
attachToSlot(collection, slot, component);
|
|
97
107
|
}
|
|
98
108
|
});
|
|
99
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@focus-reactive/payload-plugin-translator",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|