@focus-reactive/payload-plugin-translator 0.10.3 → 0.11.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 +152 -10
- package/dist/core/domain/locales/index.d.ts +6 -0
- package/dist/core/domain/locales/index.js +7 -0
- package/dist/core/domain/locales/resolveLocales.d.ts +59 -0
- package/dist/core/domain/locales/resolveLocales.js +68 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +4 -3
- package/dist/server/features/enqueue-translation/handler.js +2 -2
- package/dist/server/modules/auto-translate/AutoTranslate.policy.js +7 -8
- package/dist/translation-providers/index.d.ts +1 -0
- package/dist/translation-providers/index.js +1 -7
- package/dist/translation-providers/openai/OpenAI.shapes.d.ts +52 -0
- package/dist/translation-providers/openai/OpenAI.shapes.js +13 -0
- package/dist/translation-providers/openai/OpenAITranslation.provider.d.ts +51 -112
- package/dist/translation-providers/openai/OpenAITranslation.provider.js +49 -137
- package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.d.ts +11 -0
- package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.js +15 -0
- package/dist/translation-providers/openai/index.d.ts +6 -1
- package/dist/translation-providers/openai/index.js +3 -3
- package/dist/translation-providers/openai/loadOpenAIClient.d.ts +24 -0
- package/dist/translation-providers/openai/loadOpenAIClient.js +79 -0
- package/dist/translation-providers/openai/openAIComplete.d.ts +39 -0
- package/dist/translation-providers/openai/openAIComplete.js +84 -0
- package/dist/translation-providers/shared/CompletionProvider.provider.d.ts +65 -0
- package/dist/translation-providers/shared/CompletionProvider.provider.js +93 -0
- package/dist/translation-providers/shared/buildResponseSchema.d.ts +13 -0
- package/dist/translation-providers/shared/buildResponseSchema.js +22 -0
- package/dist/translation-providers/shared/buildSystemPrompt.d.ts +28 -0
- package/dist/translation-providers/shared/buildSystemPrompt.js +25 -0
- package/dist/translation-providers/shared/errors/KeySetMismatchError.d.ts +15 -0
- package/dist/translation-providers/shared/errors/KeySetMismatchError.js +26 -0
- package/dist/translation-providers/shared/errors/NoContentError.d.ts +9 -0
- package/dist/translation-providers/shared/errors/NoContentError.js +10 -0
- package/dist/translation-providers/shared/errors/ProviderConfigurationError.d.ts +10 -0
- package/dist/translation-providers/shared/errors/ProviderConfigurationError.js +11 -0
- package/dist/translation-providers/shared/errors/TranslationProviderError.d.ts +18 -0
- package/dist/translation-providers/shared/errors/TranslationProviderError.js +21 -0
- package/dist/translation-providers/shared/errors/TransportError.d.ts +10 -0
- package/dist/translation-providers/shared/errors/TransportError.js +11 -0
- package/dist/translation-providers/shared/errors/UnparseableReplyError.d.ts +9 -0
- package/dist/translation-providers/shared/errors/UnparseableReplyError.js +10 -0
- package/dist/translation-providers/shared/errors/errorMessageLower.d.ts +1 -0
- package/dist/translation-providers/shared/errors/errorMessageLower.js +8 -0
- package/dist/translation-providers/shared/errors/index.d.ts +9 -0
- package/dist/translation-providers/shared/errors/index.js +10 -0
- package/dist/translation-providers/shared/errors/wrapTransportError.d.ts +7 -0
- package/dist/translation-providers/shared/errors/wrapTransportError.js +14 -0
- package/dist/translation-providers/shared/index.d.ts +8 -0
- package/dist/translation-providers/shared/index.js +5 -0
- package/dist/translation-providers/shared/parseAndValidateReply.d.ts +13 -0
- package/dist/translation-providers/shared/parseAndValidateReply.js +46 -0
- package/dist/translation-providers/shared/runDryRun.d.ts +20 -0
- package/dist/translation-providers/shared/runDryRun.js +17 -0
- package/package.json +1 -1
- package/dist/server/features/enqueue-translation/resolveTargetLocales.d.ts +0 -30
- package/dist/server/features/enqueue-translation/resolveTargetLocales.js +0 -35
package/README.md
CHANGED
|
@@ -288,21 +288,86 @@ translatorPlugin({
|
|
|
288
288
|
|
|
289
289
|
#### OpenAI (built in) — `createOpenAIProvider(config)`
|
|
290
290
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
291
|
+
Pass **either** an `apiKey` **or** a ready-made `client` — never both; the types enforce it.
|
|
292
|
+
|
|
293
|
+
| Property | Type | Required | Default | Description |
|
|
294
|
+
| -------------- | ------------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
295
|
+
| `apiKey` | `string` | Unless `client` | — | OpenAI API key. The `openai` package is loaded on first translation, not at config load. _Since v0.11.0: optional when `client` is given._ |
|
|
296
|
+
| `client` | `OpenAIClientShape` | Unless `apiKey` | — | Your own client — Azure OpenAI, a corporate proxy, OpenRouter, anything with a matching `chat.completions.create`. On this path the `openai` package is never loaded. _Since v0.11.0._ |
|
|
297
|
+
| `model` | `string` | No | `'gpt-4o'` | Model used for translation. **The default may change in a minor release** — pin it if you need reproducible output and cost. |
|
|
298
|
+
| `systemPrompt` | `SystemPromptBuilder` | No | Built-in prompt | Custom system-prompt builder. |
|
|
299
|
+
| `dryRun` | `boolean \| DryRunConfig` | No | `false` | **Deprecated** — see the note below. Simulates translations without API calls, but still writes, publishes and records provenance. |
|
|
300
|
+
| `sampling` | `OpenAISamplingParams` | No | not sent | `temperature`, `top_p`, `frequency_penalty`, `presence_penalty`. Omitted entirely unless set — several models reject them. **Before v0.11.0 this package always sent `temperature: 0, top_p: 1, frequency_penalty: 0, presence_penalty: 0`**, so translations were deterministic; they now follow the model's defaults. Set `{ temperature: 0 }` to restore that. _Since v0.11.0._ |
|
|
301
|
+
| `structuredOutput` | `"json_schema" \| "json_object"` | No | `"json_schema"` | Which structured-output envelope to send. See the note below — the two carry different risks. _Since v0.11.0._ |
|
|
302
|
+
| `timeout` | `number` | No | `60000` | Per-request timeout (ms) for the client this package builds. Ignored when you pass your own `client`. _Since v0.6.0; the default dropped from the SDK's 10 minutes to 60 s in v0.11.0._ |
|
|
303
|
+
| `maxRetries` | `number` | No | SDK default (2) | Max automatic retries on transient errors (429/5xx/network) for the client this package builds. `0` disables. Ignored when you pass your own `client`. _Since v0.6.0._ |
|
|
304
|
+
|
|
305
|
+
> **`createOpenAIProvider` is deprecated and goes away in the next major.** What it adds over
|
|
306
|
+
> `openAIComplete` is building the SDK client for you — and carrying `openai` as an optional
|
|
307
|
+
> dependency of ours to do it, which is where the cost is: a lazy import shaped around deployment
|
|
308
|
+
> file-tracers, and a classifier telling "not installed" from "installed but broken" across four
|
|
309
|
+
> runtimes. Construct the client yourself instead and keep everything else:
|
|
310
|
+
>
|
|
311
|
+
> ```typescript
|
|
312
|
+
> import OpenAI from "openai";
|
|
313
|
+
> import {
|
|
314
|
+
> createTranslationProvider,
|
|
315
|
+
> openAIComplete,
|
|
316
|
+
> } from "@focus-reactive/payload-plugin-translator";
|
|
317
|
+
>
|
|
318
|
+
> // The SDK's own default timeout is ten minutes — far too long for a live edit.
|
|
319
|
+
> const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, timeout: 60_000 });
|
|
320
|
+
>
|
|
321
|
+
> const translationProvider = createTranslationProvider({
|
|
322
|
+
> complete: openAIComplete({ client, model: "gpt-4o" }),
|
|
323
|
+
> });
|
|
324
|
+
> ```
|
|
325
|
+
>
|
|
326
|
+
> `openAIComplete` stays: the request body, the `structuredOutput` choice below, and the message
|
|
327
|
+
> naming that option when a gateway rejects a strict schema are all still ours. What becomes yours
|
|
328
|
+
> is the SDK version and the client's own settings, the timeout most of all.
|
|
329
|
+
> See [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md#openai-client-construction).
|
|
330
|
+
|
|
331
|
+
#### Choosing a structured-output envelope
|
|
332
|
+
|
|
333
|
+
`json_schema` (the default) sends the request with a schema the reply must satisfy, so a compliant
|
|
334
|
+
model **cannot** drop a requested field. That is what closed the silent half-translation defect. It
|
|
335
|
+
costs two things:
|
|
336
|
+
|
|
337
|
+
- Older models, and some gateways (OpenRouter with certain upstream models, older Azure
|
|
338
|
+
deployments, self-hosted proxies), reject it with a 400. You do not need to know which: the error
|
|
339
|
+
names this option as the fix.
|
|
340
|
+
- The schema has a size limit, so **one request carries a limited number of pieces of text**, and a
|
|
341
|
+
document past that ceiling fails as a whole. The schema names one property per translatable piece,
|
|
342
|
+
and rich text is split one piece per text node — a sentence with two emphasised spans is already
|
|
343
|
+
four pieces — so the count grows faster than "one per field" suggests, and a document is never
|
|
344
|
+
split across requests. The ceiling differs by model and moves over time; measure it against your
|
|
345
|
+
largest documents rather than assuming headroom.
|
|
346
|
+
|
|
347
|
+
`json_object` asks only for valid JSON. There is no schema, so no ceiling — but key preservation
|
|
348
|
+
falls back to this package's key-set check, which **detects** a dropped key instead of preventing
|
|
349
|
+
it. A reply missing one key out of two hundred still writes the other 199, the gap is reported to
|
|
350
|
+
the server log, and only a reply matching nothing at all fails. Your editors never see that log, so
|
|
351
|
+
a dropped field looks translated in the admin UI.
|
|
352
|
+
|
|
353
|
+
Pick by which risk you would rather carry: a hard failure on very large documents, or a quiet gap on
|
|
354
|
+
any document.
|
|
299
355
|
|
|
300
356
|
```typescript
|
|
357
|
+
// Quick start
|
|
301
358
|
createOpenAIProvider({
|
|
302
359
|
apiKey: process.env.OPENAI_API_KEY,
|
|
303
360
|
model: "gpt-4o-mini",
|
|
304
361
|
systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) => `${defaultPrompt}\nUse formal language. Keep brand names unchanged.`,
|
|
305
362
|
});
|
|
363
|
+
|
|
364
|
+
// Your own client — Azure, a proxy, OpenRouter. Since v0.11.0.
|
|
365
|
+
import OpenAI from "openai";
|
|
366
|
+
|
|
367
|
+
createOpenAIProvider({
|
|
368
|
+
client: new OpenAI({ apiKey: process.env.AZURE_KEY, baseURL: process.env.AZURE_ENDPOINT }),
|
|
369
|
+
model: "gpt-4o",
|
|
370
|
+
});
|
|
306
371
|
```
|
|
307
372
|
|
|
308
373
|
`systemPrompt` receives `{ sourceLang, targetLang, defaultPrompt }` and returns the prompt string. When `dryRun` is an object it can transform text locally with an optional delay:
|
|
@@ -314,6 +379,81 @@ type DryRunConfig = {
|
|
|
314
379
|
};
|
|
315
380
|
```
|
|
316
381
|
|
|
382
|
+
> **`dryRun` is deprecated and will be removed in the next major.** It skips the network call and
|
|
383
|
+
> nothing else: the transformed strings are still written to the target locale, still published when
|
|
384
|
+
> `publishOnTranslation` is set, and still recorded as provenance — after which the locale reads as
|
|
385
|
+
> up to date and no re-translation is prompted. Use your own fake instead, which is explicit about
|
|
386
|
+
> being one:
|
|
387
|
+
>
|
|
388
|
+
> ```typescript
|
|
389
|
+
> import { createTranslationProvider } from "@focus-reactive/payload-plugin-translator";
|
|
390
|
+
>
|
|
391
|
+
> const fakeProvider = createTranslationProvider({
|
|
392
|
+
> complete: async ({ userContent }) => {
|
|
393
|
+
> const input = JSON.parse(userContent) as Record<string, string>;
|
|
394
|
+
> const reversed: Record<string, string> = {};
|
|
395
|
+
> for (const [key, value] of Object.entries(input)) {
|
|
396
|
+
> reversed[key] = value.trim() ? [...value].reverse().join("") : value;
|
|
397
|
+
> }
|
|
398
|
+
> return JSON.stringify(reversed);
|
|
399
|
+
> },
|
|
400
|
+
> });
|
|
401
|
+
> ```
|
|
402
|
+
>
|
|
403
|
+
> `complete` returns the reply as raw text, exactly as a service would; parsing and validation stay
|
|
404
|
+
> on our side. See [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md#provider-dry-run).
|
|
405
|
+
|
|
406
|
+
#### Another service — `createTranslationProvider(config)`
|
|
407
|
+
|
|
408
|
+
_Since v0.11.0._
|
|
409
|
+
|
|
410
|
+
Need a different model provider, or full control of the request body? Supply one function — "send this text, give me the reply" — and keep everything else. The prompt, the response schema, reply parsing, key-set validation, dry-run simulation and the failure taxonomy all stay on our side, so you cannot accidentally skip them.
|
|
411
|
+
|
|
412
|
+
For OpenAI specifically you do not have to write that function: `openAIComplete({ client, model })` is one, built from a client you constructed. _Since v0.11.0._
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { createTranslationProvider } from "@focus-reactive/payload-plugin-translator";
|
|
416
|
+
|
|
417
|
+
const provider = createTranslationProvider({
|
|
418
|
+
complete: async ({ systemPrompt, userContent, responseSchema }) => {
|
|
419
|
+
const reply = await myService.chat({
|
|
420
|
+
system: systemPrompt,
|
|
421
|
+
user: userContent,
|
|
422
|
+
schema: responseSchema, // hand this to whatever structured-output mechanism your service offers
|
|
423
|
+
});
|
|
424
|
+
return reply.text; // raw text, not a parsed object — we parse it
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
> `signal` is reserved and currently always `undefined` — the port does not carry cancellation yet, so wiring it into your client is harmless but has no effect today. It is in the request shape so that adding cancellation later is a pure addition rather than a breaking change.
|
|
430
|
+
|
|
431
|
+
| Property | Type | Required | Default | Description |
|
|
432
|
+
| -------------- | ------------------------- | -------- | --------------- | -------------------------------------------------- |
|
|
433
|
+
| `complete` | `CompletionFn` | Yes | — | Sends one request, returns the reply as raw text. |
|
|
434
|
+
| `systemPrompt` | `SystemPromptBuilder` | No | Built-in prompt | Custom system-prompt builder. |
|
|
435
|
+
| `dryRun` | `boolean \| DryRunConfig` | No | `false` | **Deprecated** — supply your own fake `complete`. |
|
|
436
|
+
|
|
437
|
+
Your `complete` owns the timeout, the retry policy and the credentials — this package adds no retry of its own and imposes no timeout on your call.
|
|
438
|
+
|
|
439
|
+
#### Failure causes
|
|
440
|
+
|
|
441
|
+
_Since v0.11.0._
|
|
442
|
+
|
|
443
|
+
Every built-in provider throws a typed error naming what went wrong, instead of returning `null`:
|
|
444
|
+
|
|
445
|
+
| Error | `code` | Means |
|
|
446
|
+
| --------------------------- | -------------------- | ---------------------------------------------------------------------- |
|
|
447
|
+
| `NoContentError` | `no-content` | The reply was empty, or the service filtered it. |
|
|
448
|
+
| `UnparseableReplyError` | `unparseable-reply` | The reply was not JSON, or not an object. |
|
|
449
|
+
| `KeySetMismatchError` | `key-set-mismatch` | The reply answered none of the requested fields. |
|
|
450
|
+
| `TransportError` | `transport` | The call failed — network, auth, rate limit, timeout. |
|
|
451
|
+
| `ProviderConfigurationError`| `config` | The provider cannot work as configured (usually a missing optional SDK). |
|
|
452
|
+
|
|
453
|
+
All extend `TranslationProviderError`, so one `catch` covers them. The original failure is on the standard `cause` property — never copied into `message`, because that text can reach an HTTP response body and a vendor error may carry your API key.
|
|
454
|
+
|
|
455
|
+
A **partial** reply is not an error: the fields that came back are applied, and the ones that did not are named in a warning. That is deliberate — dropping good translations because one field is missing helps nobody — but it does mean a partial translation still completes.
|
|
456
|
+
|
|
317
457
|
#### Custom provider
|
|
318
458
|
|
|
319
459
|
Implement the `TranslationProvider` interface — a single `translate` method:
|
|
@@ -341,8 +481,10 @@ class DeepLProvider implements TranslationProvider {
|
|
|
341
481
|
result[key] = data.translations[i].text;
|
|
342
482
|
});
|
|
343
483
|
return result;
|
|
344
|
-
} catch {
|
|
345
|
-
|
|
484
|
+
} catch (cause) {
|
|
485
|
+
// Throwing is preferred — the cause reaches the log and the editor sees why it failed.
|
|
486
|
+
// Returning `null` still works and still aborts the whole run, but it says nothing about why.
|
|
487
|
+
throw new Error("DeepL translation failed", { cause });
|
|
346
488
|
}
|
|
347
489
|
}
|
|
348
490
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace re-export: callers `import { Locales }` and use `Locales.dedupe(...)`,
|
|
3
|
+
* `Locales.resolveTargets(...)`, etc. Grouped this way (rather than loose named exports) so the domain
|
|
4
|
+
* reads at the call site; `export * as` keeps each member individually tree-shakeable.
|
|
5
|
+
*/
|
|
6
|
+
export * as Locales from "./resolveLocales";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace re-export: callers `import { Locales }` and use `Locales.dedupe(...)`,
|
|
3
|
+
* `Locales.resolveTargets(...)`, etc. Grouped this way (rather than loose named exports) so the domain
|
|
4
|
+
* reads at the call site; `export * as` keeps each member individually tree-shakeable.
|
|
5
|
+
*/ export * as Locales from "./resolveLocales";
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single, canonical home for the locale-set invariants shared by every enqueue path: dedup,
|
|
3
|
+
* unknown-locale dropping, and source-locale exclusion. Previously each was hand-written in 2–3 places
|
|
4
|
+
* (the manual `/enqueue` resolver, the auto-translate config-time filter, and the auto-translate runtime
|
|
5
|
+
* task builder), which risked silent drift if one copy changed. These are the primitives; each path
|
|
6
|
+
* composes the ones it needs at whatever phase it runs (manual: one shot at request time; auto: dedup +
|
|
7
|
+
* unknown-drop at config time, source-exclusion per-document at runtime).
|
|
8
|
+
*
|
|
9
|
+
* Consumed as a namespace — the barrel re-exports this module as `Locales`, so call sites read
|
|
10
|
+
* `Locales.dedupe(...)` / `Locales.resolveTargets(...)`. Members are named for that dotted form (no
|
|
11
|
+
* `Locales` suffix), since the namespace carries the domain.
|
|
12
|
+
*
|
|
13
|
+
* Payload-free (plain `string[]` / `Set<string>`), so it stays a pure kernel usable from both the
|
|
14
|
+
* server features and the auto-translate module without pulling framework types in. Not part of the
|
|
15
|
+
* plugin's public API — an implementation detail of the enqueue paths.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
/** Whether a single locale code is one of the configured locales. The atomic "is this known?" check
|
|
20
|
+
* that {@link dropUnknown} is built on — shared so "unknown locale" means one thing everywhere. */
|
|
21
|
+
export declare function isKnown(locale: string, known: Set<string>): boolean;
|
|
22
|
+
/** Remove duplicate locale codes, preserving first-seen order. */
|
|
23
|
+
export declare function dedupe(locales: string[]): string[];
|
|
24
|
+
/** Split a locale list into those that are configured (`kept`) and those that are not (`dropped`),
|
|
25
|
+
* preserving order within each. */
|
|
26
|
+
export declare function dropUnknown(locales: string[], known: Set<string>): {
|
|
27
|
+
kept: string[];
|
|
28
|
+
dropped: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Remove the source locale from a target list; report whether it was present (so the caller can note
|
|
31
|
+
* it was excluded). */
|
|
32
|
+
export declare function excludeSource(targets: string[], source: string): {
|
|
33
|
+
kept: string[];
|
|
34
|
+
wasPresent: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolved target locales for a manual enqueue, plus what was dropped so the handler can log precisely.
|
|
38
|
+
*/
|
|
39
|
+
export type ResolvedTargets = {
|
|
40
|
+
/** The concrete locales to fan out to — de-duplicated, source excluded, unknown removed. */
|
|
41
|
+
targets: string[];
|
|
42
|
+
/** Requested locales that are not configured (dropped). */
|
|
43
|
+
droppedUnknown: string[];
|
|
44
|
+
/** Whether the source locale was requested as a target and excluded. */
|
|
45
|
+
droppedSource: boolean;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* One-shot resolution for the manual enqueue path: coerce scalar→array, then dedup → exclude source →
|
|
49
|
+
* drop unknown, in that order. A pure composition of the primitives above.
|
|
50
|
+
*
|
|
51
|
+
* @param knownLocales - the configured locale codes. Never null: the caller must reject a
|
|
52
|
+
* localization-less config before reaching here (translating without localization has no valid target
|
|
53
|
+
* and would corrupt data), so "no localization" is not representable as an input.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveTargets(args: {
|
|
56
|
+
target_lng: string | string[];
|
|
57
|
+
source_lng: string;
|
|
58
|
+
knownLocales: Set<string>;
|
|
59
|
+
}): ResolvedTargets;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single, canonical home for the locale-set invariants shared by every enqueue path: dedup,
|
|
3
|
+
* unknown-locale dropping, and source-locale exclusion. Previously each was hand-written in 2–3 places
|
|
4
|
+
* (the manual `/enqueue` resolver, the auto-translate config-time filter, and the auto-translate runtime
|
|
5
|
+
* task builder), which risked silent drift if one copy changed. These are the primitives; each path
|
|
6
|
+
* composes the ones it needs at whatever phase it runs (manual: one shot at request time; auto: dedup +
|
|
7
|
+
* unknown-drop at config time, source-exclusion per-document at runtime).
|
|
8
|
+
*
|
|
9
|
+
* Consumed as a namespace — the barrel re-exports this module as `Locales`, so call sites read
|
|
10
|
+
* `Locales.dedupe(...)` / `Locales.resolveTargets(...)`. Members are named for that dotted form (no
|
|
11
|
+
* `Locales` suffix), since the namespace carries the domain.
|
|
12
|
+
*
|
|
13
|
+
* Payload-free (plain `string[]` / `Set<string>`), so it stays a pure kernel usable from both the
|
|
14
|
+
* server features and the auto-translate module without pulling framework types in. Not part of the
|
|
15
|
+
* plugin's public API — an implementation detail of the enqueue paths.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/ /** Whether a single locale code is one of the configured locales. The atomic "is this known?" check
|
|
19
|
+
* that {@link dropUnknown} is built on — shared so "unknown locale" means one thing everywhere. */ export function isKnown(locale, known) {
|
|
20
|
+
return known.has(locale);
|
|
21
|
+
}
|
|
22
|
+
/** Remove duplicate locale codes, preserving first-seen order. */ export function dedupe(locales) {
|
|
23
|
+
return [
|
|
24
|
+
...new Set(locales)
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
/** Split a locale list into those that are configured (`kept`) and those that are not (`dropped`),
|
|
28
|
+
* preserving order within each. */ export function dropUnknown(locales, known) {
|
|
29
|
+
const kept = [];
|
|
30
|
+
const dropped = [];
|
|
31
|
+
for (const locale of locales){
|
|
32
|
+
(isKnown(locale, known) ? kept : dropped).push(locale);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
kept,
|
|
36
|
+
dropped
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Remove the source locale from a target list; report whether it was present (so the caller can note
|
|
40
|
+
* it was excluded). */ export function excludeSource(targets, source) {
|
|
41
|
+
const kept = targets.filter((target)=>target !== source);
|
|
42
|
+
return {
|
|
43
|
+
kept,
|
|
44
|
+
wasPresent: kept.length !== targets.length
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One-shot resolution for the manual enqueue path: coerce scalar→array, then dedup → exclude source →
|
|
49
|
+
* drop unknown, in that order. A pure composition of the primitives above.
|
|
50
|
+
*
|
|
51
|
+
* @param knownLocales - the configured locale codes. Never null: the caller must reject a
|
|
52
|
+
* localization-less config before reaching here (translating without localization has no valid target
|
|
53
|
+
* and would corrupt data), so "no localization" is not representable as an input.
|
|
54
|
+
*/ export function resolveTargets(args) {
|
|
55
|
+
const { target_lng, source_lng, knownLocales } = args;
|
|
56
|
+
const requested = Array.isArray(target_lng) ? target_lng : [
|
|
57
|
+
target_lng
|
|
58
|
+
];
|
|
59
|
+
const { kept: withoutSource, wasPresent: droppedSource } = excludeSource(dedupe(requested), source_lng);
|
|
60
|
+
const { kept: targets, dropped: droppedUnknown } = dropUnknown(withoutSource, knownLocales);
|
|
61
|
+
return {
|
|
62
|
+
targets,
|
|
63
|
+
droppedUnknown,
|
|
64
|
+
droppedSource
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=resolveLocales.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,12 @@ export type { TranslationTask, TranslationLifecycleCallbacks } from "./server/mo
|
|
|
5
5
|
export type { TranslationProvenanceRecord } from "./core";
|
|
6
6
|
export type { AccessGuard, AccessGuardRequest } from "./types/AccessGuard";
|
|
7
7
|
export type { TranslationProvider, TranslationInput, TranslationOutput } from "./core";
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
8
|
+
export { openAIComplete } from "./translation-providers";
|
|
9
|
+
export type { OpenAIClientShape, OpenAISamplingParams, OpenAIStructuredOutput, DryRunConfig, } from "./translation-providers";
|
|
10
|
+
export { createTranslationProvider } from "./translation-providers";
|
|
11
|
+
export type { CompletionFn, CompletionRequest, TranslationProviderConfig, JsonSchemaObject, SystemPromptBuilder, SystemPromptContext, } from "./translation-providers";
|
|
12
|
+
export { TranslationProviderError, NoContentError, UnparseableReplyError, KeySetMismatchError, TransportError, ProviderConfigurationError, } from "./translation-providers";
|
|
13
|
+
export type { TranslationFailureCode } from "./translation-providers";
|
|
10
14
|
export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
|
|
11
15
|
export type { TaskRunnerProvider, PayloadJobsRunnerOptions } from "./server/modules/task-runner";
|
|
12
16
|
export { documentLevel, collectionLevel, fieldLevel } from "./composition/levels";
|
|
@@ -17,6 +21,7 @@ export { withAutoTranslate } from "./auto-translate-config";
|
|
|
17
21
|
export type { AutoTranslateConfig, AutoTranslateStrategy } from "./auto-translate-config";
|
|
18
22
|
export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
|
|
19
23
|
export type { TranslateCollectionPluginConfig } from "./plugin";
|
|
20
|
-
export { OpenAITranslationProvider } from "./translation-providers";
|
|
24
|
+
export { createOpenAIProvider, OpenAITranslationProvider } from "./translation-providers";
|
|
25
|
+
export type { OpenAIProviderConfig } from "./translation-providers";
|
|
21
26
|
export { translateKitField } from "./field-config";
|
|
22
27
|
export type { TranslateKitFieldConfig } from "./field-config";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Main plugin
|
|
2
2
|
export { translatorPlugin } from "./plugin";
|
|
3
|
-
|
|
4
|
-
export {
|
|
3
|
+
export { openAIComplete } from "./translation-providers";
|
|
4
|
+
export { createTranslationProvider } from "./translation-providers";
|
|
5
|
+
export { TranslationProviderError, NoContentError, UnparseableReplyError, KeySetMismatchError, TransportError, ProviderConfigurationError } from "./translation-providers";
|
|
5
6
|
// Task runners
|
|
6
7
|
export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
|
|
7
8
|
// Translation levels
|
|
@@ -14,7 +15,7 @@ export { withFieldTranslation } from "./field-config";
|
|
|
14
15
|
export { withAutoTranslate } from "./auto-translate-config";
|
|
15
16
|
// Deprecated exports (for backwards compatibility)
|
|
16
17
|
export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
|
|
17
|
-
export { OpenAITranslationProvider } from "./translation-providers";
|
|
18
|
+
export { createOpenAIProvider, OpenAITranslationProvider } from "./translation-providers";
|
|
18
19
|
export { translateKitField } from "./field-config";
|
|
19
20
|
|
|
20
21
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ServerResponse } from "../../shared";
|
|
2
2
|
import { extractLocaleCodes } from "../../modules/auto-translate";
|
|
3
3
|
import { isCollectionAvailable, getAllCollectionIds } from "../_lib/collection-utils";
|
|
4
|
+
import { Locales } from "../../../core/domain/locales";
|
|
4
5
|
import { EnqueueInputSchema } from "./model";
|
|
5
|
-
import { resolveTargetLocales } from "./resolveTargetLocales";
|
|
6
6
|
/**
|
|
7
7
|
* Enqueues translation tasks for documents
|
|
8
8
|
*/ export class EnqueueTranslationHandler {
|
|
@@ -26,7 +26,7 @@ import { resolveTargetLocales } from "./resolveTargetLocales";
|
|
|
26
26
|
if (!knownLocales) return ServerResponse.badRequest("Localization is not enabled in this Payload config; there are no target locales to translate into");
|
|
27
27
|
// Normalize the scalar-or-array target into the concrete locales to fan out to: de-dup, exclude the
|
|
28
28
|
// source, and drop locales that are not configured.
|
|
29
|
-
const { targets, droppedUnknown } =
|
|
29
|
+
const { targets, droppedUnknown } = Locales.resolveTargets({
|
|
30
30
|
target_lng,
|
|
31
31
|
source_lng,
|
|
32
32
|
knownLocales
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { Locales } from "../../../core/domain/locales";
|
|
1
2
|
/** Apply defaults to a raw config: strategy → "overwrite", debounce → 0. Duplicate target locales are
|
|
2
3
|
* de-duplicated so a misconfigured `targets: ["de","de"]` never enqueues two racing jobs for the same
|
|
3
4
|
* (document, locale) in one batch (the runner's supersession only dedupes against already-stored jobs). */ export function normalizeAutoTranslateConfig(config) {
|
|
4
5
|
return {
|
|
5
|
-
targets:
|
|
6
|
-
...new Set(config.targets)
|
|
7
|
-
],
|
|
6
|
+
targets: Locales.dedupe(config.targets),
|
|
8
7
|
strategy: config.strategy ?? "overwrite",
|
|
9
8
|
debounceMs: config.debounceMs ?? 0,
|
|
10
9
|
sourceLocale: config.sourceLocale
|
|
@@ -20,15 +19,15 @@
|
|
|
20
19
|
* either error at the DB (Postgres locale enum) or write orphaned, invisible data (Mongo/SQLite). Pure:
|
|
21
20
|
* returns a new policy and reports the drops; the caller (config-time wiring) emits the warning.
|
|
22
21
|
*/ export function filterPolicyToKnownLocales(policy, knownLocales) {
|
|
23
|
-
const
|
|
24
|
-
const sourceUnknown = policy.sourceLocale !== undefined && !
|
|
22
|
+
const { kept, dropped } = Locales.dropUnknown(policy.targets, knownLocales);
|
|
23
|
+
const sourceUnknown = policy.sourceLocale !== undefined && !Locales.isKnown(policy.sourceLocale, knownLocales);
|
|
25
24
|
return {
|
|
26
25
|
policy: {
|
|
27
26
|
...policy,
|
|
28
|
-
targets:
|
|
27
|
+
targets: kept,
|
|
29
28
|
sourceLocale: sourceUnknown ? undefined : policy.sourceLocale
|
|
30
29
|
},
|
|
31
|
-
droppedTargets,
|
|
30
|
+
droppedTargets: dropped,
|
|
32
31
|
droppedSourceLocale: sourceUnknown ? policy.sourceLocale ?? null : null
|
|
33
32
|
};
|
|
34
33
|
}
|
|
@@ -63,7 +62,7 @@
|
|
|
63
62
|
const { policy, collectionSlug, documentId, sourceLocale, doc, hasDrafts, now } = args;
|
|
64
63
|
const publishOnTranslation = resolvePublishOnTranslation(doc, hasDrafts);
|
|
65
64
|
const waitUntil = policy.debounceMs > 0 ? new Date(now + policy.debounceMs) : undefined;
|
|
66
|
-
return policy.targets
|
|
65
|
+
return Locales.excludeSource(policy.targets, sourceLocale).kept.map((target)=>({
|
|
67
66
|
collectionSlug,
|
|
68
67
|
collectionId: documentId,
|
|
69
68
|
sourceLng: sourceLocale,
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// opt-in — each vendor pulls its own deps (e.g. `openai`). Kept OUT of the framework-agnostic core
|
|
3
|
-
// so the core stays dependency-free; the PORT they implement lives in the core and is re-exported
|
|
4
|
-
// here for convenience. Naming is explicit ("translation-providers") to leave room for other kinds
|
|
5
|
-
// of providers later without a collision.
|
|
6
|
-
// The port (contract) — source of truth is the core.
|
|
7
|
-
// Vendor implementations.
|
|
1
|
+
export * from "./shared";
|
|
8
2
|
export * from "./openai";
|
|
9
3
|
|
|
10
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type OpenAIChatMessage = {
|
|
2
|
+
role: "system" | "user";
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* `schema` is `Record<string, unknown>`, not `unknown`: a widened field is not assignable to the
|
|
7
|
+
* SDK's closed union and OpenAI.shapes.test.ts stops compiling.
|
|
8
|
+
*/
|
|
9
|
+
export type OpenAIResponseFormat = {
|
|
10
|
+
type: "json_object";
|
|
11
|
+
} | {
|
|
12
|
+
type: "json_schema";
|
|
13
|
+
json_schema: {
|
|
14
|
+
name: string;
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
schema: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type OpenAIChatParams = {
|
|
20
|
+
model: string;
|
|
21
|
+
messages: OpenAIChatMessage[];
|
|
22
|
+
response_format?: OpenAIResponseFormat;
|
|
23
|
+
temperature?: number;
|
|
24
|
+
top_p?: number;
|
|
25
|
+
frequency_penalty?: number;
|
|
26
|
+
presence_penalty?: number;
|
|
27
|
+
};
|
|
28
|
+
export type OpenAIChatResult = {
|
|
29
|
+
choices: Array<{
|
|
30
|
+
message?: {
|
|
31
|
+
content?: string | null;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
export type OpenAIRequestOptions = {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Any client with a `chat.completions.create` of this shape.
|
|
40
|
+
*
|
|
41
|
+
* Method syntax, not a property with a function type: property syntax is checked contravariantly
|
|
42
|
+
* under strictFunctionTypes and the real SDK's overloaded `create` stops being assignable.
|
|
43
|
+
*
|
|
44
|
+
* @since 0.11.0
|
|
45
|
+
*/
|
|
46
|
+
export type OpenAIClientShape = {
|
|
47
|
+
chat: {
|
|
48
|
+
completions: {
|
|
49
|
+
create(params: OpenAIChatParams, options?: OpenAIRequestOptions): Promise<OpenAIChatResult>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Never import from `openai` here, in any position: an SDK type would enter this package's emitted
|
|
2
|
+
// declarations and force every consumer to install the SDK to type-check. Chat Completions rather
|
|
3
|
+
// than the Responses API — that is what Azure, OpenRouter and LiteLLM gateways implement.
|
|
4
|
+
/**
|
|
5
|
+
* Any client with a `chat.completions.create` of this shape.
|
|
6
|
+
*
|
|
7
|
+
* Method syntax, not a property with a function type: property syntax is checked contravariantly
|
|
8
|
+
* under strictFunctionTypes and the real SDK's overloaded `create` stops being assignable.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.11.0
|
|
11
|
+
*/ export { };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=OpenAI.shapes.js.map
|