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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +44 -29
  2. package/dist/index.d.ts +16 -14
  3. package/dist/index.js +9 -7
  4. package/dist/plugin.d.ts +15 -4
  5. package/dist/plugin.js +34 -57
  6. package/dist/server/features/cancel/handler.d.ts +2 -2
  7. package/dist/server/features/cancel/handler.js +3 -1
  8. package/dist/server/features/cancel/route.d.ts +4 -4
  9. package/dist/server/features/cancel/route.js +4 -4
  10. package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
  11. package/dist/server/features/cancel-by-collection/handler.js +1 -1
  12. package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
  13. package/dist/server/features/cancel-by-collection/route.js +4 -4
  14. package/dist/server/features/createTranslationRoutes.d.ts +23 -0
  15. package/dist/server/features/createTranslationRoutes.js +27 -0
  16. package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
  17. package/dist/server/features/enqueue-translation/handler.js +1 -1
  18. package/dist/server/features/enqueue-translation/route.d.ts +5 -5
  19. package/dist/server/features/enqueue-translation/route.js +4 -4
  20. package/dist/server/features/get-collection-status/handler.d.ts +3 -3
  21. package/dist/server/features/get-collection-status/handler.js +1 -1
  22. package/dist/server/features/get-collection-status/route.d.ts +5 -5
  23. package/dist/server/features/get-collection-status/route.js +4 -4
  24. package/dist/server/features/get-document-status/handler.d.ts +3 -3
  25. package/dist/server/features/get-document-status/handler.js +1 -1
  26. package/dist/server/features/get-document-status/route.d.ts +5 -5
  27. package/dist/server/features/get-document-status/route.js +4 -4
  28. package/dist/server/features/index.d.ts +7 -6
  29. package/dist/server/features/index.js +7 -6
  30. package/dist/server/features/run-translation/handler.d.ts +2 -2
  31. package/dist/server/features/run-translation/route.d.ts +4 -4
  32. package/dist/server/features/run-translation/route.js +4 -4
  33. package/dist/server/features/translate-document/handler.js +7 -11
  34. package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +18 -1
  35. package/dist/server/modules/task-runner/index.d.ts +6 -6
  36. package/dist/server/modules/task-runner/index.js +2 -2
  37. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
  38. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +16 -1
  39. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
  40. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
  41. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
  42. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
  43. package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
  44. package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
  45. package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
  46. package/dist/server/modules/translation-levels/documentLevel.js +28 -0
  47. package/dist/server/modules/translation-levels/index.d.ts +3 -0
  48. package/dist/server/modules/translation-levels/index.js +4 -0
  49. package/dist/server/modules/translation-levels/types.d.ts +42 -0
  50. package/dist/server/modules/translation-levels/types.js +10 -0
  51. package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
  52. package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
  53. package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
  54. package/dist/server/modules/translation-pipeline/index.js +3 -2
  55. package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
  56. package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
  57. package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
  58. package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
  59. package/dist/types/AccessGuard.d.ts +24 -1
  60. package/dist/types/AccessGuard.js +12 -1
  61. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ import { TranslateDocumentExport } from "../../../client/widgets/translate-document";
2
+ import { useDocTranslationApi } from "./useDocTranslationApi";
3
+ /**
4
+ * Per-document translation: a popup control on the document edit view, backed by
5
+ * the shared document-translation API (async/job by default; synchronous if the
6
+ * configured `runner` is a sync runner).
7
+ *
8
+ * @since 0.5.0
9
+ * @returns An opaque {@link TranslationLevel} to list in `translatorPlugin({ levels })`.
10
+ * @example
11
+ * ```ts
12
+ * translatorPlugin({
13
+ * collections: [Posts],
14
+ * translationProvider,
15
+ * runner: createPayloadJobsRunner(),
16
+ * levels: [documentLevel(), collectionLevel()], // this is the default — omit for the same result
17
+ * })
18
+ * ```
19
+ */ export function documentLevel() {
20
+ return {
21
+ extend (ctx) {
22
+ useDocTranslationApi(ctx);
23
+ ctx.addCollectionComponent("beforeDocumentControls", (collection)=>new TranslateDocumentExport(collection, ctx.access));
24
+ }
25
+ };
26
+ }
27
+
28
+ //# sourceMappingURL=documentLevel.js.map
@@ -0,0 +1,3 @@
1
+ export type { TranslationLevel } from "./types";
2
+ export { documentLevel } from "./documentLevel";
3
+ export { collectionLevel } from "./collectionLevel";
@@ -0,0 +1,4 @@
1
+ export { documentLevel } from "./documentLevel";
2
+ export { collectionLevel } from "./collectionLevel";
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,42 @@
1
+ import type { CollectionConfig, Endpoint } from "payload";
2
+ import type { AccessGuard } from "../../../types/AccessGuard";
3
+ import type { RawPayloadComponentExport } from "../../../client/shared/types/PayloadComponentExport";
4
+ import type { TaskRunnerFactory } from "../task-runner";
5
+ export type CollectionAdminSlot = "beforeDocumentControls" | "beforeListTable";
6
+ /**
7
+ * A composable translation surface (document / collection / field).
8
+ *
9
+ * Treat it as an **opaque value**: create one with `documentLevel()` /
10
+ * `collectionLevel()` (and, later, `fieldLevel()`) and list it in
11
+ * `translatorPlugin({ levels })`. It is produced only by those built-in
12
+ * factories — not externally implementable yet, since {@link LevelContext} is
13
+ * intentionally not exported. Opening it later (export `LevelContext`, drop the
14
+ * `@internal` tag) is additive and non-breaking.
15
+ *
16
+ * @since 0.5.0
17
+ */
18
+ export interface TranslationLevel {
19
+ /**
20
+ * Not a stable contract yet — produce via the level factories.
21
+ * @internal
22
+ */
23
+ extend(ctx: LevelContext): void;
24
+ }
25
+ /**
26
+ * Public-ready shape, kept internal until external levels are needed.
27
+ *
28
+ * A level contributes via these generic primitives; it never mutates the raw
29
+ * Payload config. The plugin deduplicates endpoints by method + path on apply.
30
+ * (fieldLevel — Phase 2 — will also read `schemaMap` + `translationProvider`.)
31
+ * @internal
32
+ */
33
+ export interface LevelContext {
34
+ readonly collections: CollectionConfig[];
35
+ readonly basePath: string;
36
+ readonly access?: AccessGuard;
37
+ readonly taskRunnerFactory: TaskRunnerFactory;
38
+ /** Register endpoints. Deduplicated by method + path when applied. */
39
+ addEndpoints(endpoints: Endpoint[]): void;
40
+ /** Attach an admin component to a slot on every managed collection. */
41
+ addCollectionComponent(slot: CollectionAdminSlot, make: (collection: CollectionConfig) => RawPayloadComponentExport): void;
42
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public-ready shape, kept internal until external levels are needed.
3
+ *
4
+ * A level contributes via these generic primitives; it never mutates the raw
5
+ * Payload config. The plugin deduplicates endpoints by method + path on apply.
6
+ * (fieldLevel — Phase 2 — will also read `schemaMap` + `translationProvider`.)
7
+ * @internal
8
+ */ export { };
9
+
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,8 @@
1
+ import type { LevelContext } from "./types";
2
+ /**
3
+ * Contribute the runner-agnostic document-translation API — the shared 6-route
4
+ * bundle, bound to the level context's runner. Both `documentLevel` and
5
+ * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
+ * path, so the bundle registers exactly once.
7
+ */
8
+ export declare function useDocTranslationApi(ctx: LevelContext): void;
@@ -0,0 +1,18 @@
1
+ import { createTranslationRoutes } from "../../features/createTranslationRoutes";
2
+ /**
3
+ * Contribute the runner-agnostic document-translation API — the shared 6-route
4
+ * bundle, bound to the level context's runner. Both `documentLevel` and
5
+ * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
+ * path, so the bundle registers exactly once.
7
+ */ export function useDocTranslationApi(ctx) {
8
+ ctx.addEndpoints(createTranslationRoutes({
9
+ taskRunnerFactory: ctx.taskRunnerFactory,
10
+ collectionConfig: {
11
+ availableCollections: new Set(ctx.collections.map((collection)=>collection.slug))
12
+ },
13
+ access: ctx.access,
14
+ basePath: ctx.basePath
15
+ }));
16
+ }
17
+
18
+ //# sourceMappingURL=useDocTranslationApi.js.map
@@ -1,3 +1,5 @@
1
- export { TranslationPipeline } from './TranslationPipeline';
2
- export type { TranslationStrategy } from './strategies';
3
- export { OverwriteStrategy, SkipExistingStrategy } from './strategies';
1
+ export { TranslationPipeline } from "./TranslationPipeline";
2
+ export { translateContent } from "./translateContent";
3
+ export type { TranslateContentArgs } from "./translateContent";
4
+ export type { TranslationStrategy } from "./strategies";
5
+ export { OverwriteStrategy, SkipExistingStrategy } from "./strategies";
@@ -1,4 +1,5 @@
1
- export { TranslationPipeline } from './TranslationPipeline';
2
- export { OverwriteStrategy, SkipExistingStrategy } from './strategies';
1
+ export { TranslationPipeline } from "./TranslationPipeline";
2
+ export { translateContent } from "./translateContent";
3
+ export { OverwriteStrategy, SkipExistingStrategy } from "./strategies";
3
4
 
4
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ import type { Field } from "payload";
2
+ import type { TranslationProvider } from "../translation-providers";
3
+ import type { TranslationStrategyName } from "./strategies";
4
+ export type TranslateContentArgs = {
5
+ /** Schema subtree to translate (e.g. `[declaredFieldConfig]`). */
6
+ schema: Field[];
7
+ /** Source values, rooted to match `schema` (e.g. `{ [fieldName]: value }`). */
8
+ sourceData: Record<string, unknown>;
9
+ /**
10
+ * Existing target-locale values to reconcile against (target wins when
11
+ * non-empty under `skip_existing`). Defaults to `{}` — i.e. translate the
12
+ * source in place (`overwrite`).
13
+ */
14
+ targetData?: Record<string, unknown>;
15
+ /** Source language code, or `''` for provider auto-detect. */
16
+ sourceLng: string;
17
+ targetLng: string;
18
+ translationProvider: TranslationProvider;
19
+ /** @default 'overwrite' */
20
+ strategy?: TranslationStrategyName;
21
+ };
22
+ /**
23
+ * Translate a content object over a schema subtree — no DB, no document.
24
+ *
25
+ * A thin reusable entry over {@link TranslationPipeline}, which is already pure
26
+ * and walks any `Field[]` + matching data (a subtree + partial data works
27
+ * unchanged). The document level routes through this wrapper today (instead of
28
+ * constructing the pipeline inline); the upcoming field level will too —
29
+ * passing a single declared field's subtree + its current unsaved form value.
30
+ *
31
+ * Only `localized` text/richText leaves are translated; non-localized values
32
+ * are reconciled through unchanged. Returns the translated data (same shape as
33
+ * `sourceData`) or `null` when nothing was translatable.
34
+ */
35
+ export declare function translateContent({ schema, sourceData, targetData, sourceLng, targetLng, translationProvider, strategy, }: TranslateContentArgs): Promise<Record<string, unknown> | null>;
@@ -0,0 +1,30 @@
1
+ import { TranslationPipeline } from "./TranslationPipeline";
2
+ import { createTranslationStrategy } from "./strategies";
3
+ /**
4
+ * Translate a content object over a schema subtree — no DB, no document.
5
+ *
6
+ * A thin reusable entry over {@link TranslationPipeline}, which is already pure
7
+ * and walks any `Field[]` + matching data (a subtree + partial data works
8
+ * unchanged). The document level routes through this wrapper today (instead of
9
+ * constructing the pipeline inline); the upcoming field level will too —
10
+ * passing a single declared field's subtree + its current unsaved form value.
11
+ *
12
+ * Only `localized` text/richText leaves are translated; non-localized values
13
+ * are reconciled through unchanged. Returns the translated data (same shape as
14
+ * `sourceData`) or `null` when nothing was translatable.
15
+ */ export async function translateContent({ schema, sourceData, targetData = {}, sourceLng, targetLng, translationProvider, strategy = "overwrite" }) {
16
+ const pipeline = new TranslationPipeline({
17
+ translationProvider,
18
+ translationStrategy: createTranslationStrategy(strategy)
19
+ });
20
+ const result = await pipeline.execute({
21
+ schema,
22
+ sourceData,
23
+ targetData,
24
+ sourceLng,
25
+ targetLng
26
+ });
27
+ return result ? result.translatedData : null;
28
+ }
29
+
30
+ //# sourceMappingURL=translateContent.js.map
@@ -1,5 +1,5 @@
1
- import type { TranslationProvider, TranslationInput, TranslationOutput } from './TranslationProvider.interface';
2
- import type { ChatModel } from 'openai/resources/index.mjs';
1
+ import type { TranslationProvider, TranslationInput, TranslationOutput } from "./TranslationProvider.interface";
2
+ import type { ChatModel } from "openai/resources/index.mjs";
3
3
  /**
4
4
  * Function to transform text in dry run mode.
5
5
  * Receives the original text and returns the transformed text.
@@ -30,6 +30,7 @@ export type SystemPromptContext = {
30
30
  */
31
31
  type SystemPromptBuilder = (context: SystemPromptContext) => string;
32
32
  export type OpenAIProviderConfig = {
33
+ /** OpenAI API key (required). Read it from an env var — never hard-code it. */
33
34
  apiKey: string;
34
35
  /**
35
36
  * OpenAI model to use for translation.
@@ -78,8 +79,8 @@ export type OpenAIProviderConfig = {
78
79
  };
79
80
  /** @deprecated Use `createOpenAIProvider` function instead */
80
81
  export declare class OpenAITranslationProvider implements TranslationProvider {
81
- private readonly config;
82
82
  private openAiClient;
83
+ private readonly config;
83
84
  constructor(config: OpenAIProviderConfig);
84
85
  translate(content: TranslationInput, souceLng: string, targetLng: string): Promise<TranslationOutput | null>;
85
86
  /**
@@ -1,21 +1,21 @@
1
- import OpenAI from 'openai';
2
- import { isObject } from '../../shared';
1
+ import OpenAI from "openai";
2
+ import { isObject } from "../../shared";
3
3
  /** @deprecated Use `createOpenAIProvider` function instead */ export class OpenAITranslationProvider {
4
- config;
5
4
  openAiClient;
5
+ config;
6
6
  constructor(config){
7
- this.config = config;
8
7
  this.openAiClient = new OpenAI({
9
8
  apiKey: config.apiKey
10
9
  });
10
+ this.config = config;
11
11
  }
12
12
  async translate(content, souceLng, targetLng) {
13
13
  if (this.config.dryRun) {
14
- console.info('[DRY RUN] Translation simulation:', {
14
+ console.info("[DRY RUN] Translation simulation:", {
15
15
  content,
16
16
  sourceLang: souceLng,
17
17
  targetLang: targetLng,
18
- provider: 'OpenAI'
18
+ provider: "OpenAI"
19
19
  });
20
20
  const timeout = this.getDryRunTimeout();
21
21
  if (timeout > 0) await new Promise((resolve)=>setTimeout(resolve, timeout));
@@ -26,21 +26,21 @@ import { isObject } from '../../shared';
26
26
  const chatCompletion = await this.openAiClient.chat.completions.create({
27
27
  messages: [
28
28
  {
29
- role: 'system',
29
+ role: "system",
30
30
  content: systemPrompt
31
31
  },
32
32
  {
33
- role: 'user',
33
+ role: "user",
34
34
  content: JSON.stringify(content)
35
35
  }
36
36
  ],
37
- model: this.config.model ?? 'gpt-4o',
37
+ model: this.config.model ?? "gpt-4o",
38
38
  temperature: 0,
39
39
  top_p: 1,
40
40
  frequency_penalty: 0,
41
41
  presence_penalty: 0,
42
42
  response_format: {
43
- type: 'json_object'
43
+ type: "json_object"
44
44
  }
45
45
  });
46
46
  const translatedContent = chatCompletion.choices[0].message.content;
@@ -54,7 +54,7 @@ import { isObject } from '../../shared';
54
54
  /**
55
55
  * Builds the system prompt for translation.
56
56
  */ buildSystemPrompt(sourceLang, targetLang) {
57
- const defaultPrompt = `Translate the values from the JSON that the user will send you${sourceLang ? ` from ${sourceLang}` : ''} into ${targetLang}. Keep all JSON keys exactly as they are, only translate the values.
57
+ const defaultPrompt = `Translate the values from the JSON that the user will send you${sourceLang ? ` from ${sourceLang}` : ""} into ${targetLang}. Keep all JSON keys exactly as they are, only translate the values.
58
58
  The response should be a valid JSON object with the same structure and keys as the input, but with translated values.
59
59
  Maintain any special formatting, placeholders, or variables within the values if they exist.`;
60
60
  if (this.config.systemPrompt) {
@@ -71,17 +71,17 @@ Maintain any special formatting, placeholders, or variables within the values if
71
71
  * If dryRun is an object with transform, returns it.
72
72
  * If dryRun is true, returns the default transformer that reverses text.
73
73
  */ getDryRunTransformer() {
74
- if (typeof this.config.dryRun === 'object' && this.config.dryRun.transform) {
74
+ if (typeof this.config.dryRun === "object" && this.config.dryRun.transform) {
75
75
  return this.config.dryRun.transform;
76
76
  }
77
- return (text)=>text.split('').reverse().join('');
77
+ return (text)=>text.split("").reverse().join("");
78
78
  }
79
79
  /**
80
80
  * Returns the timeout for dry run mode.
81
81
  * If dryRun is an object with timeout, returns it.
82
82
  * Otherwise returns 0 (no delay).
83
83
  */ getDryRunTimeout() {
84
- if (typeof this.config.dryRun === 'object' && this.config.dryRun.timeout) {
84
+ if (typeof this.config.dryRun === "object" && this.config.dryRun.timeout) {
85
85
  return this.config.dryRun.timeout;
86
86
  }
87
87
  return 0;
@@ -89,7 +89,7 @@ Maintain any special formatting, placeholders, or variables within the values if
89
89
  async createMockTranslation(content, transformer) {
90
90
  try {
91
91
  const mockTranslation = await this.transformObjectValues(content, async (value)=>{
92
- if (typeof value === 'string' && value.trim()) return transformer(value);
92
+ if (typeof value === "string" && value.trim()) return transformer(value);
93
93
  return value;
94
94
  });
95
95
  return mockTranslation;
@@ -1,10 +1,33 @@
1
- import type { BasePayload, TypedUser } from 'payload';
1
+ import type { BasePayload, TypedUser } from "payload";
2
+ /**
3
+ * The request context an {@link AccessGuard} receives when deciding whether to
4
+ * allow a translation API call.
5
+ */
2
6
  export type AccessGuardRequest = {
7
+ /** Incoming request headers (e.g. for token/cookie checks). */
3
8
  headers: Headers;
9
+ /** The authenticated user, or `null`/`undefined` if the request is anonymous. */
4
10
  user?: TypedUser | null;
11
+ /** The Payload instance, for any DB/permission lookups the guard needs. */
5
12
  payload: BasePayload;
6
13
  };
14
+ /**
15
+ * Gate for the translation API endpoints. Provide one via `translatorPlugin({ access })`
16
+ * to control who may trigger translations; omit it to leave the endpoints open.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const adminsOnly: AccessGuard = {
21
+ * check: ({ req }) => req.user?.role === 'admin',
22
+ * }
23
+ * translatorPlugin({ collections, translationProvider, runner, access: adminsOnly })
24
+ * ```
25
+ */
7
26
  export interface AccessGuard {
27
+ /**
28
+ * Return `true` to allow the request, `false` to reject it with `403 Forbidden`.
29
+ * May be async (e.g. to query permissions).
30
+ */
8
31
  check<R extends AccessGuardRequest>({ req }: {
9
32
  req: R;
10
33
  }): Promise<boolean> | boolean;
@@ -1,3 +1,14 @@
1
- export { };
1
+ /**
2
+ * Gate for the translation API endpoints. Provide one via `translatorPlugin({ access })`
3
+ * to control who may trigger translations; omit it to leave the endpoints open.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const adminsOnly: AccessGuard = {
8
+ * check: ({ req }) => req.user?.role === 'admin',
9
+ * }
10
+ * translatorPlugin({ collections, translationProvider, runner, access: adminsOnly })
11
+ * ```
12
+ */ export { };
2
13
 
3
14
  //# sourceMappingURL=AccessGuard.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",