1- import type { CollectionConfig , Config , Payload } from "payload" ;
1+ import type { CollectionConfig , Config } from "payload" ;
22
33import { CacheProviderExport } from "./client/app/cache/CacheProvider.export" ;
4- import {
5- DEFAULT_PROVENANCE_SLUG ,
6- PayloadProvenanceStore ,
7- assertProvenanceSlugFree ,
8- injectProvenanceCleanup ,
9- isProvenanceCollection ,
10- makeProvenanceCollection ,
11- } from "./server/modules/provenance" ;
4+ import { configureProvenance } from "./server/modules/provenance" ;
125import type { AccessGuard } from "./types/AccessGuard" ;
136import type { TranslationProvider } from "./core/translation-providers" ;
14- import type {
15- TaskRunnerProvider ,
16- TaskRunnerContext ,
17- TaskRunnerFactory ,
18- } from "./server/modules/task-runner" ;
19- import { TranslateDocumentHandler } from "./server/features/translate-document" ;
20- import {
21- LifecycleNotifier ,
22- taskFromHandlerInput ,
23- withQueuedNotification ,
24- } from "./server/modules/lifecycle" ;
7+ import type { TaskRunnerProvider } from "./server/modules/task-runner" ;
8+ import { wireTranslateRunner } from "./server/features/translate-document" ;
259import type { TranslationLifecycleCallbacks } from "./server/modules/lifecycle" ;
2610import { documentLevel , collectionLevel } from "./composition/levels" ;
2711import type { TranslationLevel } from "./server/modules/translation-levels" ;
2812import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder" ;
2913import { normalizePath } from "./server/shared" ;
3014
31- /**
32- * Resolve the opt-in `provenance` config to a sidecar slug, or `null` when disabled.
33- * `false`/omitted → off; `true` or `{}` → on with the default slug; `{ slug }` → on with that slug.
34- */
35- function resolveProvenanceSlug ( provenance : TranslatorPluginConfig [ "provenance" ] ) : string | null {
36- if ( ! provenance ) return null ;
37- if ( provenance === true ) return DEFAULT_PROVENANCE_SLUG ;
38- // `||` (not `??`) so an empty/blank slug falls back to the default instead of silently disabling.
39- return provenance . slug || DEFAULT_PROVENANCE_SLUG ;
40- }
41-
4215export type TranslatorPluginConfig = {
4316 /**
4417 * Original collection configs (same objects passed to buildConfig).
@@ -132,75 +105,30 @@ export class TranslateCollectionPlugin {
132105 basePath : rawBasePath = "/translate" ,
133106 } = this . pluginConfig ;
134107
135- const lifecycleCallbacks : TranslationLifecycleCallbacks = lifecycle ?? { } ;
136-
137- // Build schema map from deep-cloned collections
138- // Deep clone is required because Payload mutates the original collection objects,
139- // removing `localized: true` from nested fields during sanitization.
140- // We use JSON round-trip instead of structuredClone because Lexical editor
141- // configs contain async functions that structuredClone cannot handle.
142- // TODO: Consider introducing a FieldLike interface with only the properties
143- // used by the pipeline (name, type, localized, fields, blocks, tabs, custom)
144- // to make the contract explicit and avoid reliance on JSON round-trip.
108+ // Build schema map from deep-cloned collections.
109+ // Deep clone is required because Payload mutates the original collection objects, removing
110+ // `localized: true` from nested fields during sanitization. JSON round-trip (not
111+ // structuredClone) because Lexical editor configs contain async functions structuredClone
112+ // cannot handle.
113+ // TODO: Consider introducing a FieldLike interface with only the properties used by the
114+ // pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
115+ // and avoid reliance on JSON round-trip.
145116 const schemaMap = new Map (
146117 collections . map ( ( col ) => [ col . slug , JSON . parse ( JSON . stringify ( col . fields ) ) ] )
147118 ) ;
148119 const collectionSlugs = new Set ( schemaMap . keys ( ) ) ;
149120 const basePath = normalizePath ( rawBasePath ) ;
150- const provenanceSlug = resolveProvenanceSlug ( provenance ) ;
151- if ( provenanceSlug ) {
152- const existing = ( config . collections ?? [ ] ) . filter (
153- ( collection ) => ! isProvenanceCollection ( collection )
154- ) ;
155- assertProvenanceSlugFree ( provenanceSlug , existing ) ;
156- }
157- const provenanceStoreFactory = provenanceSlug
158- ? ( p : Payload ) => new PayloadProvenanceStore ( p , provenanceSlug )
159- : undefined ;
160121
161- const translateHandler = new TranslateDocumentHandler (
122+ // Each concern owns its own config-time wiring and exposes it uniformly; init() just composes.
123+ const provenanceModule = configureProvenance ( provenance , schemaMap ) ;
124+ const { taskRunnerFactory, configModifier : runnerConfigModifier } = wireTranslateRunner ( {
162125 translationProvider,
163126 schemaMap,
164- provenanceStoreFactory
165- ) ;
166-
167- const runnerContext : TaskRunnerContext = {
168- handler : async ( payload , input ) => {
169- const notifier = new LifecycleNotifier ( lifecycleCallbacks , payload . logger ) ;
170- const task = taskFromHandlerInput ( input ) ;
171- try {
172- await translateHandler . handle ( payload , {
173- collection : input . collection ,
174- collectionId : input . collectionId ,
175- sourceLng : input . sourceLng ,
176- targetLng : input . targetLng ,
177- strategy : input . strategy ,
178- publishOnTranslation : input . publishOnTranslation ,
179- } ) ;
180- } catch ( error ) {
181- await notifier . failed ( task , error ) ;
182- throw error ; // rethrow so the runner marks the job failed
183- }
184- await notifier . completed ( task ) ;
185- } ,
127+ provenanceServiceFactory : provenanceModule . serviceFactory ,
128+ runner,
129+ lifecycle : lifecycle ?? { } ,
186130 collections : Array . from ( collectionSlugs ) ,
187- } ;
188- const runnerConfigModifier = runner . configure ( runnerContext ) ;
189-
190- // Bind the context once so routes receive a self-sufficient factory: the
191- // runner needs no mutable per-instance handler state and create() has no
192- // "configure() must run first" ordering coupling (translator plan, 0c).
193- // When an `onQueued` callback is set, decorate the runner so `enqueue` fires it.
194- const taskRunnerFactory : TaskRunnerFactory = {
195- create : ( payload ) => {
196- const taskRunner = runner . create ( payload , runnerContext . handler ) ;
197- if ( ! lifecycleCallbacks . onQueued ) return taskRunner ;
198- return withQueuedNotification (
199- taskRunner ,
200- new LifecycleNotifier ( lifecycleCallbacks , payload . logger )
201- ) ;
202- } ,
203- } ;
131+ } ) ;
204132
205133 const activeLevels = levels ?? [ documentLevel ( ) , collectionLevel ( ) ] ;
206134 const builder = new PluginConfigBuilder ( {
@@ -210,26 +138,12 @@ export class TranslateCollectionPlugin {
210138 taskRunnerFactory,
211139 schemaMap,
212140 translationProvider,
213- provenanceStoreFactory ,
141+ provenanceServiceFactory : provenanceModule . serviceFactory ,
214142 } ) ;
215143 for ( const level of activeLevels ) level . extend ( builder ) ;
216144
217145 builder . addConfigModifier ( runnerConfigModifier ) ;
218- if ( provenanceSlug && provenanceStoreFactory ) {
219- builder . addConfigModifier ( ( cfg ) => {
220- const alreadyAdded = cfg . collections ?. some (
221- ( collection ) => collection . slug === provenanceSlug && isProvenanceCollection ( collection )
222- ) ;
223- if ( ! alreadyAdded ) {
224- cfg . collections = [
225- ...( cfg . collections ?? [ ] ) ,
226- makeProvenanceCollection ( provenanceSlug ) ,
227- ] ;
228- }
229- injectProvenanceCleanup ( cfg , collectionSlugs , provenanceStoreFactory , provenanceSlug ) ;
230- return cfg ;
231- } ) ;
232- }
146+ builder . addConfigModifier ( provenanceModule . configure ( collectionSlugs ) ) ;
233147 builder . addAdminProvider ( new CacheProviderExport ( basePath ) ) ;
234148
235149 // The single place the Payload config is mutated.
0 commit comments