@focus-reactive/payload-plugin-translator 0.5.1 → 0.6.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.
Files changed (64) hide show
  1. package/README.md +46 -8
  2. package/dist/client/entities/translation/api/mutations/useTranslateField.d.ts +22 -0
  3. package/dist/client/entities/translation/api/mutations/useTranslateField.js +31 -0
  4. package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.d.ts +3 -3
  5. package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +15 -13
  6. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts +2 -2
  7. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.js +10 -10
  8. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.d.ts +1 -1
  9. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js +9 -9
  10. package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.d.ts +2 -2
  11. package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +10 -8
  12. package/dist/client/shared/lib/assets/icons/SendIcon.d.ts +1 -0
  13. package/dist/client/shared/lib/assets/icons/SendIcon.js +17 -0
  14. package/dist/client/shared/ui/Button/Button.d.ts +2 -1
  15. package/dist/client/shared/ui/Button/Button.js +1 -1
  16. package/dist/client/shared/ui/Button/styles.module.scss +26 -3
  17. package/dist/client/widgets/translate-field-control/index.d.ts +1 -0
  18. package/dist/client/widgets/translate-field-control/index.js +3 -0
  19. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.d.ts +26 -0
  20. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.d.ts +13 -0
  21. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.js +14 -0
  22. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.js +204 -0
  23. package/dist/client/widgets/translate-field-control/ui/styles.module.scss +48 -0
  24. package/dist/field-actions.d.ts +25 -0
  25. package/dist/field-actions.js +23 -0
  26. package/dist/field-config.d.ts +26 -26
  27. package/dist/field-config.js +14 -37
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/plugin.d.ts +1 -1
  31. package/dist/plugin.js +3 -1
  32. package/dist/server/features/translate-field/handler.d.ts +17 -0
  33. package/dist/server/features/translate-field/handler.js +85 -0
  34. package/dist/server/features/translate-field/index.d.ts +6 -0
  35. package/dist/server/features/translate-field/index.js +5 -0
  36. package/dist/server/features/translate-field/model.d.ts +62 -0
  37. package/dist/server/features/translate-field/model.js +26 -0
  38. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +15 -7
  39. package/dist/server/features/translate-field/resolveFieldSubtree.js +14 -8
  40. package/dist/server/features/translate-field/route.d.ts +12 -0
  41. package/dist/server/features/translate-field/route.js +18 -0
  42. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -0
  43. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +4 -0
  44. package/dist/server/modules/translation-levels/fieldLevel.d.ts +23 -0
  45. package/dist/server/modules/translation-levels/fieldLevel.js +37 -0
  46. package/dist/server/modules/translation-levels/index.d.ts +1 -0
  47. package/dist/server/modules/translation-levels/index.js +1 -0
  48. package/dist/server/modules/translation-levels/types.d.ts +6 -1
  49. package/dist/server/modules/translation-levels/types.js +0 -1
  50. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +10 -4
  51. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.d.ts +1 -1
  52. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.js +1 -1
  53. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +5 -0
  54. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +13 -4
  55. package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +17 -0
  56. package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +8 -2
  57. package/dist/server/shared/field-config/types.js +1 -1
  58. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +25 -10
  59. package/dist/server/shared/field-traversal/findFieldByPath.js +59 -14
  60. package/dist/server/shared/field-traversal/index.d.ts +1 -1
  61. package/dist/server/shared/field-traversal/index.js +1 -1
  62. package/dist/server/shared/field-traversal/kernel.d.ts +22 -0
  63. package/dist/server/shared/field-traversal/kernel.js +27 -0
  64. package/package.json +2 -2
@@ -1,19 +1,29 @@
1
- import { classifyField, tabScopes } from "./kernel";
1
+ import { classifyField, resolveBlockFields, tabScopes } from "./kernel";
2
+ const isIndexSegment = (segment)=>/^\d+$/u.test(segment);
3
+ /** Safely read `key` off an object/array; `undefined` for non-objects (arrays index by string key). */ const childData = (data, key)=>data != null && typeof data === "object" ? data[key] : undefined;
2
4
  /**
3
5
  * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
4
6
  * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
5
7
  * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
6
- * path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
7
- * in one place.
8
+ * path scope. Built on {@link classifyField} / {@link tabScopes} / {@link resolveBlockFields} so
9
+ * the structural dispatch lives in one place.
8
10
  *
9
- * The caller supplies already-prepared name segments (split, trimmed, with array indices dropped
10
- * a field's config is shared across array items, so indices never appear in the schema).
11
+ * **Array indices and `blocks`.** Array element configs are shared, so a numeric segment after an
12
+ * array name selects the data item but not the schema (the schema continues into `array.fields`).
13
+ * A `blocks` field is polymorphic — which block (and thus which fields) sits at an index lives in
14
+ * the *data*, not the schema. So to descend through `blocks` the caller must pass the document
15
+ * `data` and keep the element index in the path: this function reads `data[name][index].blockType`
16
+ * and resolves the matching block via {@link resolveBlockFields}. Without `data` (or with a
17
+ * non-indexed path), descending through `blocks` returns `inside-blocks`.
11
18
  *
12
19
  * @param fields - The schema level to search.
13
- * @param segments - Remaining path segments (names) to match, head-first.
20
+ * @param segments - Remaining path segments to match, head-first. Keep array/block element indices
21
+ * in the path when navigating with `data`.
22
+ * @param data - The document (sub)tree aligned with `fields`, used only to disambiguate `blocks`.
23
+ * Omit for schema-only navigation.
14
24
  * @returns A {@link FieldPathResult}.
15
25
  * @public
16
- */ export function findFieldByPath(fields, segments) {
26
+ */ export function findFieldByPath(fields, segments, data) {
17
27
  const [head, ...rest] = segments;
18
28
  if (head === undefined) return {
19
29
  status: "not-found"
@@ -28,10 +38,10 @@ import { classifyField, tabScopes } from "./kernel";
28
38
  if (scope.tab.name === head) {
29
39
  return rest.length === 0 ? {
30
40
  status: "container"
31
- } : findFieldByPath(scope.tab.fields, rest);
41
+ } : findFieldByPath(scope.tab.fields, rest, childData(data, head));
32
42
  }
33
43
  } else {
34
- const found = findFieldByPath(scope.fields, segments); // unnamed tab → same path scope
44
+ const found = findFieldByPath(scope.fields, segments, data); // unnamed tab → same path + data scope
35
45
  if (found.status !== "not-found") return found;
36
46
  }
37
47
  }
@@ -39,28 +49,63 @@ import { classifyField, tabScopes } from "./kernel";
39
49
  }
40
50
  case "transparent":
41
51
  {
42
- const found = findFieldByPath(structure.fields, segments); // row/collapsible/unnamed group → same scope
52
+ const found = findFieldByPath(structure.fields, segments, data); // row/collapsible/unnamed group → same scope
43
53
  if (found.status !== "not-found") return found;
44
54
  break;
45
55
  }
46
56
  case "presentational":
47
57
  break;
48
58
  case "group":
49
- case "array":
50
59
  {
51
60
  if (structure.name !== head) break;
52
61
  return rest.length === 0 ? {
53
62
  status: "container"
54
- } : findFieldByPath(structure.fields, rest);
63
+ } : findFieldByPath(structure.fields, rest, childData(data, head));
64
+ }
65
+ case "array":
66
+ {
67
+ if (structure.name !== head) break;
68
+ if (rest.length === 0) return {
69
+ status: "container"
70
+ };
71
+ // A localized array is an independent per-locale structure — its element index is not a
72
+ // stable identity across locales, so refuse to navigate THROUGH it by path.
73
+ if (structure.field.localized) return {
74
+ status: "localized-list-ancestor"
75
+ };
76
+ // A numeric segment selects the data element; the schema is shared across elements.
77
+ const [next, ...tail] = rest;
78
+ if (isIndexSegment(next)) {
79
+ return tail.length === 0 ? {
80
+ status: "container"
81
+ } : findFieldByPath(structure.fields, tail, childData(childData(data, head), next));
82
+ }
83
+ return findFieldByPath(structure.fields, rest, childData(data, head));
55
84
  }
56
85
  case "blocks":
57
86
  {
58
87
  if (structure.name !== head) break;
59
- return rest.length === 0 ? {
88
+ if (rest.length === 0) return {
60
89
  status: "container"
61
- } : {
90
+ };
91
+ // A localized blocks field is an independent per-locale structure — its element index is
92
+ // not a stable identity across locales, so refuse to navigate THROUGH it by path.
93
+ if (structure.field.localized) return {
94
+ status: "localized-list-ancestor"
95
+ };
96
+ // Polymorphic: need the element index + its data to pick the block schema by `blockType`.
97
+ const [next, ...tail] = rest;
98
+ if (!isIndexSegment(next)) return {
99
+ status: "inside-blocks"
100
+ };
101
+ const item = childData(childData(data, head), next);
102
+ const blockFields = resolveBlockFields(structure.field, item);
103
+ if (!blockFields) return {
62
104
  status: "inside-blocks"
63
105
  };
106
+ return tail.length === 0 ? {
107
+ status: "container"
108
+ } : findFieldByPath(blockFields, tail, item);
64
109
  }
65
110
  case "leaf":
66
111
  {
@@ -1,5 +1,5 @@
1
1
  export { findFieldByPath } from "./findFieldByPath";
2
2
  export type { FieldPathResult } from "./findFieldByPath";
3
- export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
3
+ export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
4
4
  export type { ChildCursor, ChildOutput, ContainerInfo, FieldStructure, FieldWalker, LeafField, TabScope, WalkSignal } from "./types";
5
5
  export { walkFields } from "./walkFields";
@@ -1,5 +1,5 @@
1
1
  export { findFieldByPath } from "./findFieldByPath";
2
- export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
2
+ export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
3
3
  export { walkFields } from "./walkFields";
4
4
 
5
5
  //# sourceMappingURL=index.js.map
@@ -83,3 +83,25 @@ export declare function tabScopes(field: TabsField): TabScope[];
83
83
  * @public
84
84
  */
85
85
  export declare function resolveBlockFields(field: BlocksField, item: unknown): Field[] | null;
86
+ /**
87
+ * Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
88
+ * position.
89
+ *
90
+ * Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
91
+ * reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
92
+ * unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
93
+ * regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
94
+ * equals the old positional match), while independent localized content has diverging ids (no
95
+ * match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
96
+ * `blockType` must also match, so an id collision across types can't graft mismatched fields.
97
+ *
98
+ * Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
99
+ * their `target` array against the source/reference element currently being visited.
100
+ *
101
+ * @param arr - The array to search (e.g. the target-locale elements).
102
+ * @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
103
+ * @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
104
+ * @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
105
+ * @public
106
+ */
107
+ export declare function matchElementById(arr: unknown[], refItem: Record<string, unknown>, isBlocks: boolean): Record<string, unknown>;
@@ -1,5 +1,6 @@
1
1
  import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
2
2
  import { hasFields, isBlockItem, isTabsField } from "../guards";
3
+ import { isObject } from "../utils";
3
4
  /**
4
5
  * Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
5
6
  * one place that encodes how Payload field types map onto data boundaries.
@@ -141,5 +142,31 @@ import { hasFields, isBlockItem, isTabsField } from "../guards";
141
142
  const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
142
143
  return block ? block.fields : null;
143
144
  }
145
+ /**
146
+ * Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
147
+ * position.
148
+ *
149
+ * Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
150
+ * reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
151
+ * unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
152
+ * regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
153
+ * equals the old positional match), while independent localized content has diverging ids (no
154
+ * match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
155
+ * `blockType` must also match, so an id collision across types can't graft mismatched fields.
156
+ *
157
+ * Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
158
+ * their `target` array against the source/reference element currently being visited.
159
+ *
160
+ * @param arr - The array to search (e.g. the target-locale elements).
161
+ * @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
162
+ * @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
163
+ * @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
164
+ * @public
165
+ */ export function matchElementById(arr, refItem, isBlocks) {
166
+ const id = refItem.id;
167
+ if (id === undefined || id === null) return {};
168
+ const match = arr.find((candidate)=>isObject(candidate) && candidate.id === id && (!isBlocks || candidate.blockType === refItem.blockType));
169
+ return isObject(match) ? match : {};
170
+ }
144
171
 
145
172
  //# sourceMappingURL=kernel.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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",
@@ -50,7 +50,7 @@
50
50
  "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
51
51
  "copyfiles": "copyfiles -u 1 \"src/**/*.scss\" dist",
52
52
  "clean": "rm -rf dist",
53
- "dev": "bun run build:swc -- --watch",
53
+ "dev": "bun run build:swc -- --watch --copy-files",
54
54
  "lint": "ultracite check",
55
55
  "lint:fix": "ultracite fix",
56
56
  "test": "vitest run",