@focus-reactive/payload-plugin-translator 0.9.1 → 0.9.2

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.
@@ -2,6 +2,7 @@ export { findFieldByPath } from "./findFieldByPath";
2
2
  export type { FieldPathResult } from "./findFieldByPath";
3
3
  export { hasFields, isBlockItem, isTabsField } from "./guards";
4
4
  export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
5
+ export { projectFieldsToFieldLike } from "./projectFieldLike";
5
6
  export { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName, } from "./predicates";
6
7
  export type { ArrayFieldLike, BlockLike, BlocksFieldLike, ChildCursor, ChildOutput, ContainerInfo, FieldLike, FieldStructure, FieldWalker, GroupFieldLike, LeafField, LeafFieldLike, TabLike, TabScope, TabsFieldLike, WalkSignal, } from "./types";
7
8
  export { walkFields } from "./walkFields";
@@ -1,6 +1,7 @@
1
1
  export { findFieldByPath } from "./findFieldByPath";
2
2
  export { hasFields, isBlockItem, isTabsField } from "./guards";
3
3
  export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
4
+ export { projectFieldsToFieldLike } from "./projectFieldLike";
4
5
  export { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "./predicates";
5
6
  export { walkFields } from "./walkFields";
6
7
 
@@ -0,0 +1,32 @@
1
+ import type { FieldLike } from "./types";
2
+ /**
3
+ * Deep-project a field schema into an independent {@link FieldLike} tree, copying ONLY the properties
4
+ * this layer reads. Replaces the `JSON.parse(JSON.stringify(fields))` clone the plugin used to snapshot
5
+ * a collection's schema before Payload's sanitizer mutates the originals.
6
+ *
7
+ * Why a bespoke projection rather than a structural clone:
8
+ * - **Independence is mandatory, not cosmetic.** Payload's `sanitizeFields` does `delete field.localized`
9
+ * **in place** on any field nested under a localized ancestor (group/array/named tab). The pipeline
10
+ * reads per-field `localized`, so a shared reference would silently lose it and the field would stop
11
+ * being translated. Every field/container here is a NEW object, and `localized` is copied by value at
12
+ * projection time (before sanitize runs), so the snapshot is preserved.
13
+ * - **`structuredClone` can't be used.** Field definitions embed Lexical editor configs containing async
14
+ * functions, which `structuredClone` throws on. The old JSON round-trip "worked" only by silently
15
+ * dropping every function; this projection drops them explicitly by copying a typed whitelist.
16
+ *
17
+ * The contract is exactly what the traversal/pipeline consumes: `type` (classify), `name`/`localized`
18
+ * (leaf data + translation gate), `custom` (only `custom.translateKit.exclude` is read), and the
19
+ * container members `fields` (group/array/row/collapsible/unnamed-group), `blocks` (with the
20
+ * load-bearing `block.slug` used for block-type dispatch), and `tabs` (named + unnamed). Every field is
21
+ * kept 1:1 (including presentational `ui`/`row`/`collapsible`) so downstream classification is unchanged;
22
+ * only the property set is narrowed.
23
+ *
24
+ * `custom` is copied by reference: it is a passthrough bag Payload does not mutate during sanitize (only
25
+ * `localized` is deleted, which we snapshot by value), and deep-copying it would re-introduce the
26
+ * `structuredClone`-on-functions problem this projection exists to avoid.
27
+ *
28
+ * @param fields - The source field list (Payload's `Field[]` is structurally assignable to `FieldLike[]`).
29
+ * @returns A new, independent `FieldLike[]` unaffected by later mutation of the source objects.
30
+ * @since 0.9.1
31
+ */
32
+ export declare function projectFieldsToFieldLike(fields: FieldLike[]): FieldLike[];
@@ -0,0 +1,60 @@
1
+ function projectField(field) {
2
+ const out = {
3
+ type: field.type
4
+ };
5
+ if (field.name !== undefined) out.name = field.name;
6
+ if (field.localized !== undefined) out.localized = field.localized;
7
+ if (field.custom !== undefined) out.custom = field.custom;
8
+ if (field.fields) out.fields = field.fields.map(projectField);
9
+ if (field.blocks) {
10
+ out.blocks = field.blocks.map((block)=>({
11
+ slug: block.slug,
12
+ fields: block.fields.map(projectField)
13
+ }));
14
+ }
15
+ if (field.tabs) {
16
+ out.tabs = field.tabs.map((tab)=>{
17
+ const projected = {
18
+ fields: tab.fields.map(projectField)
19
+ };
20
+ if (tab.name !== undefined) projected.name = tab.name;
21
+ if (tab.localized !== undefined) projected.localized = tab.localized;
22
+ return projected;
23
+ });
24
+ }
25
+ return out;
26
+ }
27
+ /**
28
+ * Deep-project a field schema into an independent {@link FieldLike} tree, copying ONLY the properties
29
+ * this layer reads. Replaces the `JSON.parse(JSON.stringify(fields))` clone the plugin used to snapshot
30
+ * a collection's schema before Payload's sanitizer mutates the originals.
31
+ *
32
+ * Why a bespoke projection rather than a structural clone:
33
+ * - **Independence is mandatory, not cosmetic.** Payload's `sanitizeFields` does `delete field.localized`
34
+ * **in place** on any field nested under a localized ancestor (group/array/named tab). The pipeline
35
+ * reads per-field `localized`, so a shared reference would silently lose it and the field would stop
36
+ * being translated. Every field/container here is a NEW object, and `localized` is copied by value at
37
+ * projection time (before sanitize runs), so the snapshot is preserved.
38
+ * - **`structuredClone` can't be used.** Field definitions embed Lexical editor configs containing async
39
+ * functions, which `structuredClone` throws on. The old JSON round-trip "worked" only by silently
40
+ * dropping every function; this projection drops them explicitly by copying a typed whitelist.
41
+ *
42
+ * The contract is exactly what the traversal/pipeline consumes: `type` (classify), `name`/`localized`
43
+ * (leaf data + translation gate), `custom` (only `custom.translateKit.exclude` is read), and the
44
+ * container members `fields` (group/array/row/collapsible/unnamed-group), `blocks` (with the
45
+ * load-bearing `block.slug` used for block-type dispatch), and `tabs` (named + unnamed). Every field is
46
+ * kept 1:1 (including presentational `ui`/`row`/`collapsible`) so downstream classification is unchanged;
47
+ * only the property set is narrowed.
48
+ *
49
+ * `custom` is copied by reference: it is a passthrough bag Payload does not mutate during sanitize (only
50
+ * `localized` is deleted, which we snapshot by value), and deep-copying it would re-introduce the
51
+ * `structuredClone`-on-functions problem this projection exists to avoid.
52
+ *
53
+ * @param fields - The source field list (Payload's `Field[]` is structurally assignable to `FieldLike[]`).
54
+ * @returns A new, independent `FieldLike[]` unaffected by later mutation of the source objects.
55
+ * @since 0.9.1
56
+ */ export function projectFieldsToFieldLike(fields) {
57
+ return fields.map(projectField);
58
+ }
59
+
60
+ //# sourceMappingURL=projectFieldLike.js.map
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
2
2
  import { configureAutoTranslate } from "./server/modules/auto-translate";
3
3
  import { configureProvenance } from "./server/modules/provenance";
4
+ import { projectFieldsToFieldLike } from "./core/kernel/field-traversal";
4
5
  import { wireTranslateRunner } from "./server/features/translate-document";
5
6
  import { documentLevel, collectionLevel } from "./composition/levels";
6
7
  import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
@@ -13,17 +14,15 @@ import { normalizePath } from "./server/shared";
13
14
  init() {
14
15
  return async (config)=>{
15
16
  const { access, translationProvider, runner, collections, levels, provenance, lifecycle, basePath: rawBasePath = "/translate" } = this.pluginConfig;
16
- // Build schema map from deep-cloned collections.
17
- // Deep clone is required because Payload mutates the original collection objects, removing
18
- // `localized: true` from nested fields during sanitization. JSON round-trip (not
19
- // structuredClone) because Lexical editor configs contain async functions structuredClone
20
- // cannot handle.
21
- // TODO: Consider introducing a FieldLike interface with only the properties used by the
22
- // pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
23
- // and avoid reliance on JSON round-trip.
17
+ // Snapshot each collection's schema as an independent FieldLike tree BEFORE Payload's sanitizer
18
+ // mutates the originals (it deletes `localized` from fields nested under a localized ancestor).
19
+ // `projectFieldsToFieldLike` deep-copies only the properties the pipeline reads — an explicit,
20
+ // typed contract, replacing the old JSON round-trip (which "worked" only by silently dropping the
21
+ // Lexical editor's async functions that structuredClone chokes on). Payload's `Field[]` is
22
+ // structurally assignable to `FieldLike[]`, so the projection happens right here at the boundary.
24
23
  const schemaMap = new Map(collections.map((col)=>[
25
24
  col.slug,
26
- JSON.parse(JSON.stringify(col.fields))
25
+ projectFieldsToFieldLike(col.fields)
27
26
  ]));
28
27
  const collectionSlugs = new Set(schemaMap.keys());
29
28
  const basePath = normalizePath(rawBasePath);
@@ -1,4 +1,4 @@
1
- import type { Field } from "payload";
1
+ import type { FieldLike } from "../../../core/kernel/field-traversal";
2
2
  /**
3
3
  * Outcome of mapping a declared field path to a translatable subtree.
4
4
  *
@@ -14,7 +14,7 @@ import type { Field } from "payload";
14
14
  */
15
15
  export type FieldSubtreeResolution = {
16
16
  status: "resolved";
17
- schema: Field[];
17
+ schema: FieldLike[];
18
18
  sourceData: Record<string, unknown>;
19
19
  fieldName: string;
20
20
  } | {
@@ -36,4 +36,4 @@ export type FieldSubtreeResolution = {
36
36
  * the element's `blockType` in `doc` picks the block schema. Without `doc`, a path through a
37
37
  * `blocks` field returns `inside-blocks`.
38
38
  */
39
- export declare function resolveFieldSubtree(rootFields: Field[], fieldPath: string, value: unknown, doc?: unknown): FieldSubtreeResolution;
39
+ export declare function resolveFieldSubtree(rootFields: FieldLike[], fieldPath: string, value: unknown, doc?: unknown): FieldSubtreeResolution;
@@ -1,6 +1,9 @@
1
- import type { CollectionSlug, Field } from "payload";
1
+ import type { CollectionSlug } from "payload";
2
+ import type { FieldLike } from "../core/kernel/field-traversal";
2
3
  /**
3
- * Map of collection slug to original field schema.
4
- * Used to access original schemas before Payload sanitization.
4
+ * Map of collection slug to its projected field schema — an independent {@link FieldLike} snapshot
5
+ * taken (via `projectFieldsToFieldLike`) before Payload's sanitizer mutates the originals, so nested
6
+ * `localized` flags survive. Typed as `FieldLike[]` (not Payload's `Field[]`) because every consumer
7
+ * reads it structurally as `FieldLike`; the projection is the boundary that makes that explicit.
5
8
  */
6
- export type CollectionSchemaMap = Map<CollectionSlug, Field[]>;
9
+ export type CollectionSchemaMap = Map<CollectionSlug, FieldLike[]>;
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Map of collection slug to original field schema.
3
- * Used to access original schemas before Payload sanitization.
2
+ * Map of collection slug to its projected field schema — an independent {@link FieldLike} snapshot
3
+ * taken (via `projectFieldsToFieldLike`) before Payload's sanitizer mutates the originals, so nested
4
+ * `localized` flags survive. Typed as `FieldLike[]` (not Payload's `Field[]`) because every consumer
5
+ * reads it structurally as `FieldLike`; the projection is the boundary that makes that explicit.
4
6
  */ export { };
5
7
 
6
8
  //# sourceMappingURL=CollectionSchemaMap.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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",