@focus-reactive/payload-plugin-translator 0.4.0 → 0.5.1

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 (87) hide show
  1. package/README.md +44 -29
  2. package/dist/client/shared/ui/Button/Button.d.ts +4 -4
  3. package/dist/client/shared/ui/Button/Button.js +18 -18
  4. package/dist/client/shared/ui/Button/styles.module.scss +1 -1
  5. package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
  6. package/dist/client/shared/ui/Popup/Popup.js +4 -3
  7. package/dist/client/shared/ui/Select/Select.d.ts +9 -7
  8. package/dist/client/shared/ui/Select/Select.js +18 -17
  9. package/dist/client/shared/ui/Select/styles.module.scss +5 -0
  10. package/dist/index.d.ts +16 -14
  11. package/dist/index.js +9 -7
  12. package/dist/plugin.d.ts +15 -4
  13. package/dist/plugin.js +34 -57
  14. package/dist/server/features/cancel/handler.d.ts +2 -2
  15. package/dist/server/features/cancel/handler.js +3 -1
  16. package/dist/server/features/cancel/route.d.ts +4 -4
  17. package/dist/server/features/cancel/route.js +4 -4
  18. package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
  19. package/dist/server/features/cancel-by-collection/handler.js +1 -1
  20. package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
  21. package/dist/server/features/cancel-by-collection/route.js +4 -4
  22. package/dist/server/features/createTranslationRoutes.d.ts +23 -0
  23. package/dist/server/features/createTranslationRoutes.js +27 -0
  24. package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
  25. package/dist/server/features/enqueue-translation/handler.js +1 -1
  26. package/dist/server/features/enqueue-translation/route.d.ts +5 -5
  27. package/dist/server/features/enqueue-translation/route.js +4 -4
  28. package/dist/server/features/get-collection-status/handler.d.ts +3 -3
  29. package/dist/server/features/get-collection-status/handler.js +1 -1
  30. package/dist/server/features/get-collection-status/route.d.ts +5 -5
  31. package/dist/server/features/get-collection-status/route.js +4 -4
  32. package/dist/server/features/get-document-status/handler.d.ts +3 -3
  33. package/dist/server/features/get-document-status/handler.js +1 -1
  34. package/dist/server/features/get-document-status/route.d.ts +5 -5
  35. package/dist/server/features/get-document-status/route.js +4 -4
  36. package/dist/server/features/index.d.ts +7 -6
  37. package/dist/server/features/index.js +7 -6
  38. package/dist/server/features/run-translation/handler.d.ts +2 -2
  39. package/dist/server/features/run-translation/route.d.ts +4 -4
  40. package/dist/server/features/run-translation/route.js +4 -4
  41. package/dist/server/features/translate-document/handler.js +7 -11
  42. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
  43. package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
  44. package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +18 -1
  45. package/dist/server/modules/task-runner/index.d.ts +6 -6
  46. package/dist/server/modules/task-runner/index.js +2 -2
  47. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
  48. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +16 -1
  49. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
  50. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
  51. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
  52. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
  53. package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
  54. package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
  55. package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
  56. package/dist/server/modules/translation-levels/documentLevel.js +28 -0
  57. package/dist/server/modules/translation-levels/index.d.ts +3 -0
  58. package/dist/server/modules/translation-levels/index.js +4 -0
  59. package/dist/server/modules/translation-levels/types.d.ts +42 -0
  60. package/dist/server/modules/translation-levels/types.js +10 -0
  61. package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
  62. package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
  63. package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
  64. package/dist/server/modules/translation-pipeline/index.js +3 -2
  65. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
  66. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
  67. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
  68. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
  69. package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
  70. package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
  71. package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
  72. package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
  73. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
  74. package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
  75. package/dist/server/shared/field-traversal/index.d.ts +5 -0
  76. package/dist/server/shared/field-traversal/index.js +5 -0
  77. package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
  78. package/dist/server/shared/field-traversal/kernel.js +145 -0
  79. package/dist/server/shared/field-traversal/types.d.ts +191 -0
  80. package/dist/server/shared/field-traversal/types.js +41 -0
  81. package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
  82. package/dist/server/shared/field-traversal/walkFields.js +164 -0
  83. package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
  84. package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
  85. package/dist/types/AccessGuard.d.ts +24 -1
  86. package/dist/types/AccessGuard.js +12 -1
  87. package/package.json +2 -2
package/README.md CHANGED
@@ -12,6 +12,7 @@ Translation plugin for Payload CMS 3.x. Automatically translate your localized c
12
12
  - **Pluggable providers** — use OpenAI or create custom translation providers
13
13
  - **Field exclusion** — exclude specific fields from translation via `withFieldTranslation`
14
14
  - **Translation strategies** — choose between overwrite all or skip existing translations
15
+ - **Configurable surfaces** — enable the per-document popup and/or the bulk-collection dashboard via the `levels` option _(since v0.5.0)_
15
16
 
16
17
  ## Installation
17
18
 
@@ -33,11 +34,7 @@ yarn add @focus-reactive/payload-plugin-translator
33
34
 
34
35
  ```typescript
35
36
  import { buildConfig } from "payload";
36
- import {
37
- translatorPlugin,
38
- createOpenAIProvider,
39
- createPayloadJobsRunner,
40
- } from "@focus-reactive/payload-plugin-translator";
37
+ import { translatorPlugin, createOpenAIProvider, createPayloadJobsRunner } from "@focus-reactive/payload-plugin-translator";
41
38
  import { Posts } from "./collections/Posts";
42
39
  import { Pages } from "./collections/Pages";
43
40
 
@@ -65,13 +62,14 @@ export default buildConfig({
65
62
 
66
63
  Configuration for `translatorPlugin()`.
67
64
 
68
- | Property | Type | Required | Default | Description |
69
- | --------------------- | --------------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
70
- | `collections` | `CollectionConfig[]` | Yes | — | Original collection configs to enable translation for. Must be the same objects passed to `buildConfig`, not slugs. |
71
- | `translationProvider` | `TranslationProvider` | Yes | — | Translation provider instance (e.g., `createOpenAIProvider(...)`) |
72
- | `runner` | `TaskRunnerProvider` | Yes | — | Task runner provider for background processing (e.g., `createPayloadJobsRunner()`) |
73
- | `access` | `AccessGuard` | No | `undefined` | Access control function for translation endpoints |
74
- | `basePath` | `string` | No | `'/translate'` | Base path for all API endpoints |
65
+ | Property | Type | Required | Default | Description |
66
+ | --------------------- | --------------------- | -------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
67
+ | `collections` | `CollectionConfig[]` | Yes | — | Original collection configs to enable translation for. Must be the same objects passed to `buildConfig`, not slugs. |
68
+ | `translationProvider` | `TranslationProvider` | Yes | — | Translation provider instance (e.g., `createOpenAIProvider(...)`) |
69
+ | `runner` | `TaskRunnerProvider` | Yes | — | Task runner provider for background processing (e.g., `createPayloadJobsRunner()`) |
70
+ | `access` | `AccessGuard` | No | `undefined` | Access guard (`{ check }`) for the translation endpoints; omit to leave them open |
71
+ | `basePath` | `string` | No | `'/translate'` | Base path for all API endpoints |
72
+ | `levels` | `TranslationLevel[]` | No | `[documentLevel(), collectionLevel()]` | Which translation surfaces to enable. See [Translation Levels](#translation-levels) |
75
73
 
76
74
  ```typescript
77
75
  translatorPlugin({
@@ -80,11 +78,37 @@ translatorPlugin({
80
78
  apiKey: process.env.OPENAI_API_KEY,
81
79
  }),
82
80
  runner: createPayloadJobsRunner(),
83
- access: async ({ req }) => req.user?.role === "admin",
81
+ access: { check: ({ req }) => req.user?.role === "admin" },
84
82
  basePath: "/translate",
85
83
  });
86
84
  ```
87
85
 
86
+ ### Translation Levels
87
+
88
+ _Since v0.5.0._
89
+
90
+ The `levels` option controls which translation surfaces the plugin exposes. Each entry is a factory you import and list:
91
+
92
+ - `documentLevel()` — a **Translate** popup on the document edit view (translate one document).
93
+ - `collectionLevel()` — a **bulk dashboard** on the collection list view (translate many at once).
94
+
95
+ Both run through the configured `runner` (async Payload Jobs by default, or synchronous via `createSyncRunner()`) and share the same translation REST API.
96
+
97
+ ```typescript
98
+ import { translatorPlugin, documentLevel, collectionLevel, createOpenAIProvider, createPayloadJobsRunner } from "@focus-reactive/payload-plugin-translator";
99
+
100
+ translatorPlugin({
101
+ collections: [Posts],
102
+ translationProvider: createOpenAIProvider({
103
+ apiKey: process.env.OPENAI_API_KEY,
104
+ }),
105
+ runner: createPayloadJobsRunner(),
106
+ levels: [collectionLevel()], // bulk dashboard only — no per-document popup
107
+ });
108
+ ```
109
+
110
+ Omit `levels` for the default `[documentLevel(), collectionLevel()]`, which is identical to the previous behaviour — so adopting this option is non-breaking. A synchronous `fieldLevel()` (in-place field translation) is planned for a later release.
111
+
88
112
  ### OpenAIProviderConfig
89
113
 
90
114
  Configuration for `createOpenAIProvider()`.
@@ -100,8 +124,7 @@ Configuration for `createOpenAIProvider()`.
100
124
  createOpenAIProvider({
101
125
  apiKey: process.env.OPENAI_API_KEY,
102
126
  model: "gpt-4o-mini",
103
- systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) =>
104
- `${defaultPrompt}\nUse formal language. Keep brand names unchanged.`,
127
+ systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) => `${defaultPrompt}\nUse formal language. Keep brand names unchanged.`,
105
128
  dryRun: false,
106
129
  });
107
130
  ```
@@ -165,10 +188,7 @@ Configuration for `withFieldTranslation()` helper or `field.custom.translateKit`
165
188
  ```typescript
166
189
  import { withFieldTranslation } from "@focus-reactive/payload-plugin-translator";
167
190
 
168
- withFieldTranslation(
169
- { name: "sku", type: "text", localized: true },
170
- { exclude: true },
171
- );
191
+ withFieldTranslation({ name: "sku", type: "text", localized: true }, { exclude: true });
172
192
  ```
173
193
 
174
194
  ## Translation Strategies
@@ -310,20 +330,12 @@ type TranslationOutput = Record<TranslationIndex, string>;
310
330
  #### Example Implementation
311
331
 
312
332
  ```typescript
313
- import type {
314
- TranslationProvider,
315
- TranslationInput,
316
- TranslationOutput,
317
- } from "@focus-reactive/payload-plugin-translator";
333
+ import type { TranslationProvider, TranslationInput, TranslationOutput } from "@focus-reactive/payload-plugin-translator";
318
334
 
319
335
  class DeepLProvider implements TranslationProvider {
320
336
  constructor(private apiKey: string) {}
321
337
 
322
- async translate(
323
- content: TranslationInput,
324
- sourceLng: string,
325
- targetLng: string,
326
- ): Promise<TranslationOutput | null> {
338
+ async translate(content: TranslationInput, sourceLng: string, targetLng: string): Promise<TranslationOutput | null> {
327
339
  try {
328
340
  // content example: { "0": "Hello", "1": "World" }
329
341
  const texts = Object.values(content);
@@ -401,6 +413,9 @@ import type {
401
413
 
402
414
  // Field config
403
415
  FieldTranslationConfig,
416
+
417
+ // Translation levels
418
+ TranslationLevel,
404
419
  } from "@focus-reactive/payload-plugin-translator";
405
420
  ```
406
421
 
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithRef, ReactNode } from 'react';
2
- type ButtonProps = ComponentPropsWithRef<'button'> & {
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ type ButtonProps = ComponentPropsWithRef<"button"> & {
3
3
  $fullWidth?: boolean;
4
- $variant?: 'outlined' | 'filled' | 'unstyled' | 'transparent' | 'light';
4
+ $variant?: "outlined" | "filled" | "unstyled" | "transparent" | "light";
5
5
  $startContent?: ReactNode;
6
- $size?: 'sm' | 'md' | 'lg';
6
+ $size?: "sm" | "md" | "lg";
7
7
  $isLoading?: boolean;
8
8
  $isIconButton?: boolean;
9
9
  };
@@ -1,31 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import classNames from 'classnames';
3
- import { forwardRef } from 'react';
4
- import Loading from '../Loading';
5
- import styles from './styles.module.scss';
2
+ import classNames from "classnames";
3
+ import { forwardRef } from "react";
4
+ import Loading from "../Loading";
5
+ import styles from "./styles.module.scss";
6
6
  const sizes = {
7
7
  sm: {
8
- height: '24px',
9
- paddingInline: '0.25rem'
8
+ height: "24px",
9
+ paddingInline: "0.25rem"
10
10
  },
11
11
  md: {
12
- height: '32px',
13
- paddingInline: '0.5rem'
12
+ height: "32px",
13
+ paddingInline: "0.5rem"
14
14
  },
15
15
  lg: {
16
- height: '40px',
17
- paddingInline: '0.75rem'
16
+ height: "40px",
17
+ paddingInline: "0.75rem"
18
18
  }
19
19
  };
20
- const Button = /*#__PURE__*/ forwardRef(function Button({ className = '', children, $fullWidth, style, $startContent, $variant, $isLoading, $size = 'md', $isIconButton, ...props }, ref) {
20
+ const Button = /*#__PURE__*/ forwardRef(function Button({ className = "", children, $fullWidth, style, $startContent, $variant, $isLoading, $size = "md", $isIconButton, ...props }, ref) {
21
21
  const _style = {
22
- '--button-width': $fullWidth ? '100%' : 'auto',
23
- '--height': sizes[$size].height,
24
- '--padding-inline': $isIconButton ? '0' : sizes[$size].paddingInline,
25
- '--width': $isIconButton ? sizes[$size].height : 'auto',
22
+ "--button-width": $fullWidth ? "100%" : "auto",
23
+ "--height": sizes[$size].height,
24
+ "--padding-inline": $isIconButton ? "0" : sizes[$size].paddingInline,
25
+ "--width": $isIconButton ? sizes[$size].height : "auto",
26
26
  ...style
27
27
  };
28
- const buttonClassName = $variant === 'unstyled' ? classNames(styles['unstyled'], className, $isIconButton && styles['icon-button']) : classNames(styles['button'], className, $variant ? styles[$variant] : undefined, $isIconButton && styles['icon-button']);
28
+ const buttonClassName = $variant === "unstyled" ? classNames(styles["unstyled"], className, $isIconButton && styles["icon-button"]) : classNames(styles["button"], className, $variant ? styles[$variant] : undefined, $isIconButton && styles["icon-button"]);
29
29
  return /*#__PURE__*/ _jsxs("button", {
30
30
  ...props,
31
31
  style: _style,
@@ -33,11 +33,11 @@ const Button = /*#__PURE__*/ forwardRef(function Button({ className = '', childr
33
33
  className: buttonClassName,
34
34
  children: [
35
35
  $startContent && /*#__PURE__*/ _jsx("div", {
36
- className: styles['start-content'],
36
+ className: styles["start-content"],
37
37
  children: $startContent
38
38
  }),
39
39
  $isLoading ? /*#__PURE__*/ _jsx(Loading, {
40
- size: $isIconButton ? 'small' : 'medium'
40
+ size: $isIconButton ? "small" : "medium"
41
41
  }) : children
42
42
  ]
43
43
  });
@@ -7,7 +7,7 @@
7
7
  justify-content: center;
8
8
  align-items: center;
9
9
  text-decoration: none;
10
- transition: background 0.1sease-in-out, opacity 0.2sease-in-out, border-color 0.2sease-in-out;
10
+ transition: background 0.1s ease-in-out, opacity 0.2s ease-in-out, border-color 0.2s ease-in-out;
11
11
  background: none;
12
12
  padding-block: 0.25rem;
13
13
  padding-inline: var(--padding-inline);
@@ -1,10 +1,16 @@
1
- import type { PropsWithChildren, ReactElement } from 'react';
1
+ import type { PropsWithChildren, ReactElement } from "react";
2
2
  type PopupProps = PropsWithChildren<{
3
3
  $trigger: ReactElement;
4
- $align?: 'center' | 'end' | 'start';
5
- $side?: 'top' | 'right' | 'bottom' | 'left';
4
+ $align?: "center" | "end" | "start";
5
+ $side?: "top" | "right" | "bottom" | "left";
6
6
  open: boolean;
7
7
  onOpenChange: (open: boolean) => void;
8
+ /**
9
+ * Forwarded to Radix `Popover.Content`. Call `event.preventDefault()` to stop the popover
10
+ * from auto-focusing its first focusable child on open — useful when that child is a
11
+ * tooltip trigger (Radix tooltips open on focus, so autofocus would flash the tooltip).
12
+ */
13
+ onOpenAutoFocus?: (event: Event) => void;
8
14
  }>;
9
- declare function Popup({ open, children, onOpenChange, $trigger, $align, $side }: PopupProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function Popup({ open, children, onOpenChange, $trigger, $align, $side, onOpenAutoFocus }: PopupProps): import("react/jsx-runtime").JSX.Element;
10
16
  export default Popup;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as Popover from '@radix-ui/react-popover';
3
- import style from './styles.module.scss';
4
- function Popup({ open, children, onOpenChange, $trigger, $align, $side }) {
2
+ import * as Popover from "@radix-ui/react-popover";
3
+ import style from "./styles.module.scss";
4
+ function Popup({ open, children, onOpenChange, $trigger, $align, $side, onOpenAutoFocus }) {
5
5
  return /*#__PURE__*/ _jsxs(Popover.Root, {
6
6
  open: open,
7
7
  onOpenChange: onOpenChange,
@@ -16,6 +16,7 @@ function Popup({ open, children, onOpenChange, $trigger, $align, $side }) {
16
16
  side: $side,
17
17
  align: $align,
18
18
  className: style.popover,
19
+ onOpenAutoFocus: onOpenAutoFocus,
19
20
  children: children
20
21
  })
21
22
  })
@@ -1,13 +1,15 @@
1
- import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react';
2
- type SelectProps = ComponentPropsWithRef<'select'> & {
1
+ import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from "react";
2
+ type SelectProps = ComponentPropsWithRef<"select"> & {
3
3
  emptyOption?: boolean;
4
4
  hasError?: boolean;
5
- $size?: 'sm' | 'md' | 'lg';
6
- 'aria-describedby'?: string;
7
- 'aria-invalid'?: boolean;
5
+ $size?: "sm" | "md" | "lg";
6
+ /** Stretch to fill the container (default). Set `false` to size to content — useful inline. */
7
+ $fullWidth?: boolean;
8
+ "aria-describedby"?: string;
9
+ "aria-invalid"?: boolean;
8
10
  };
9
- type SelectOptionProps = ComponentPropsWithoutRef<'option'>;
10
- type SelectEmptyOptionProps = Omit<SelectOptionProps, 'value' | 'disabled' | 'hidden'>;
11
+ type SelectOptionProps = ComponentPropsWithoutRef<"option">;
12
+ type SelectEmptyOptionProps = Omit<SelectOptionProps, "value" | "disabled" | "hidden">;
11
13
  declare const SelectOption: ({ children, ...props }: SelectOptionProps) => import("react/jsx-runtime").JSX.Element;
12
14
  declare const SelectEmptyOption: ({ children, ...props }: SelectEmptyOptionProps) => import("react/jsx-runtime").JSX.Element;
13
15
  declare const Select: import("react").ForwardRefExoticComponent<Omit<SelectProps, "ref"> & import("react").RefAttributes<HTMLSelectElement>>;
@@ -1,22 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import classNames from 'classnames';
3
- import { forwardRef } from 'react';
4
- import styles from './styles.module.scss';
2
+ import classNames from "classnames";
3
+ import { forwardRef } from "react";
4
+ import styles from "./styles.module.scss";
5
5
  const sizes = {
6
6
  sm: {
7
- height: '24px',
8
- paddingInline: '0.25rem',
9
- fontSize: '0.875rem'
7
+ height: "24px",
8
+ paddingInline: "0.25rem",
9
+ fontSize: "0.875rem"
10
10
  },
11
11
  md: {
12
- height: '32px',
13
- paddingInline: '0.5rem',
14
- fontSize: '1rem'
12
+ height: "32px",
13
+ paddingInline: "0.5rem",
14
+ fontSize: "1rem"
15
15
  },
16
16
  lg: {
17
- height: '40px',
18
- paddingInline: '0.75rem',
19
- fontSize: '1rem'
17
+ height: "40px",
18
+ paddingInline: "0.75rem",
19
+ fontSize: "1rem"
20
20
  }
21
21
  };
22
22
  const SelectOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option", {
@@ -28,15 +28,16 @@ const SelectEmptyOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option",
28
28
  ...props,
29
29
  children: children
30
30
  });
31
- const Select = /*#__PURE__*/ forwardRef(function Select({ children, className = '', hasError = false, $size = 'lg', style, 'aria-invalid': ariaInvalid, ...props }, ref) {
31
+ const Select = /*#__PURE__*/ forwardRef(function Select({ children, className = "", hasError = false, $size = "lg", $fullWidth = true, style, "aria-invalid": ariaInvalid, ...props }, ref) {
32
32
  const _style = {
33
- '--height': sizes[$size].height,
34
- '--padding-inline': sizes[$size].paddingInline,
35
- '--font-size': sizes[$size].fontSize,
33
+ "--height": sizes[$size].height,
34
+ "--padding-inline": sizes[$size].paddingInline,
35
+ "--font-size": sizes[$size].fontSize,
36
36
  ...style
37
37
  };
38
38
  const selectClassName = classNames(styles.select, styles[`size-${$size}`], {
39
- [styles.selectError]: hasError
39
+ [styles.selectError]: hasError,
40
+ [styles.auto]: !$fullWidth
40
41
  }, className);
41
42
  return /*#__PURE__*/ _jsx("select", {
42
43
  ref: ref,
@@ -31,6 +31,11 @@
31
31
  }
32
32
  }
33
33
 
34
+ /* Opt out of full width (`$fullWidth={false}`) — size to content, e.g. inline in a row. */
35
+ .auto {
36
+ width: auto;
37
+ }
38
+
34
39
  /* Size-specific styles if needed */
35
40
  .size-sm {
36
41
  background-size: 12px 12px;
package/dist/index.d.ts CHANGED
@@ -1,14 +1,16 @@
1
- export { translatorPlugin } from './plugin';
2
- export type { TranslatorPluginConfig } from './plugin';
3
- export type { AccessGuard, AccessGuardRequest } from './types/AccessGuard';
4
- export { createOpenAIProvider } from './server/modules/translation-providers';
5
- export type { TranslationProvider, TranslationInput, TranslationOutput, OpenAIProviderConfig, DryRunConfig, } from './server/modules/translation-providers';
6
- export { createPayloadJobsRunner, createSyncRunner } from './server/modules/task-runner';
7
- export type { TaskRunnerProvider, PayloadJobsRunnerOptions } from './server/modules/task-runner';
8
- export { withFieldTranslation } from './field-config';
9
- export type { FieldTranslationConfig } from './field-config';
10
- export { createTranslatePlugin, TranslateCollectionPlugin } from './plugin';
11
- export type { TranslateCollectionPluginConfig } from './plugin';
12
- export { OpenAITranslationProvider } from './server/modules/translation-providers';
13
- export { translateKitField } from './field-config';
14
- export type { TranslateKitFieldConfig } from './field-config';
1
+ export { translatorPlugin } from "./plugin";
2
+ export type { TranslatorPluginConfig } from "./plugin";
3
+ export type { AccessGuard, AccessGuardRequest } from "./types/AccessGuard";
4
+ export { createOpenAIProvider } from "./server/modules/translation-providers";
5
+ export type { TranslationProvider, TranslationInput, TranslationOutput, OpenAIProviderConfig, DryRunConfig } from "./server/modules/translation-providers";
6
+ export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
7
+ export type { TaskRunnerProvider, PayloadJobsRunnerOptions } from "./server/modules/task-runner";
8
+ export { documentLevel, collectionLevel } from "./server/modules/translation-levels";
9
+ export type { TranslationLevel } from "./server/modules/translation-levels";
10
+ export { withFieldTranslation } from "./field-config";
11
+ export type { FieldTranslationConfig } from "./field-config";
12
+ export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
13
+ export type { TranslateCollectionPluginConfig } from "./plugin";
14
+ export { OpenAITranslationProvider } from "./server/modules/translation-providers";
15
+ export { translateKitField } from "./field-config";
16
+ export type { TranslateKitFieldConfig } from "./field-config";
package/dist/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  // Main plugin
2
- export { translatorPlugin } from './plugin';
2
+ export { translatorPlugin } from "./plugin";
3
3
  // Translation providers
4
- export { createOpenAIProvider } from './server/modules/translation-providers';
4
+ export { createOpenAIProvider } from "./server/modules/translation-providers";
5
5
  // Task runners
6
- export { createPayloadJobsRunner, createSyncRunner } from './server/modules/task-runner';
6
+ export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
7
+ // Translation levels
8
+ export { documentLevel, collectionLevel } from "./server/modules/translation-levels";
7
9
  // Field config
8
- export { withFieldTranslation } from './field-config';
10
+ export { withFieldTranslation } from "./field-config";
9
11
  // Deprecated exports (for backwards compatibility)
10
- export { createTranslatePlugin, TranslateCollectionPlugin } from './plugin';
11
- export { OpenAITranslationProvider } from './server/modules/translation-providers';
12
- export { translateKitField } from './field-config';
12
+ export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
13
+ export { OpenAITranslationProvider } from "./server/modules/translation-providers";
14
+ export { translateKitField } from "./field-config";
13
15
 
14
16
  //# sourceMappingURL=index.js.map
package/dist/plugin.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { CollectionConfig, Config } from 'payload';
2
- import type { AccessGuard } from './types/AccessGuard';
3
- import type { TranslationProvider } from './server/modules/translation-providers';
4
- import type { TaskRunnerProvider } from './server/modules/task-runner';
1
+ import type { CollectionConfig, Config } from "payload";
2
+ import type { AccessGuard } from "./types/AccessGuard";
3
+ import type { TranslationProvider } from "./server/modules/translation-providers";
4
+ import type { TaskRunnerProvider } from "./server/modules/task-runner";
5
+ import type { TranslationLevel } from "./server/modules/translation-levels";
5
6
  export type TranslatorPluginConfig = {
6
7
  /**
7
8
  * Original collection configs (same objects passed to buildConfig).
@@ -32,6 +33,16 @@ export type TranslatorPluginConfig = {
32
33
  * @default '/translate'
33
34
  */
34
35
  basePath?: string;
36
+ /**
37
+ * Which translation surfaces to enable, as level factories — `documentLevel()`,
38
+ * `collectionLevel()` (and, from a later phase, `fieldLevel()`). Omit for the
39
+ * default `[documentLevel(), collectionLevel()]`, which is exactly today's behaviour.
40
+ * Each level should appear at most once: endpoints are deduplicated, but admin
41
+ * components are not, so a duplicated level renders a duplicated control.
42
+ * @default [documentLevel(), collectionLevel()]
43
+ * @since 0.5.0
44
+ */
45
+ levels?: TranslationLevel[];
35
46
  };
36
47
  /** @deprecated Use `TranslatorPluginConfig` instead */
37
48
  export type TranslateCollectionPluginConfig = TranslatorPluginConfig;
package/dist/plugin.js CHANGED
@@ -1,14 +1,8 @@
1
- import { CacheProviderExport } from './client/app/cache/CacheProvider.export';
2
- import { BulkDocumentTranslationDashboard } from './client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.export';
3
- import { TranslateDocumentExport } from './client/widgets/translate-document';
4
- import { TranslateDocumentHandler } from './server/features/translate-document';
5
- import { createEnqueueRoute } from './server/features/enqueue-translation';
6
- import { createRunRoute } from './server/features/run-translation';
7
- import { createCancelRoute } from './server/features/cancel';
8
- import { createCancelByCollectionRoute } from './server/features/cancel-by-collection';
9
- import { createGetDocumentStatusRoute } from './server/features/get-document-status';
10
- import { createGetCollectionStatusRoute } from './server/features/get-collection-status';
11
- import { normalizePath, pipe } from './server/shared';
1
+ import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
2
+ import { TranslateDocumentHandler } from "./server/features/translate-document";
3
+ import { documentLevel, collectionLevel } from "./server/modules/translation-levels";
4
+ import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
5
+ import { normalizePath } from "./server/shared";
12
6
  /** @deprecated Use `translatorPlugin` function instead */ export class TranslateCollectionPlugin {
13
7
  pluginConfig;
14
8
  constructor(pluginConfig){
@@ -16,7 +10,7 @@ import { normalizePath, pipe } from './server/shared';
16
10
  }
17
11
  init() {
18
12
  return async (config)=>{
19
- const { access, translationProvider, basePath: rawBasePath = '/translate' } = this.pluginConfig;
13
+ const { access, translationProvider, runner, collections, levels, basePath: rawBasePath = "/translate" } = this.pluginConfig;
20
14
  // Build schema map from deep-cloned collections
21
15
  // Deep clone is required because Payload mutates the original collection objects,
22
16
  // removing `localized: true` from nested fields during sanitization.
@@ -25,14 +19,14 @@ import { normalizePath, pipe } from './server/shared';
25
19
  // TODO: Consider introducing a FieldLike interface with only the properties
26
20
  // used by the pipeline (name, type, localized, fields, blocks, tabs, custom)
27
21
  // to make the contract explicit and avoid reliance on JSON round-trip.
28
- const schemaMap = new Map(this.pluginConfig.collections.map((col)=>[
22
+ const schemaMap = new Map(collections.map((col)=>[
29
23
  col.slug,
30
24
  JSON.parse(JSON.stringify(col.fields))
31
25
  ]));
32
26
  const collectionSlugs = new Set(schemaMap.keys());
33
27
  const basePath = normalizePath(rawBasePath);
34
28
  const translateHandler = new TranslateDocumentHandler(translationProvider, schemaMap);
35
- const runnerConfigModifier = this.pluginConfig.runner.configure({
29
+ const runnerContext = {
36
30
  handler: async (payload, input)=>{
37
31
  await translateHandler.handle(payload, {
38
32
  collection: input.collection,
@@ -44,50 +38,33 @@ import { normalizePath, pipe } from './server/shared';
44
38
  });
45
39
  },
46
40
  collections: Array.from(collectionSlugs)
41
+ };
42
+ const runnerConfigModifier = runner.configure(runnerContext);
43
+ // Bind the context once so routes receive a self-sufficient factory: the
44
+ // runner needs no mutable per-instance handler state and create() has no
45
+ // "configure() must run first" ordering coupling (translator plan, 0c).
46
+ const taskRunnerFactory = {
47
+ create: (payload)=>runner.create(payload, runnerContext.handler)
48
+ };
49
+ const activeLevels = levels ?? [
50
+ documentLevel(),
51
+ collectionLevel()
52
+ ];
53
+ const builder = new PluginConfigBuilder({
54
+ collections,
55
+ basePath,
56
+ access,
57
+ taskRunnerFactory
47
58
  });
48
- return pipe(// 1. Set up UI component provider import
49
- (cfg)=>{
50
- if (!cfg.admin) cfg.admin = {};
51
- if (!cfg.admin.components) cfg.admin.components = {};
52
- if (!cfg.admin.components.providers) cfg.admin.components.providers = [];
53
- cfg.admin.components.providers.push(new CacheProviderExport(basePath));
54
- return cfg;
55
- }, // 2. Set up UI component import
56
- (cfg)=>{
57
- cfg.collections?.forEach((collection)=>{
58
- if (!collectionSlugs.has(collection.slug)) {
59
- return;
60
- }
61
- if (!collection.admin) collection.admin = {};
62
- if (!collection.admin.components) collection.admin.components = {};
63
- if (!collection.admin.components.edit) collection.admin.components.edit = {};
64
- if (!collection.admin.components.edit.beforeDocumentControls) {
65
- collection.admin.components.edit.beforeDocumentControls = [];
66
- }
67
- if (!collection.admin.components.beforeListTable) {
68
- collection.admin.components.beforeListTable = [];
69
- }
70
- collection.admin.components.edit.beforeDocumentControls.push(new TranslateDocumentExport(collection, access));
71
- collection.admin.components.beforeListTable.push(new BulkDocumentTranslationDashboard(access));
72
- });
73
- return cfg;
74
- }, // 3. Runner configures jobs/tasks/autorun
75
- runnerConfigModifier, // 4. Add global endpoints
76
- (cfg)=>{
77
- if (!cfg.endpoints) cfg.endpoints = [];
78
- const collectionConfig = {
79
- availableCollections: collectionSlugs
80
- };
81
- cfg.endpoints.push(...[
82
- createEnqueueRoute(this.pluginConfig.runner, collectionConfig, access, basePath),
83
- createRunRoute(this.pluginConfig.runner, access, basePath),
84
- createCancelRoute(this.pluginConfig.runner, access, basePath),
85
- createCancelByCollectionRoute(collectionConfig, this.pluginConfig.runner, access, basePath),
86
- createGetDocumentStatusRoute(collectionConfig, this.pluginConfig.runner, access, basePath),
87
- createGetCollectionStatusRoute(collectionConfig, this.pluginConfig.runner, access, basePath)
88
- ]);
89
- return cfg;
90
- })(config);
59
+ for (const level of activeLevels)level.extend(builder);
60
+ // Plugin-level contributions, routed through the same single config-writer:
61
+ // - the runner's jobs/autorun/onInit modifier (the builder applies it first,
62
+ // so a modifier returning a fresh config object doesn't drop later writes),
63
+ // - the always-on client cache provider.
64
+ builder.addConfigModifier(runnerConfigModifier);
65
+ builder.addAdminProvider(new CacheProviderExport(basePath));
66
+ // The single place the Payload config is mutated.
67
+ return builder.applyTo(config);
91
68
  };
92
69
  }
93
70
  }
@@ -1,10 +1,10 @@
1
1
  import type { PayloadRequest } from "payload";
2
- import type { TaskRunnerProvider } from "../../modules/task-runner";
2
+ import type { TaskRunnerFactory } from "../../modules/task-runner";
3
3
  /**
4
4
  * Cancels and deletes translation tasks by IDs
5
5
  */
6
6
  export declare class CancelHandler {
7
7
  private readonly taskRunnerFactory;
8
- constructor(taskRunnerFactory: TaskRunnerProvider);
8
+ constructor(taskRunnerFactory: TaskRunnerFactory);
9
9
  handle(req: PayloadRequest): Promise<Response>;
10
10
  }
@@ -9,7 +9,9 @@ import { CancelInputSchema } from "./model";
9
9
  }
10
10
  async handle(req) {
11
11
  const validationResult = CancelInputSchema.safeParse(await req.json?.());
12
- if (validationResult.error) return ServerResponse.validationError(validationResult.error.issues);
12
+ if (validationResult.error) {
13
+ return ServerResponse.validationError(validationResult.error.issues);
14
+ }
13
15
  const { ids } = validationResult.data;
14
16
  const runner = this.taskRunnerFactory.create(req.payload);
15
17
  await runner.cancel(ids);
@@ -1,7 +1,7 @@
1
- import type { Endpoint } from 'payload';
2
- import type { AccessGuard } from '../../shared';
3
- import type { TaskRunnerProvider } from '../../modules/task-runner';
1
+ import type { Endpoint } from "payload";
2
+ import type { AccessGuard } from "../../shared";
3
+ import type { TaskRunnerFactory } from "../../modules/task-runner";
4
4
  /**
5
5
  * Creates the batch cancel endpoint
6
6
  */
7
- export declare function createCancelRoute(taskRunnerFactory: TaskRunnerProvider, access?: AccessGuard, basePath?: string): Endpoint;
7
+ export declare function createCancelRoute(taskRunnerFactory: TaskRunnerFactory, access?: AccessGuard, basePath?: string): Endpoint;