@focus-reactive/payload-plugin-translator 0.1.5 → 0.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 +80 -92
- package/dist/client/features/collection-translation-form/model/form-model.d.ts +2 -2
- package/dist/client/features/collection-translation-form/model/form-model.js +13 -10
- package/dist/client/features/translate-document-form/model/form-model.d.ts +2 -2
- package/dist/client/features/translate-document-form/model/form-model.js +13 -10
- package/dist/server/features/cancel/model.d.ts +4 -4
- package/dist/server/features/cancel/model.js +4 -3
- package/dist/server/features/get-document-status/model.d.ts +10 -6
- package/dist/server/features/get-document-status/model.js +8 -5
- package/dist/server/features/run-translation/model.d.ts +8 -4
- package/dist/server/features/run-translation/model.js +8 -3
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.d.ts +80 -4
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +98 -38
- package/dist/server/shared/index.d.ts +11 -10
- package/dist/server/shared/index.js +8 -6
- package/dist/server/shared/validation/index.d.ts +1 -0
- package/dist/server/shared/validation/index.js +3 -0
- package/dist/server/shared/validation/jobId.d.ts +17 -0
- package/dist/server/shared/validation/jobId.js +23 -0
- package/package.json +5 -10
package/README.md
CHANGED
|
@@ -32,14 +32,14 @@ yarn add @focus-reactive/payload-plugin-translator
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
34
34
|
```typescript
|
|
35
|
-
import { buildConfig } from
|
|
35
|
+
import { buildConfig } from "payload";
|
|
36
36
|
import {
|
|
37
37
|
translatorPlugin,
|
|
38
38
|
createOpenAIProvider,
|
|
39
39
|
createPayloadJobsRunner,
|
|
40
|
-
} from
|
|
41
|
-
import { Posts } from
|
|
42
|
-
import { Pages } from
|
|
40
|
+
} from "@focus-reactive/payload-plugin-translator";
|
|
41
|
+
import { Posts } from "./collections/Posts";
|
|
42
|
+
import { Pages } from "./collections/Pages";
|
|
43
43
|
|
|
44
44
|
export default buildConfig({
|
|
45
45
|
collections: [Posts, Pages],
|
|
@@ -53,10 +53,10 @@ export default buildConfig({
|
|
|
53
53
|
}),
|
|
54
54
|
],
|
|
55
55
|
localization: {
|
|
56
|
-
locales: [
|
|
57
|
-
defaultLocale:
|
|
56
|
+
locales: ["en", "de", "fr"],
|
|
57
|
+
defaultLocale: "en",
|
|
58
58
|
},
|
|
59
|
-
})
|
|
59
|
+
});
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Configuration
|
|
@@ -65,7 +65,6 @@ export default buildConfig({
|
|
|
65
65
|
|
|
66
66
|
Configuration for `translatorPlugin()`.
|
|
67
67
|
|
|
68
|
-
|
|
69
68
|
| Property | Type | Required | Default | Description |
|
|
70
69
|
| --------------------- | --------------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
71
70
|
| `collections` | `CollectionConfig[]` | Yes | — | Original collection configs to enable translation for. Must be the same objects passed to `buildConfig`, not slugs. |
|
|
@@ -74,38 +73,37 @@ Configuration for `translatorPlugin()`.
|
|
|
74
73
|
| `access` | `AccessGuard` | No | `undefined` | Access control function for translation endpoints |
|
|
75
74
|
| `basePath` | `string` | No | `'/translate'` | Base path for all API endpoints |
|
|
76
75
|
|
|
77
|
-
|
|
78
76
|
```typescript
|
|
79
77
|
translatorPlugin({
|
|
80
78
|
collections: [Posts, Pages],
|
|
81
|
-
translationProvider: createOpenAIProvider({
|
|
79
|
+
translationProvider: createOpenAIProvider({
|
|
80
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
81
|
+
}),
|
|
82
82
|
runner: createPayloadJobsRunner(),
|
|
83
|
-
access: async ({ req }) => req.user?.role ===
|
|
84
|
-
basePath:
|
|
85
|
-
})
|
|
83
|
+
access: async ({ req }) => req.user?.role === "admin",
|
|
84
|
+
basePath: "/translate",
|
|
85
|
+
});
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### OpenAIProviderConfig
|
|
89
89
|
|
|
90
90
|
Configuration for `createOpenAIProvider()`.
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `dryRun` | `boolean | DryRunConfig` | No | `false` | Simulate translations without API calls |
|
|
99
|
-
|
|
92
|
+
| Property | Type | Required | Default | Description |
|
|
93
|
+
| -------------- | ------------------------- | -------- | --------------- | ------------------------------------------ |
|
|
94
|
+
| `apiKey` | `string` | Yes | — | OpenAI API key |
|
|
95
|
+
| `model` | `string \| ChatModel` | No | `'gpt-4o'` | OpenAI model to use for translation |
|
|
96
|
+
| `systemPrompt` | `SystemPromptBuilder` | No | Built-in prompt | Custom function to build the system prompt |
|
|
97
|
+
| `dryRun` | `boolean \| DryRunConfig` | No | `false` | Simulate translations without API calls |
|
|
100
98
|
|
|
101
99
|
```typescript
|
|
102
100
|
createOpenAIProvider({
|
|
103
101
|
apiKey: process.env.OPENAI_API_KEY,
|
|
104
|
-
model:
|
|
102
|
+
model: "gpt-4o-mini",
|
|
105
103
|
systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) =>
|
|
106
104
|
`${defaultPrompt}\nUse formal language. Keep brand names unchanged.`,
|
|
107
105
|
dryRun: false,
|
|
108
|
-
})
|
|
106
|
+
});
|
|
109
107
|
```
|
|
110
108
|
|
|
111
109
|
#### SystemPromptBuilder
|
|
@@ -113,13 +111,13 @@ createOpenAIProvider({
|
|
|
113
111
|
Function signature for custom system prompt:
|
|
114
112
|
|
|
115
113
|
```typescript
|
|
116
|
-
type SystemPromptBuilder = (context: SystemPromptContext) => string
|
|
114
|
+
type SystemPromptBuilder = (context: SystemPromptContext) => string;
|
|
117
115
|
|
|
118
116
|
type SystemPromptContext = {
|
|
119
|
-
sourceLang: string
|
|
120
|
-
targetLang: string
|
|
121
|
-
defaultPrompt: string
|
|
122
|
-
}
|
|
117
|
+
sourceLang: string;
|
|
118
|
+
targetLang: string;
|
|
119
|
+
defaultPrompt: string;
|
|
120
|
+
};
|
|
123
121
|
```
|
|
124
122
|
|
|
125
123
|
#### DryRunConfig
|
|
@@ -127,64 +125,61 @@ type SystemPromptContext = {
|
|
|
127
125
|
When `dryRun` is an object, it allows custom transformation with optional delay:
|
|
128
126
|
|
|
129
127
|
```typescript
|
|
130
|
-
type DryRunTransformer = (text: string) => string | Promise<string
|
|
128
|
+
type DryRunTransformer = (text: string) => string | Promise<string>;
|
|
131
129
|
|
|
132
130
|
type DryRunConfig = {
|
|
133
|
-
transform: DryRunTransformer // Custom transformer function
|
|
134
|
-
timeout?: number // Delay in ms (simulates API latency)
|
|
135
|
-
}
|
|
131
|
+
transform: DryRunTransformer; // Custom transformer function
|
|
132
|
+
timeout?: number; // Delay in ms (simulates API latency)
|
|
133
|
+
};
|
|
136
134
|
```
|
|
137
135
|
|
|
138
136
|
### PayloadJobsRunnerOptions
|
|
139
137
|
|
|
140
138
|
Configuration for `createPayloadJobsRunner()`.
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `autoRun` | `false | { cron, limit }` | No | `{ cron: '* * * * *', limit: 50 }` | Auto-run config, or `false` to disable (for serverless) |
|
|
148
|
-
|
|
140
|
+
| Property | Type | Required | Default | Description |
|
|
141
|
+
| ----------- | -------------------------- | -------- | ---------------------------------- | ------------------------------------------------------- |
|
|
142
|
+
| `taskName` | `string` | No | `'translate_document'` | Task name in Payload jobs collection |
|
|
143
|
+
| `queueName` | `string` | No | `'translations'` | Queue name for grouping jobs |
|
|
144
|
+
| `autoRun` | `false \| { cron, limit }` | No | `{ cron: '* * * * *', limit: 50 }` | Auto-run config, or `false` to disable (for serverless) |
|
|
149
145
|
|
|
150
146
|
```typescript
|
|
151
147
|
createPayloadJobsRunner({
|
|
152
|
-
taskName:
|
|
153
|
-
queueName:
|
|
148
|
+
taskName: "translate_document",
|
|
149
|
+
queueName: "translations",
|
|
154
150
|
autoRun: {
|
|
155
|
-
cron:
|
|
151
|
+
cron: "* * * * *",
|
|
156
152
|
limit: 50,
|
|
157
153
|
},
|
|
158
|
-
})
|
|
154
|
+
});
|
|
159
155
|
```
|
|
160
156
|
|
|
161
157
|
### FieldTranslationConfig
|
|
162
158
|
|
|
163
159
|
Configuration for `withFieldTranslation()` helper or `field.custom.translateKit`.
|
|
164
160
|
|
|
165
|
-
|
|
166
161
|
| Property | Type | Required | Default | Description |
|
|
167
162
|
| --------- | --------- | -------- | ------- | ----------------------------------- |
|
|
168
163
|
| `exclude` | `boolean` | No | `false` | Exclude this field from translation |
|
|
169
164
|
|
|
170
|
-
|
|
171
165
|
```typescript
|
|
172
|
-
import { withFieldTranslation } from
|
|
166
|
+
import { withFieldTranslation } from "@focus-reactive/payload-plugin-translator";
|
|
173
167
|
|
|
174
|
-
withFieldTranslation(
|
|
168
|
+
withFieldTranslation(
|
|
169
|
+
{ name: "sku", type: "text", localized: true },
|
|
170
|
+
{ exclude: true },
|
|
171
|
+
);
|
|
175
172
|
```
|
|
176
173
|
|
|
177
174
|
## Translation Strategies
|
|
178
175
|
|
|
179
176
|
When translating, you can choose how to handle existing translations:
|
|
180
177
|
|
|
181
|
-
|
|
182
178
|
| Strategy | Description |
|
|
183
179
|
| ----------------- | ------------------------------------------------------------------------ |
|
|
184
180
|
| `'overwrite'` | (Default) Replaces all existing translated content with new translations |
|
|
185
181
|
| `'skip_existing'` | Only translates fields that are empty in the target locale |
|
|
186
182
|
|
|
187
|
-
|
|
188
183
|
## Important Notes
|
|
189
184
|
|
|
190
185
|
### Explicit `localized: true` for nested fields
|
|
@@ -239,7 +234,7 @@ export default buildConfig({
|
|
|
239
234
|
jobs: {
|
|
240
235
|
deleteJobOnComplete: false,
|
|
241
236
|
},
|
|
242
|
-
})
|
|
237
|
+
});
|
|
243
238
|
```
|
|
244
239
|
|
|
245
240
|
## Task Runners
|
|
@@ -249,16 +244,16 @@ export default buildConfig({
|
|
|
249
244
|
Uses Payload's built-in job queue for background processing:
|
|
250
245
|
|
|
251
246
|
```typescript
|
|
252
|
-
import { createPayloadJobsRunner } from
|
|
247
|
+
import { createPayloadJobsRunner } from "@focus-reactive/payload-plugin-translator";
|
|
253
248
|
|
|
254
249
|
const runner = createPayloadJobsRunner({
|
|
255
|
-
taskName:
|
|
256
|
-
queueName:
|
|
250
|
+
taskName: "translate_document",
|
|
251
|
+
queueName: "translations",
|
|
257
252
|
autoRun: {
|
|
258
|
-
cron:
|
|
253
|
+
cron: "* * * * *",
|
|
259
254
|
limit: 50,
|
|
260
255
|
},
|
|
261
|
-
})
|
|
256
|
+
});
|
|
262
257
|
```
|
|
263
258
|
|
|
264
259
|
### SyncRunner
|
|
@@ -266,9 +261,9 @@ const runner = createPayloadJobsRunner({
|
|
|
266
261
|
Executes translations synchronously (useful for development or small datasets):
|
|
267
262
|
|
|
268
263
|
```typescript
|
|
269
|
-
import { createSyncRunner } from
|
|
264
|
+
import { createSyncRunner } from "@focus-reactive/payload-plugin-translator";
|
|
270
265
|
|
|
271
|
-
const runner = createSyncRunner()
|
|
266
|
+
const runner = createSyncRunner();
|
|
272
267
|
```
|
|
273
268
|
|
|
274
269
|
## Translation Providers
|
|
@@ -278,13 +273,13 @@ const runner = createSyncRunner()
|
|
|
278
273
|
Built-in provider using OpenAI's API:
|
|
279
274
|
|
|
280
275
|
```typescript
|
|
281
|
-
import { createOpenAIProvider } from
|
|
276
|
+
import { createOpenAIProvider } from "@focus-reactive/payload-plugin-translator";
|
|
282
277
|
|
|
283
278
|
const provider = createOpenAIProvider({
|
|
284
279
|
apiKey: process.env.OPENAI_API_KEY,
|
|
285
|
-
model:
|
|
280
|
+
model: "gpt-4o-mini",
|
|
286
281
|
systemPrompt: ({ defaultPrompt }) => `${defaultPrompt}\nUse formal language.`,
|
|
287
|
-
})
|
|
282
|
+
});
|
|
288
283
|
```
|
|
289
284
|
|
|
290
285
|
### Custom Provider
|
|
@@ -293,11 +288,9 @@ Create your own translation provider by implementing the `TranslationProvider` i
|
|
|
293
288
|
|
|
294
289
|
#### TranslationProvider Interface
|
|
295
290
|
|
|
296
|
-
|
|
297
|
-
|
|
|
298
|
-
|
|
|
299
|
-
| `translate` | `(content: TranslationInput, sourceLng: string, targetLng: string) => Promise<TranslationOutput | null>` | Translates content from source to target language |
|
|
300
|
-
|
|
291
|
+
| Method | Signature | Description |
|
|
292
|
+
| ----------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
293
|
+
| `translate` | `(content: TranslationInput, sourceLng: string, targetLng: string) => Promise<TranslationOutput \| null>` | Translates content from source to target language |
|
|
301
294
|
|
|
302
295
|
**Important:** The numeric indices in `TranslationInput` must be preserved exactly in `TranslationOutput`. Each index maps to a specific field in the document structure, so the provider must return the same keys with translated values.
|
|
303
296
|
|
|
@@ -305,13 +298,13 @@ Create your own translation provider by implementing the `TranslationProvider` i
|
|
|
305
298
|
|
|
306
299
|
```typescript
|
|
307
300
|
// Numeric index representing position in document structure
|
|
308
|
-
type TranslationIndex = number
|
|
301
|
+
type TranslationIndex = number;
|
|
309
302
|
|
|
310
303
|
// Input: Map of numeric indices to text strings
|
|
311
|
-
type TranslationInput = Record<TranslationIndex, string
|
|
304
|
+
type TranslationInput = Record<TranslationIndex, string>;
|
|
312
305
|
|
|
313
306
|
// Output: Same indices with translated values
|
|
314
|
-
type TranslationOutput = Record<TranslationIndex, string
|
|
307
|
+
type TranslationOutput = Record<TranslationIndex, string>;
|
|
315
308
|
```
|
|
316
309
|
|
|
317
310
|
#### Example Implementation
|
|
@@ -321,40 +314,44 @@ import type {
|
|
|
321
314
|
TranslationProvider,
|
|
322
315
|
TranslationInput,
|
|
323
316
|
TranslationOutput,
|
|
324
|
-
} from
|
|
317
|
+
} from "@focus-reactive/payload-plugin-translator";
|
|
325
318
|
|
|
326
319
|
class DeepLProvider implements TranslationProvider {
|
|
327
320
|
constructor(private apiKey: string) {}
|
|
328
321
|
|
|
329
|
-
async translate(
|
|
322
|
+
async translate(
|
|
323
|
+
content: TranslationInput,
|
|
324
|
+
sourceLng: string,
|
|
325
|
+
targetLng: string,
|
|
326
|
+
): Promise<TranslationOutput | null> {
|
|
330
327
|
try {
|
|
331
328
|
// content example: { "0": "Hello", "1": "World" }
|
|
332
|
-
const texts = Object.values(content)
|
|
329
|
+
const texts = Object.values(content);
|
|
333
330
|
|
|
334
|
-
const response = await fetch(
|
|
335
|
-
method:
|
|
331
|
+
const response = await fetch("https://api.deepl.com/v2/translate", {
|
|
332
|
+
method: "POST",
|
|
336
333
|
headers: {
|
|
337
334
|
Authorization: `DeepL-Auth-Key ${this.apiKey}`,
|
|
338
|
-
|
|
335
|
+
"Content-Type": "application/json",
|
|
339
336
|
},
|
|
340
337
|
body: JSON.stringify({
|
|
341
338
|
text: texts,
|
|
342
339
|
source_lang: sourceLng.toUpperCase(),
|
|
343
340
|
target_lang: targetLng.toUpperCase(),
|
|
344
341
|
}),
|
|
345
|
-
})
|
|
342
|
+
});
|
|
346
343
|
|
|
347
|
-
const data = await response.json()
|
|
344
|
+
const data = await response.json();
|
|
348
345
|
|
|
349
346
|
// Reconstruct the result with same keys
|
|
350
|
-
const result: TranslationOutput = {}
|
|
347
|
+
const result: TranslationOutput = {};
|
|
351
348
|
Object.keys(content).forEach((key, index) => {
|
|
352
|
-
result[key] = data.translations[index].text
|
|
353
|
-
})
|
|
349
|
+
result[key] = data.translations[index].text;
|
|
350
|
+
});
|
|
354
351
|
|
|
355
|
-
return result
|
|
352
|
+
return result;
|
|
356
353
|
} catch {
|
|
357
|
-
return null
|
|
354
|
+
return null;
|
|
358
355
|
}
|
|
359
356
|
}
|
|
360
357
|
}
|
|
@@ -364,7 +361,7 @@ translatorPlugin({
|
|
|
364
361
|
collections: [Posts],
|
|
365
362
|
translationProvider: new DeepLProvider(process.env.DEEPL_API_KEY),
|
|
366
363
|
runner: createPayloadJobsRunner(),
|
|
367
|
-
})
|
|
364
|
+
});
|
|
368
365
|
```
|
|
369
366
|
|
|
370
367
|
## UI Components
|
|
@@ -404,17 +401,9 @@ import type {
|
|
|
404
401
|
|
|
405
402
|
// Field config
|
|
406
403
|
FieldTranslationConfig,
|
|
407
|
-
} from
|
|
404
|
+
} from "@focus-reactive/payload-plugin-translator";
|
|
408
405
|
```
|
|
409
406
|
|
|
410
|
-
## Known Issues
|
|
411
|
-
|
|
412
|
-
### SQLite: Nested JSON queries not supported
|
|
413
|
-
|
|
414
|
-
SQLite adapter doesn't support nested JSON field queries like `{ 'input.collection.value': { equals: '5' } }`.
|
|
415
|
-
|
|
416
|
-
**Affected databases:** SQLite only
|
|
417
|
-
|
|
418
407
|
## Roadmap
|
|
419
408
|
|
|
420
409
|
Planned features for future releases:
|
|
@@ -423,4 +412,3 @@ Planned features for future releases:
|
|
|
423
412
|
- **Global translation dashboard** — Translate all collections at once from a single interface, with progress tracking across the entire CMS
|
|
424
413
|
- **Vercel Cron Jobs runner** — Built-in runner for seamless Vercel/serverless deployments without manual API route configuration
|
|
425
414
|
- Auto-translate on source change — Automatically trigger translation when the default locale content is updated
|
|
426
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UseFormReturn } from
|
|
2
|
-
import type { FormInput, FormValues } from
|
|
1
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
2
|
+
import type { FormInput, FormValues } from "./schema";
|
|
3
3
|
type UseFormReturn_ = {
|
|
4
4
|
form: UseFormReturn<FormValues>;
|
|
5
5
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import { zodResolver } from
|
|
3
|
-
import { useEffect, useMemo } from
|
|
4
|
-
import { useForm } from
|
|
5
|
-
import { defaultValues } from
|
|
6
|
-
import { validationSchema } from
|
|
1
|
+
"use client";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { defaultValues } from "./constants";
|
|
6
|
+
import { validationSchema } from "./schema";
|
|
7
7
|
export const useCollectionTranslationForm = ({ initialValues, disabled } = {})=>{
|
|
8
8
|
const defaultFormValues = useMemo(()=>({
|
|
9
9
|
...defaultValues,
|
|
@@ -14,15 +14,18 @@ export const useCollectionTranslationForm = ({ initialValues, disabled } = {})=>
|
|
|
14
14
|
const form = useForm({
|
|
15
15
|
defaultValues: defaultFormValues,
|
|
16
16
|
resolver: zodResolver(validationSchema),
|
|
17
|
-
mode:
|
|
17
|
+
mode: "onTouched",
|
|
18
18
|
disabled
|
|
19
19
|
});
|
|
20
|
+
const isFirstRender = useRef(true);
|
|
20
21
|
useEffect(()=>{
|
|
22
|
+
if (isFirstRender.current) {
|
|
23
|
+
isFirstRender.current = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
form.reset(defaultFormValues);
|
|
22
27
|
}, [
|
|
23
|
-
defaultFormValues
|
|
24
|
-
form,
|
|
25
|
-
form.reset
|
|
28
|
+
defaultFormValues
|
|
26
29
|
]);
|
|
27
30
|
return {
|
|
28
31
|
form
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UseFormReturn } from
|
|
2
|
-
import type { FormInput, FormValues } from
|
|
1
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
2
|
+
import type { FormInput, FormValues } from "./schema";
|
|
3
3
|
type UseFormReturn_ = {
|
|
4
4
|
form: UseFormReturn<FormValues>;
|
|
5
5
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import { zodResolver } from
|
|
3
|
-
import { useEffect, useMemo } from
|
|
4
|
-
import { useForm } from
|
|
5
|
-
import { defaultValues } from
|
|
6
|
-
import { validationSchema } from
|
|
1
|
+
"use client";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { defaultValues } from "./constants";
|
|
6
|
+
import { validationSchema } from "./schema";
|
|
7
7
|
export const useTranslateDocumentForm = ({ initialValues, disabled } = {})=>{
|
|
8
8
|
const defaultFormValues = useMemo(()=>({
|
|
9
9
|
...defaultValues,
|
|
@@ -14,15 +14,18 @@ export const useTranslateDocumentForm = ({ initialValues, disabled } = {})=>{
|
|
|
14
14
|
const form = useForm({
|
|
15
15
|
defaultValues: defaultFormValues,
|
|
16
16
|
resolver: zodResolver(validationSchema),
|
|
17
|
-
mode:
|
|
17
|
+
mode: "onTouched",
|
|
18
18
|
disabled
|
|
19
19
|
});
|
|
20
|
+
const isFirstRender = useRef(true);
|
|
20
21
|
useEffect(()=>{
|
|
22
|
+
if (isFirstRender.current) {
|
|
23
|
+
isFirstRender.current = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
form.reset(defaultFormValues);
|
|
22
27
|
}, [
|
|
23
|
-
defaultFormValues
|
|
24
|
-
form,
|
|
25
|
-
form.reset
|
|
28
|
+
defaultFormValues
|
|
26
29
|
]);
|
|
27
30
|
return {
|
|
28
31
|
form
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
/**
|
|
3
|
-
* Input validation schema for batch cancel
|
|
3
|
+
* Input validation schema for batch cancel.
|
|
4
4
|
*/
|
|
5
5
|
export declare const CancelInputSchema: z.ZodObject<{
|
|
6
|
-
ids: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
ids: z.ZodArray<z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>, "many">;
|
|
7
7
|
}, "strip", z.ZodTypeAny, {
|
|
8
8
|
ids: string[];
|
|
9
9
|
}, {
|
|
10
|
-
ids: string[];
|
|
10
|
+
ids: (string | number)[];
|
|
11
11
|
}>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { JobIdSchema } from "../../shared";
|
|
2
3
|
/**
|
|
3
|
-
* Input validation schema for batch cancel
|
|
4
|
+
* Input validation schema for batch cancel.
|
|
4
5
|
*/ export const CancelInputSchema = z.object({
|
|
5
|
-
ids: z.array(
|
|
6
|
+
ids: z.array(JobIdSchema).min(1)
|
|
6
7
|
});
|
|
7
8
|
|
|
8
9
|
//# sourceMappingURL=model.js.map
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import type { CollectionSlug } from
|
|
3
|
-
import type { Task, TaskStatus } from
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CollectionSlug } from "payload";
|
|
3
|
+
import type { Task, TaskStatus } from "../../modules/task-runner";
|
|
4
4
|
/**
|
|
5
|
-
* Input validation schema
|
|
5
|
+
* Input validation schema.
|
|
6
|
+
*
|
|
7
|
+
* `collection_id` accepts any of the shapes Payload allows as a document ID
|
|
8
|
+
* (integer autoincrement, UUID, MongoDB ObjectId, etc.). See JobIdSchema for
|
|
9
|
+
* the rationale.
|
|
6
10
|
*/
|
|
7
11
|
export declare const GetDocumentStatusInputSchema: z.ZodObject<{
|
|
8
|
-
collection_id: z.ZodEffects<z.ZodString, string, string>;
|
|
12
|
+
collection_id: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
|
|
9
13
|
collection_slug: z.ZodString;
|
|
10
14
|
}, "strip", z.ZodTypeAny, {
|
|
11
15
|
collection_slug: string;
|
|
12
16
|
collection_id: string;
|
|
13
17
|
}, {
|
|
14
18
|
collection_slug: string;
|
|
15
|
-
collection_id: string;
|
|
19
|
+
collection_id: string | number;
|
|
16
20
|
}>;
|
|
17
21
|
export type GetDocumentStatusInput = z.infer<typeof GetDocumentStatusInputSchema>;
|
|
18
22
|
/**
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { JobIdSchema } from "../../shared";
|
|
2
3
|
/**
|
|
3
|
-
* Input validation schema
|
|
4
|
+
* Input validation schema.
|
|
5
|
+
*
|
|
6
|
+
* `collection_id` accepts any of the shapes Payload allows as a document ID
|
|
7
|
+
* (integer autoincrement, UUID, MongoDB ObjectId, etc.). See JobIdSchema for
|
|
8
|
+
* the rationale.
|
|
4
9
|
*/ export const GetDocumentStatusInputSchema = z.object({
|
|
5
|
-
collection_id:
|
|
6
|
-
message: 'Required'
|
|
7
|
-
}),
|
|
10
|
+
collection_id: JobIdSchema,
|
|
8
11
|
collection_slug: z.string().nonempty()
|
|
9
12
|
});
|
|
10
13
|
/**
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
/**
|
|
3
|
-
* Input validation schema
|
|
3
|
+
* Input validation schema.
|
|
4
|
+
*
|
|
5
|
+
* `id` comes from the URL route param (always a string at the HTTP edge),
|
|
6
|
+
* but we reuse the canonical JobIdSchema so the contract stays consistent
|
|
7
|
+
* with batch cancel.
|
|
4
8
|
*/
|
|
5
9
|
export declare const RunInputSchema: z.ZodObject<{
|
|
6
|
-
id: z.ZodString
|
|
10
|
+
id: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
|
|
7
11
|
}, "strip", z.ZodTypeAny, {
|
|
8
12
|
id: string;
|
|
9
13
|
}, {
|
|
10
|
-
id: string;
|
|
14
|
+
id: string | number;
|
|
11
15
|
}>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { JobIdSchema } from "../../shared";
|
|
2
3
|
/**
|
|
3
|
-
* Input validation schema
|
|
4
|
+
* Input validation schema.
|
|
5
|
+
*
|
|
6
|
+
* `id` comes from the URL route param (always a string at the HTTP edge),
|
|
7
|
+
* but we reuse the canonical JobIdSchema so the contract stays consistent
|
|
8
|
+
* with batch cancel.
|
|
4
9
|
*/ export const RunInputSchema = z.object({
|
|
5
|
-
id:
|
|
10
|
+
id: JobIdSchema
|
|
6
11
|
});
|
|
7
12
|
|
|
8
13
|
//# sourceMappingURL=model.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Payload, CollectionSlug } from
|
|
2
|
-
import type { TaskRunner } from
|
|
3
|
-
import type { Task, TaskInput, RunResult } from
|
|
4
|
-
import type { PayloadJobsRunnerConfig } from
|
|
1
|
+
import type { Payload, CollectionSlug } from "payload";
|
|
2
|
+
import type { TaskRunner } from "../TaskRunner.interface";
|
|
3
|
+
import type { Task, TaskInput, RunResult } from "../types";
|
|
4
|
+
import type { PayloadJobsRunnerConfig } from "./types";
|
|
5
5
|
/**
|
|
6
6
|
* TaskRunner implementation using Payload Jobs.
|
|
7
7
|
*
|
|
@@ -14,6 +14,82 @@ export declare class PayloadJobsTaskRunner implements TaskRunner {
|
|
|
14
14
|
enqueue(tasks: TaskInput[]): Promise<void>;
|
|
15
15
|
cancel(taskIds: string[]): Promise<void>;
|
|
16
16
|
run(taskId: string): Promise<RunResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
19
|
+
*
|
|
20
|
+
* IMPORTANT — why we filter `collection.value` in memory instead of in WHERE
|
|
21
|
+
* ------------------------------------------------------------------------
|
|
22
|
+
* The "natural" implementation would be a single `payload.find` with the
|
|
23
|
+
* full where clause:
|
|
24
|
+
*
|
|
25
|
+
* where: {
|
|
26
|
+
* and: [
|
|
27
|
+
* { 'input.collection.relationTo': { equals: collectionSlug } },
|
|
28
|
+
* { 'input.collection.value': { in: documentIds } },
|
|
29
|
+
* ],
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* This DOES NOT work on SQLite (and is unreliable on any adapter) because
|
|
33
|
+
* of two compounding bugs in Payload's drizzle layer.
|
|
34
|
+
*
|
|
35
|
+
* 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
|
|
36
|
+
* drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
|
|
37
|
+
* has no `case 'json'` branch, so the value is left as a raw column and
|
|
38
|
+
* the path segments are passed through to `parseParams.js`, which on
|
|
39
|
+
* SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
|
|
40
|
+
* expressions like `input->>'collection'->>'value'`.
|
|
41
|
+
*
|
|
42
|
+
* 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
|
|
43
|
+
* (and even `equals` when `!isNaN(val)`), it inlines values via JS
|
|
44
|
+
* template literals WITHOUT wrapping strings in quotes. The string
|
|
45
|
+
* `'1'` from our WHERE becomes raw `1` in the SQL. Drizzle therefore
|
|
46
|
+
* emits queries like:
|
|
47
|
+
*
|
|
48
|
+
* WHERE input->>'collection'->>'value' IN (1)
|
|
49
|
+
*
|
|
50
|
+
* even though the caller passed `['1']` (an array of strings).
|
|
51
|
+
*
|
|
52
|
+
* On SQLite, `->>` preserves the JSON value's type — if the stored JSON
|
|
53
|
+
* has `"value": "1"` (a JSON string), `->>` returns SQLite TEXT `'1'`;
|
|
54
|
+
* if the JSON has `"value": 1` (a JSON number), `->>` returns INTEGER `1`.
|
|
55
|
+
* SQLite's `IN (...)` does NOT coerce between TEXT and INTEGER. So:
|
|
56
|
+
*
|
|
57
|
+
* TEXT '1' IN (1) → false (text vs integer, no match)
|
|
58
|
+
* INTEGER 1 IN ('1') → false (integer vs text, no match)
|
|
59
|
+
*
|
|
60
|
+
* Combined with bug #2 above, any value passed by the caller — even if
|
|
61
|
+
* we normalize it to a string on write — gets re-coerced to a number in
|
|
62
|
+
* the generated SQL and never matches the stored JSON.
|
|
63
|
+
*
|
|
64
|
+
* Postgres avoids most of this because `jsonb_path_query` returns text
|
|
65
|
+
* uniformly and PG's type coercion is more permissive, but the same
|
|
66
|
+
* un-quoted-string bug technically affects it too.
|
|
67
|
+
*
|
|
68
|
+
* Why we don't fix it upstream / patch the dep
|
|
69
|
+
* --------------------------------------------
|
|
70
|
+
* - This plugin is published to npm. Consumers install it with their own
|
|
71
|
+
* Payload version and would not receive any local `bun patch` /
|
|
72
|
+
* `patch-package` overrides on `@payloadcms/drizzle`. The plugin must
|
|
73
|
+
* work against vanilla Payload.
|
|
74
|
+
* - A PR to Payload core is the proper long-term fix, but the plugin
|
|
75
|
+
* cannot block on its merge/release cycle.
|
|
76
|
+
* - Forcing a non-numeric prefix on the stored ID (e.g., `"id:1"`) would
|
|
77
|
+
* work around bug #2, but bloats the data shape and breaks anything
|
|
78
|
+
* that reads `input.collection.value` expecting a plain id.
|
|
79
|
+
*
|
|
80
|
+
* Why in-memory filtering is acceptable here
|
|
81
|
+
* ------------------------------------------
|
|
82
|
+
* We narrow the SQL query to `taskSlug + relationTo` (both string
|
|
83
|
+
* equality, which Payload quotes correctly), then filter the result set
|
|
84
|
+
* by `collection.value` in JavaScript. Per-collection job sets are
|
|
85
|
+
* small (typically <100 rows; the plugin actively cancels superseded
|
|
86
|
+
* jobs so they don't accumulate), so a Set-membership check in JS is
|
|
87
|
+
* effectively free.
|
|
88
|
+
*
|
|
89
|
+
* If/when the upstream drizzle bug is fixed (or this plugin gains a
|
|
90
|
+
* mirror collection with indexed flat columns), this method can collapse
|
|
91
|
+
* back to a single SQL query.
|
|
92
|
+
*/
|
|
17
93
|
findByCollection(collectionSlug: CollectionSlug, documentIds?: Array<string | number>): Promise<Task[]>;
|
|
18
94
|
/**
|
|
19
95
|
* Group tasks by collection slug
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeJob } from
|
|
1
|
+
import { normalizeJob } from "./normalizeJob";
|
|
2
2
|
/**
|
|
3
3
|
* TaskRunner implementation using Payload Jobs.
|
|
4
4
|
*
|
|
@@ -13,21 +13,8 @@ import { normalizeJob } from './normalizeJob';
|
|
|
13
13
|
async enqueue(tasks) {
|
|
14
14
|
const byCollection = this.groupByCollection(tasks);
|
|
15
15
|
for (const [collectionSlug, items] of byCollection){
|
|
16
|
-
const documentIds = items.map((t)=>t.collectionId);
|
|
17
|
-
const existing = await this.
|
|
18
|
-
and: [
|
|
19
|
-
{
|
|
20
|
-
'input.collection.relationTo': {
|
|
21
|
-
equals: collectionSlug
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
'input.collection.value': {
|
|
26
|
-
in: documentIds
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
});
|
|
16
|
+
const documentIds = items.map((t)=>String(t.collectionId));
|
|
17
|
+
const existing = await this.findByCollection(collectionSlug, documentIds);
|
|
31
18
|
if (existing.length > 0) {
|
|
32
19
|
await this.cancelInternal(existing.map((t)=>t.id));
|
|
33
20
|
}
|
|
@@ -37,6 +24,15 @@ import { normalizeJob } from './normalizeJob';
|
|
|
37
24
|
queue: this.config.queueName,
|
|
38
25
|
input: {
|
|
39
26
|
collection: {
|
|
27
|
+
// Pass `value` through verbatim. The Payload Jobs `input` schema
|
|
28
|
+
// declares this as a `relationship` field, which validates the
|
|
29
|
+
// value's type against the target collection's ID type (number
|
|
30
|
+
// for autoincrement, string for uuid). Coercing to string here
|
|
31
|
+
// would silently fail validation for number-id collections and
|
|
32
|
+
// leave jobs stuck in processing without ever invoking the
|
|
33
|
+
// task handler. `findByCollection` reads back via in-memory
|
|
34
|
+
// filtering and normalizes both sides with String(...) for the
|
|
35
|
+
// comparison, so it does not need write-side normalization.
|
|
40
36
|
relationTo: task.collectionSlug,
|
|
41
37
|
value: task.collectionId
|
|
42
38
|
},
|
|
@@ -63,19 +59,19 @@ import { normalizeJob } from './normalizeJob';
|
|
|
63
59
|
if (!task) {
|
|
64
60
|
return {
|
|
65
61
|
success: false,
|
|
66
|
-
error:
|
|
62
|
+
error: "not_found"
|
|
67
63
|
};
|
|
68
64
|
}
|
|
69
65
|
if (task.completedAt) {
|
|
70
66
|
return {
|
|
71
67
|
success: false,
|
|
72
|
-
error:
|
|
68
|
+
error: "already_completed"
|
|
73
69
|
};
|
|
74
70
|
}
|
|
75
|
-
if (task.status ===
|
|
71
|
+
if (task.status === "running") {
|
|
76
72
|
return {
|
|
77
73
|
success: false,
|
|
78
|
-
error:
|
|
74
|
+
error: "already_running"
|
|
79
75
|
};
|
|
80
76
|
}
|
|
81
77
|
this.payload.jobs.runByID({
|
|
@@ -85,28 +81,92 @@ import { normalizeJob } from './normalizeJob';
|
|
|
85
81
|
success: true
|
|
86
82
|
};
|
|
87
83
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
86
|
+
*
|
|
87
|
+
* IMPORTANT — why we filter `collection.value` in memory instead of in WHERE
|
|
88
|
+
* ------------------------------------------------------------------------
|
|
89
|
+
* The "natural" implementation would be a single `payload.find` with the
|
|
90
|
+
* full where clause:
|
|
91
|
+
*
|
|
92
|
+
* where: {
|
|
93
|
+
* and: [
|
|
94
|
+
* { 'input.collection.relationTo': { equals: collectionSlug } },
|
|
95
|
+
* { 'input.collection.value': { in: documentIds } },
|
|
96
|
+
* ],
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* This DOES NOT work on SQLite (and is unreliable on any adapter) because
|
|
100
|
+
* of two compounding bugs in Payload's drizzle layer.
|
|
101
|
+
*
|
|
102
|
+
* 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
|
|
103
|
+
* drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
|
|
104
|
+
* has no `case 'json'` branch, so the value is left as a raw column and
|
|
105
|
+
* the path segments are passed through to `parseParams.js`, which on
|
|
106
|
+
* SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
|
|
107
|
+
* expressions like `input->>'collection'->>'value'`.
|
|
108
|
+
*
|
|
109
|
+
* 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
|
|
110
|
+
* (and even `equals` when `!isNaN(val)`), it inlines values via JS
|
|
111
|
+
* template literals WITHOUT wrapping strings in quotes. The string
|
|
112
|
+
* `'1'` from our WHERE becomes raw `1` in the SQL. Drizzle therefore
|
|
113
|
+
* emits queries like:
|
|
114
|
+
*
|
|
115
|
+
* WHERE input->>'collection'->>'value' IN (1)
|
|
116
|
+
*
|
|
117
|
+
* even though the caller passed `['1']` (an array of strings).
|
|
118
|
+
*
|
|
119
|
+
* On SQLite, `->>` preserves the JSON value's type — if the stored JSON
|
|
120
|
+
* has `"value": "1"` (a JSON string), `->>` returns SQLite TEXT `'1'`;
|
|
121
|
+
* if the JSON has `"value": 1` (a JSON number), `->>` returns INTEGER `1`.
|
|
122
|
+
* SQLite's `IN (...)` does NOT coerce between TEXT and INTEGER. So:
|
|
123
|
+
*
|
|
124
|
+
* TEXT '1' IN (1) → false (text vs integer, no match)
|
|
125
|
+
* INTEGER 1 IN ('1') → false (integer vs text, no match)
|
|
126
|
+
*
|
|
127
|
+
* Combined with bug #2 above, any value passed by the caller — even if
|
|
128
|
+
* we normalize it to a string on write — gets re-coerced to a number in
|
|
129
|
+
* the generated SQL and never matches the stored JSON.
|
|
130
|
+
*
|
|
131
|
+
* Postgres avoids most of this because `jsonb_path_query` returns text
|
|
132
|
+
* uniformly and PG's type coercion is more permissive, but the same
|
|
133
|
+
* un-quoted-string bug technically affects it too.
|
|
134
|
+
*
|
|
135
|
+
* Why we don't fix it upstream / patch the dep
|
|
136
|
+
* --------------------------------------------
|
|
137
|
+
* - This plugin is published to npm. Consumers install it with their own
|
|
138
|
+
* Payload version and would not receive any local `bun patch` /
|
|
139
|
+
* `patch-package` overrides on `@payloadcms/drizzle`. The plugin must
|
|
140
|
+
* work against vanilla Payload.
|
|
141
|
+
* - A PR to Payload core is the proper long-term fix, but the plugin
|
|
142
|
+
* cannot block on its merge/release cycle.
|
|
143
|
+
* - Forcing a non-numeric prefix on the stored ID (e.g., `"id:1"`) would
|
|
144
|
+
* work around bug #2, but bloats the data shape and breaks anything
|
|
145
|
+
* that reads `input.collection.value` expecting a plain id.
|
|
146
|
+
*
|
|
147
|
+
* Why in-memory filtering is acceptable here
|
|
148
|
+
* ------------------------------------------
|
|
149
|
+
* We narrow the SQL query to `taskSlug + relationTo` (both string
|
|
150
|
+
* equality, which Payload quotes correctly), then filter the result set
|
|
151
|
+
* by `collection.value` in JavaScript. Per-collection job sets are
|
|
152
|
+
* small (typically <100 rows; the plugin actively cancels superseded
|
|
153
|
+
* jobs so they don't accumulate), so a Set-membership check in JS is
|
|
154
|
+
* effectively free.
|
|
155
|
+
*
|
|
156
|
+
* If/when the upstream drizzle bug is fixed (or this plugin gains a
|
|
157
|
+
* mirror collection with indexed flat columns), this method can collapse
|
|
158
|
+
* back to a single SQL query.
|
|
159
|
+
*/ async findByCollection(collectionSlug, documentIds) {
|
|
160
|
+
const tasks = await this.findJobsInternal({
|
|
161
|
+
"input.collection.relationTo": {
|
|
104
162
|
equals: collectionSlug
|
|
105
163
|
}
|
|
106
|
-
}
|
|
107
|
-
return this.findJobsInternal(where, {
|
|
164
|
+
}, {
|
|
108
165
|
pagination: false
|
|
109
166
|
});
|
|
167
|
+
if (!documentIds?.length) return tasks;
|
|
168
|
+
const wanted = new Set(documentIds.map(String));
|
|
169
|
+
return tasks.filter((t)=>wanted.has(String(t.input.collectionId)));
|
|
110
170
|
}
|
|
111
171
|
/**
|
|
112
172
|
* Group tasks by collection slug
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export { ServerResponse, withErrorHandler, withAccessCheck } from
|
|
2
|
-
export { AnyAccessGuard } from
|
|
3
|
-
export type { AccessGuard, AccessGuardRequest, Handler } from
|
|
4
|
-
export { isEmpty, isObject, normalizePath, pipe, getByPath, setByPath, filterLocalizedFields } from
|
|
5
|
-
export type { TranslatableField } from
|
|
6
|
-
export { isTranslatableField, isLocalizedField, isRelationshipField, isTabsField, isBlockItem, hasFields, } from
|
|
7
|
-
export { isFieldExcludedFromTranslation, getTranslateKitFieldConfig } from
|
|
8
|
-
export type { TranslateKitFieldConfig } from
|
|
9
|
-
export { isSerializedLexicalRoot, isEmptyRichText, traverseLexicalTree, collectSerializedLexicalTextNodes, } from
|
|
10
|
-
export type { SerializedTextNodeRef } from
|
|
1
|
+
export { ServerResponse, withErrorHandler, withAccessCheck } from "./http";
|
|
2
|
+
export { AnyAccessGuard } from "./access";
|
|
3
|
+
export type { AccessGuard, AccessGuardRequest, Handler } from "./access";
|
|
4
|
+
export { isEmpty, isObject, normalizePath, pipe, getByPath, setByPath, filterLocalizedFields, } from "./utils";
|
|
5
|
+
export type { TranslatableField } from "./guards";
|
|
6
|
+
export { isTranslatableField, isLocalizedField, isRelationshipField, isTabsField, isBlockItem, hasFields, } from "./guards";
|
|
7
|
+
export { isFieldExcludedFromTranslation, getTranslateKitFieldConfig, } from "./field-config";
|
|
8
|
+
export type { TranslateKitFieldConfig } from "./field-config";
|
|
9
|
+
export { isSerializedLexicalRoot, isEmptyRichText, traverseLexicalTree, collectSerializedLexicalTextNodes, } from "./lexical";
|
|
10
|
+
export type { SerializedTextNodeRef } from "./lexical";
|
|
11
|
+
export { JobIdSchema } from "./validation";
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// HTTP utilities
|
|
2
|
-
export { ServerResponse, withErrorHandler, withAccessCheck } from
|
|
2
|
+
export { ServerResponse, withErrorHandler, withAccessCheck } from "./http";
|
|
3
3
|
// Access control
|
|
4
|
-
export { AnyAccessGuard } from
|
|
4
|
+
export { AnyAccessGuard } from "./access";
|
|
5
5
|
// General utilities
|
|
6
|
-
export { isEmpty, isObject, normalizePath, pipe, getByPath, setByPath, filterLocalizedFields } from
|
|
7
|
-
export { isTranslatableField, isLocalizedField, isRelationshipField, isTabsField, isBlockItem, hasFields } from
|
|
6
|
+
export { isEmpty, isObject, normalizePath, pipe, getByPath, setByPath, filterLocalizedFields } from "./utils";
|
|
7
|
+
export { isTranslatableField, isLocalizedField, isRelationshipField, isTabsField, isBlockItem, hasFields } from "./guards";
|
|
8
8
|
// Field config
|
|
9
|
-
export { isFieldExcludedFromTranslation, getTranslateKitFieldConfig } from
|
|
9
|
+
export { isFieldExcludedFromTranslation, getTranslateKitFieldConfig } from "./field-config";
|
|
10
10
|
// Lexical utilities
|
|
11
|
-
export { isSerializedLexicalRoot, isEmptyRichText, traverseLexicalTree, collectSerializedLexicalTextNodes } from
|
|
11
|
+
export { isSerializedLexicalRoot, isEmptyRichText, traverseLexicalTree, collectSerializedLexicalTextNodes } from "./lexical";
|
|
12
|
+
// Validation primitives
|
|
13
|
+
export { JobIdSchema } from "./validation";
|
|
12
14
|
|
|
13
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { JobIdSchema } from "./jobId";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for a Payload document/job ID over the HTTP boundary.
|
|
4
|
+
*
|
|
5
|
+
* Accepts the two shapes IDs can arrive in: a JS string (UUID, MongoDB
|
|
6
|
+
* ObjectId hex, custom string id) or a JS number (SQLite/Postgres integer
|
|
7
|
+
* autoincrement). Anything else (null, undefined, boolean, object, array)
|
|
8
|
+
* is rejected by the union itself. The schema normalizes to a non-empty
|
|
9
|
+
* string so callers can compare and Set-membership-check uniformly.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This is intentionally a wire-format guard, not a domain check. We
|
|
12
|
+
* do not pattern-match for UUID v4 or ObjectId here — different deployments
|
|
13
|
+
* use different ID formats, and a stricter regex would lock the plugin to
|
|
14
|
+
* one adapter. Callers that need DB-specific validation should layer it
|
|
15
|
+
* on top.
|
|
16
|
+
*/
|
|
17
|
+
export declare const JobIdSchema: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for a Payload document/job ID over the HTTP boundary.
|
|
4
|
+
*
|
|
5
|
+
* Accepts the two shapes IDs can arrive in: a JS string (UUID, MongoDB
|
|
6
|
+
* ObjectId hex, custom string id) or a JS number (SQLite/Postgres integer
|
|
7
|
+
* autoincrement). Anything else (null, undefined, boolean, object, array)
|
|
8
|
+
* is rejected by the union itself. The schema normalizes to a non-empty
|
|
9
|
+
* string so callers can compare and Set-membership-check uniformly.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This is intentionally a wire-format guard, not a domain check. We
|
|
12
|
+
* do not pattern-match for UUID v4 or ObjectId here — different deployments
|
|
13
|
+
* use different ID formats, and a stricter regex would lock the plugin to
|
|
14
|
+
* one adapter. Callers that need DB-specific validation should layer it
|
|
15
|
+
* on top.
|
|
16
|
+
*/ export const JobIdSchema = z.union([
|
|
17
|
+
z.string().refine((val)=>val.length > 0 && val !== "undefined", {
|
|
18
|
+
message: "ID must be a non-empty string"
|
|
19
|
+
}),
|
|
20
|
+
z.number().finite()
|
|
21
|
+
]).transform(String);
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=jobId.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@focus-reactive/payload-plugin-translator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "bun run clean && bun run build:swc && bun run build:types && bun run copyfiles",
|
|
49
49
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths --ignore '**/*.test.ts' --ignore '**/*.d.ts' --ignore '**/__tests__/**' --ignore '**/__mocks__/**'",
|
|
50
|
-
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
50
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
51
51
|
"copyfiles": "copyfiles -u 1 \"src/**/*.scss\" dist",
|
|
52
52
|
"clean": "rm -rf dist",
|
|
53
53
|
"dev": "bun run build:swc -- --watch",
|
|
54
|
-
"lint": "
|
|
55
|
-
"lint:fix": "
|
|
54
|
+
"lint": "ultracite check",
|
|
55
|
+
"lint:fix": "ultracite fix",
|
|
56
56
|
"test": "vitest run",
|
|
57
57
|
"test:watch": "vitest",
|
|
58
|
-
"check-types": "
|
|
58
|
+
"check-types": "tsgo --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@payloadcms/richtext-lexical": "^3.76.0",
|
|
@@ -84,14 +84,9 @@
|
|
|
84
84
|
"@types/react": "19.2.9",
|
|
85
85
|
"@vitest/coverage-v8": "3.2.4",
|
|
86
86
|
"copyfiles": "2.4.1",
|
|
87
|
-
"eslint": "9.0.0",
|
|
88
|
-
"eslint-config-prettier": "9.0.0",
|
|
89
|
-
"next": "15.4.11",
|
|
90
87
|
"payload": "3.79.0",
|
|
91
88
|
"react": "19.0.0",
|
|
92
89
|
"typescript": "5.5.2",
|
|
93
|
-
"eslint-plugin-react-hooks": "6.0.0-rc.2",
|
|
94
|
-
"typescript-eslint": "8.0.0",
|
|
95
90
|
"vitest": "3.2.4"
|
|
96
91
|
}
|
|
97
92
|
}
|