@focus-reactive/payload-plugin-translator 0.8.1 → 0.9.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 +37 -0
- package/dist/auto-translate-config.d.ts +40 -0
- package/dist/auto-translate-config.js +46 -0
- package/dist/client/entities/translation/api/queries/useDocumentTranslation.d.ts +15 -15
- package/dist/client/entities/translation/api/queries/useDocumentTranslation.js +27 -11
- package/dist/client/entities/translation/index.d.ts +2 -1
- package/dist/client/entities/translation/index.js +2 -1
- package/dist/client/entities/translation/model/autoTranslateSummary.d.ts +15 -0
- package/dist/client/entities/translation/model/autoTranslateSummary.js +22 -0
- package/dist/client/entities/translation/model/panelStatus.d.ts +8 -2
- package/dist/client/entities/translation/model/panelStatus.js +16 -0
- package/dist/client/entities/translation/model/statusRows.d.ts +9 -6
- package/dist/client/entities/translation/model/statusRows.js +18 -16
- package/dist/client/entities/translation/model/types.d.ts +6 -1
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.d.ts +14 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.js +34 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/index.d.ts +1 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/index.js +3 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/styles.module.scss +30 -0
- package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.d.ts +3 -0
- package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.js +14 -0
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.d.ts +3 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.js +14 -5
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js +4 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/styles.module.scss +9 -0
- package/dist/client/widgets/translate-document/ui/TranslateDocument.d.ts +3 -1
- package/dist/client/widgets/translate-document/ui/TranslateDocument.js +17 -8
- package/dist/client/widgets/translate-document/ui/TranslateDocument.server.js +4 -1
- package/dist/client/widgets/translate-document/ui/styles.module.scss +9 -0
- package/dist/core/auto-translate/hasSourceContentChanged.d.ts +21 -0
- package/dist/core/auto-translate/hasSourceContentChanged.js +25 -0
- package/dist/core/auto-translate/index.d.ts +1 -0
- package/dist/core/auto-translate/index.js +3 -0
- package/dist/core/auto-translate-config/getAutoTranslateConfig.d.ts +14 -0
- package/dist/core/auto-translate-config/getAutoTranslateConfig.js +29 -0
- package/dist/core/auto-translate-config/index.d.ts +3 -0
- package/dist/core/auto-translate-config/index.js +4 -0
- package/dist/core/auto-translate-config/types.d.ts +32 -0
- package/dist/core/auto-translate-config/types.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/plugin.js +5 -0
- package/dist/server/features/get-document-status/handler.js +4 -2
- package/dist/server/features/get-document-status/model.d.ts +10 -0
- package/dist/server/features/get-document-status/model.js +21 -0
- package/dist/server/features/translate-document/handler.js +9 -1
- package/dist/server/modules/auto-translate/AutoTranslate.policy.d.ts +79 -0
- package/dist/server/modules/auto-translate/AutoTranslate.policy.js +77 -0
- package/dist/server/modules/auto-translate/AutoTranslate.shapes.d.ts +22 -0
- package/dist/server/modules/auto-translate/AutoTranslate.shapes.js +3 -0
- package/dist/server/modules/auto-translate/AutoTranslate.wiring.d.ts +21 -0
- package/dist/server/modules/auto-translate/AutoTranslate.wiring.js +68 -0
- package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.d.ts +39 -0
- package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.js +94 -0
- package/dist/server/modules/auto-translate/index.d.ts +4 -0
- package/dist/server/modules/auto-translate/index.js +7 -0
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +15 -2
- package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.js +5 -3
- package/dist/server/modules/task-runner/types.d.ts +7 -0
- package/dist/types/AutoTranslateContext.d.ts +10 -0
- package/dist/types/AutoTranslateContext.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Apply defaults to a raw config: strategy → "overwrite", debounce → 0. Duplicate target locales are
|
|
2
|
+
* de-duplicated so a misconfigured `targets: ["de","de"]` never enqueues two racing jobs for the same
|
|
3
|
+
* (document, locale) in one batch (the runner's supersession only dedupes against already-stored jobs). */ export function normalizeAutoTranslateConfig(config) {
|
|
4
|
+
return {
|
|
5
|
+
targets: [
|
|
6
|
+
...new Set(config.targets)
|
|
7
|
+
],
|
|
8
|
+
strategy: config.strategy ?? "overwrite",
|
|
9
|
+
debounceMs: config.debounceMs ?? 0,
|
|
10
|
+
sourceLocale: config.sourceLocale
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Extract the configured locale codes, or `null` when localization is disabled/absent. */ export function extractLocaleCodes(localization) {
|
|
14
|
+
if (!localization) return null;
|
|
15
|
+
return new Set(localization.locales.map((locale)=>typeof locale === "string" ? locale : locale.code));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drop targets (and a `sourceLocale` override) that are not configured locales, so a mistyped locale in
|
|
19
|
+
* `withAutoTranslate` never reaches the pipeline — where it would silently burn a provider call and
|
|
20
|
+
* either error at the DB (Postgres locale enum) or write orphaned, invisible data (Mongo/SQLite). Pure:
|
|
21
|
+
* returns a new policy and reports the drops; the caller (config-time wiring) emits the warning.
|
|
22
|
+
*/ export function filterPolicyToKnownLocales(policy, knownLocales) {
|
|
23
|
+
const droppedTargets = policy.targets.filter((target)=>!knownLocales.has(target));
|
|
24
|
+
const sourceUnknown = policy.sourceLocale !== undefined && !knownLocales.has(policy.sourceLocale);
|
|
25
|
+
return {
|
|
26
|
+
policy: {
|
|
27
|
+
...policy,
|
|
28
|
+
targets: policy.targets.filter((target)=>knownLocales.has(target)),
|
|
29
|
+
sourceLocale: sourceUnknown ? undefined : policy.sourceLocale
|
|
30
|
+
},
|
|
31
|
+
droppedTargets,
|
|
32
|
+
droppedSourceLocale: sourceUnknown ? policy.sourceLocale ?? null : null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The v1 (collection-level) resolver. Ignores `doc` — see {@link AutoTranslatePolicyResolver}. This is
|
|
37
|
+
* the single seam a future document-level manager replaces.
|
|
38
|
+
*/ export function makeCollectionPolicyResolver(policies) {
|
|
39
|
+
return (collectionSlug, _doc)=>policies.get(collectionSlug) ?? null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Publish-gate (#51 D8): a drafts-enabled collection auto-translates only on a **published** save;
|
|
43
|
+
* autosave/draft saves are ignored. A collection without drafts has no `_status`, so every save
|
|
44
|
+
* qualifies. Applies uniformly to create and update.
|
|
45
|
+
*/ export function passesPublishGate(doc, hasDrafts) {
|
|
46
|
+
if (!hasDrafts) return true;
|
|
47
|
+
return doc._status === "published";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mirror the source document's status onto the translation (#51 D9). Combined with the publish-gate,
|
|
51
|
+
* the source is published whenever we reach enqueue, so translations publish; a no-drafts collection
|
|
52
|
+
* publishes too. Deliberately identical to {@link passesPublishGate} today — kept as a SEPARATE
|
|
53
|
+
* function (not merged) because it diverges once a future document-level manager (R8) can bypass the
|
|
54
|
+
* gate and translate a still-draft source; do not collapse the two.
|
|
55
|
+
*/ export function resolvePublishOnTranslation(doc, hasDrafts) {
|
|
56
|
+
if (!hasDrafts) return true;
|
|
57
|
+
return doc._status === "published";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build one {@link TaskInput} per configured target locale (the source locale is always excluded).
|
|
61
|
+
* `waitUntil` encodes the debounce; `now` is injected so the timestamp is deterministic in tests.
|
|
62
|
+
*/ export function buildAutoTranslateTasks(args) {
|
|
63
|
+
const { policy, collectionSlug, documentId, sourceLocale, doc, hasDrafts, now } = args;
|
|
64
|
+
const publishOnTranslation = resolvePublishOnTranslation(doc, hasDrafts);
|
|
65
|
+
const waitUntil = policy.debounceMs > 0 ? new Date(now + policy.debounceMs) : undefined;
|
|
66
|
+
return policy.targets.filter((target)=>target !== sourceLocale).map((target)=>({
|
|
67
|
+
collectionSlug,
|
|
68
|
+
collectionId: documentId,
|
|
69
|
+
sourceLng: sourceLocale,
|
|
70
|
+
targetLng: target,
|
|
71
|
+
strategy: policy.strategy,
|
|
72
|
+
publishOnTranslation,
|
|
73
|
+
waitUntil
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=AutoTranslate.policy.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* The minimal slice of a Payload collection the auto-translate wiring mutates: its `slug` and the
|
|
4
|
+
* `afterChange` hook slot. A real `CollectionConfig` is structurally assignable to this, so call sites
|
|
5
|
+
* pass the live collection with no adapter and tests pass a `{ slug: "posts" }` literal. Keeps
|
|
6
|
+
* `injectAutoTranslateHook` off the god-`CollectionConfig` type (own shape — provenance's is not reused,
|
|
7
|
+
* per the module-owns-its-shape convention). The only Payload type imported is the hook callback
|
|
8
|
+
* contract, which legitimately stays framework-typed.
|
|
9
|
+
*/
|
|
10
|
+
export type AutoTranslateManagedEntry = {
|
|
11
|
+
slug: string;
|
|
12
|
+
hooks?: {
|
|
13
|
+
afterChange?: CollectionAfterChangeHook[];
|
|
14
|
+
};
|
|
15
|
+
/** The `custom` bag — the opt-in is propagated here so the admin UI can read it back off the
|
|
16
|
+
* REGISTERED collection (which may be a different object than the one `withAutoTranslate` wrapped). */
|
|
17
|
+
custom?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
/** The minimal config host: a mutable `collections` array. A real Payload `Config` plugs straight in. */
|
|
20
|
+
export type AutoTranslateManagedConfig = {
|
|
21
|
+
collections?: AutoTranslateManagedEntry[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
2
|
+
import type { ConfigModifier } from "../../../types/ConfigModifier";
|
|
3
|
+
import type { TaskRunnerFactory } from "../task-runner";
|
|
4
|
+
/** A collection as the plugin receives it — only `slug` + `custom` are read to resolve the opt-in. */
|
|
5
|
+
type ConfigurableCollection = {
|
|
6
|
+
slug: string;
|
|
7
|
+
custom?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
/** Everything the auto-translate module contributes at config time (mirrors `ProvenanceModule`). */
|
|
10
|
+
export type AutoTranslateModule = {
|
|
11
|
+
configure(managedSlugs: Set<string>): ConfigModifier;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Turn the opt-in `withAutoTranslate` config (read from each collection's `custom`) into a
|
|
15
|
+
* self-contained {@link AutoTranslateModule} — mirrors `configureProvenance`. Builds the per-collection
|
|
16
|
+
* policy map + resolver once, then returns a `configure(managedSlugs) → ConfigModifier` that injects a
|
|
17
|
+
* single best-effort `afterChange` hook onto every enabled + managed collection. When no collection
|
|
18
|
+
* opted in, `configure` is a no-op (no hook, no behaviour change).
|
|
19
|
+
*/
|
|
20
|
+
export declare function configureAutoTranslate(collections: ConfigurableCollection[], schemaMap: CollectionSchemaMap, taskRunnerFactory: TaskRunnerFactory): AutoTranslateModule;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getAutoTranslateConfig } from "../../../core/auto-translate-config";
|
|
2
|
+
import { extractLocaleCodes, filterPolicyToKnownLocales, makeCollectionPolicyResolver, normalizeAutoTranslateConfig } from "./AutoTranslate.policy";
|
|
3
|
+
import { injectAutoTranslateHook, makeAutoTranslateHook, propagateAutoTranslateCustom } from "./AutoTranslateEnqueue.hook";
|
|
4
|
+
const NOOP = (config)=>config;
|
|
5
|
+
/** Emit one clear config-time warning per collection whose auto-translate config named unknown locales. */ function warnDroppedLocales(slug, filtered, knownLocales) {
|
|
6
|
+
const known = [
|
|
7
|
+
...knownLocales
|
|
8
|
+
].join(", ");
|
|
9
|
+
if (filtered.droppedTargets.length > 0) {
|
|
10
|
+
console.warn(`[payload-plugin-translator] auto-translate on "${slug}": ignoring unknown target locale(s) ${filtered.droppedTargets.join(", ")} (configured locales: ${known}).`);
|
|
11
|
+
}
|
|
12
|
+
if (filtered.droppedSourceLocale) {
|
|
13
|
+
console.warn(`[payload-plugin-translator] auto-translate on "${slug}": unknown sourceLocale "${filtered.droppedSourceLocale}" ignored, falling back to the default locale (configured locales: ${known}).`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Turn the opt-in `withAutoTranslate` config (read from each collection's `custom`) into a
|
|
18
|
+
* self-contained {@link AutoTranslateModule} — mirrors `configureProvenance`. Builds the per-collection
|
|
19
|
+
* policy map + resolver once, then returns a `configure(managedSlugs) → ConfigModifier` that injects a
|
|
20
|
+
* single best-effort `afterChange` hook onto every enabled + managed collection. When no collection
|
|
21
|
+
* opted in, `configure` is a no-op (no hook, no behaviour change).
|
|
22
|
+
*/ export function configureAutoTranslate(collections, schemaMap, taskRunnerFactory) {
|
|
23
|
+
const policies = new Map();
|
|
24
|
+
for (const collection of collections){
|
|
25
|
+
const config = getAutoTranslateConfig(collection);
|
|
26
|
+
if (config) policies.set(collection.slug, normalizeAutoTranslateConfig(config));
|
|
27
|
+
}
|
|
28
|
+
if (policies.size === 0) return {
|
|
29
|
+
configure: ()=>NOOP
|
|
30
|
+
};
|
|
31
|
+
const enabledSlugs = new Set(policies.keys());
|
|
32
|
+
const resolvePolicy = makeCollectionPolicyResolver(policies);
|
|
33
|
+
const hook = makeAutoTranslateHook({
|
|
34
|
+
resolvePolicy,
|
|
35
|
+
schemaMap,
|
|
36
|
+
taskRunnerFactory
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
configure: (managedSlugs)=>(config)=>{
|
|
40
|
+
// Inject only onto collections that both opted in AND are plugin-managed.
|
|
41
|
+
const slugs = new Set([
|
|
42
|
+
...enabledSlugs
|
|
43
|
+
].filter((slug)=>managedSlugs.has(slug)));
|
|
44
|
+
// Drop targets / source-locale overrides that are not configured locales, fail-fast with a
|
|
45
|
+
// warning at init — else a mistyped locale silently burns provider calls and orphans data at
|
|
46
|
+
// runtime. Filter the shared `policies` map once here (config-time, before any hook fires), so
|
|
47
|
+
// the hook, the propagated `custom`, and the admin indicator all read the corrected policy.
|
|
48
|
+
const knownLocales = extractLocaleCodes(config.localization);
|
|
49
|
+
if (knownLocales) {
|
|
50
|
+
for (const slug of slugs){
|
|
51
|
+
const policy = policies.get(slug);
|
|
52
|
+
if (!policy) continue;
|
|
53
|
+
const filtered = filterPolicyToKnownLocales(policy, knownLocales);
|
|
54
|
+
warnDroppedLocales(slug, filtered, knownLocales);
|
|
55
|
+
policies.set(slug, filtered.policy);
|
|
56
|
+
}
|
|
57
|
+
} else if (slugs.size > 0) {
|
|
58
|
+
console.warn("[payload-plugin-translator] auto-translate is configured but localization is disabled; no translations will be enqueued.");
|
|
59
|
+
}
|
|
60
|
+
injectAutoTranslateHook(config, slugs, hook);
|
|
61
|
+
// Mirror the opt-in onto the registered collection's `custom` so the admin UI can read it back.
|
|
62
|
+
propagateAutoTranslateCustom(config, slugs, policies);
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=AutoTranslate.wiring.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from "payload";
|
|
2
|
+
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
3
|
+
import type { TaskRunnerFactory } from "../task-runner";
|
|
4
|
+
import type { AutoTranslatePolicyResolver, NormalizedAutoTranslatePolicy } from "./AutoTranslate.policy";
|
|
5
|
+
import type { AutoTranslateManagedConfig } from "./AutoTranslate.shapes";
|
|
6
|
+
type AutoTranslateHookDeps = {
|
|
7
|
+
resolvePolicy: AutoTranslatePolicyResolver;
|
|
8
|
+
schemaMap: CollectionSchemaMap;
|
|
9
|
+
taskRunnerFactory: TaskRunnerFactory;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Build the `afterChange` hook that auto-enqueues translations when a document's source-locale content
|
|
13
|
+
* changes. Thin orchestration only — every decision lives in `AutoTranslate.policy.ts` or the core
|
|
14
|
+
* drift predicate. Best-effort by contract: any failure is logged and swallowed, never failing the
|
|
15
|
+
* editor's save.
|
|
16
|
+
*
|
|
17
|
+
* Order (cheap guards first): (1) skip the translator's own writes via the `req.context` flag;
|
|
18
|
+
* (2) resolve the policy — off ⇒ skip; (3) resolve the source locale (per-collection override else
|
|
19
|
+
* `localization.defaultLocale`) — unresolved ⇒ log + skip; (4) skip non-source-locale writes (the
|
|
20
|
+
* pipeline's target writes never match); (5) publish-gate (D8); (6) drift-gate (D3); then enqueue one
|
|
21
|
+
* job per target locale.
|
|
22
|
+
*/
|
|
23
|
+
export declare function makeAutoTranslateHook(deps: AutoTranslateHookDeps): CollectionAfterChangeHook;
|
|
24
|
+
/**
|
|
25
|
+
* Attach the auto-translate hook to every enabled collection on `config`, appending to any
|
|
26
|
+
* consumer-supplied `afterChange` array. Idempotent: a collection that already carries the marked hook
|
|
27
|
+
* is skipped, so a repeated `init()` never stacks duplicates.
|
|
28
|
+
*/
|
|
29
|
+
export declare function injectAutoTranslateHook(config: AutoTranslateManagedConfig, enabledSlugs: Set<string>, hook: CollectionAfterChangeHook): void;
|
|
30
|
+
/**
|
|
31
|
+
* Propagate each enabled collection's resolved policy onto the REGISTERED collection's `custom` bag, so
|
|
32
|
+
* the admin UI can read the opt-in back via `getAutoTranslateConfig`. This is required because
|
|
33
|
+
* `withAutoTranslate` stamps `custom` on the object passed to the plugin's `collections` param, which
|
|
34
|
+
* can be a DIFFERENT object than the one registered in `buildConfig.collections` (the reader would
|
|
35
|
+
* otherwise see no config and the indicator would disagree with the behaviour). Idempotent + additive:
|
|
36
|
+
* re-stamping the same value is a no-op and the behaviour wiring still reads from the plugin param.
|
|
37
|
+
*/
|
|
38
|
+
export declare function propagateAutoTranslateCustom(config: AutoTranslateManagedConfig, enabledSlugs: Set<string>, policies: Map<string, NormalizedAutoTranslatePolicy>): void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { hasSourceContentChanged } from "../../../core/auto-translate";
|
|
2
|
+
import { AUTO_TRANSLATE_CUSTOM_KEY } from "../../../core/auto-translate-config";
|
|
3
|
+
import { AUTO_TRANSLATE_SKIP_CONTEXT_KEY } from "../../../types/AutoTranslateContext";
|
|
4
|
+
import { buildAutoTranslateTasks, passesPublishGate } from "./AutoTranslate.policy";
|
|
5
|
+
/**
|
|
6
|
+
* Build the `afterChange` hook that auto-enqueues translations when a document's source-locale content
|
|
7
|
+
* changes. Thin orchestration only — every decision lives in `AutoTranslate.policy.ts` or the core
|
|
8
|
+
* drift predicate. Best-effort by contract: any failure is logged and swallowed, never failing the
|
|
9
|
+
* editor's save.
|
|
10
|
+
*
|
|
11
|
+
* Order (cheap guards first): (1) skip the translator's own writes via the `req.context` flag;
|
|
12
|
+
* (2) resolve the policy — off ⇒ skip; (3) resolve the source locale (per-collection override else
|
|
13
|
+
* `localization.defaultLocale`) — unresolved ⇒ log + skip; (4) skip non-source-locale writes (the
|
|
14
|
+
* pipeline's target writes never match); (5) publish-gate (D8); (6) drift-gate (D3); then enqueue one
|
|
15
|
+
* job per target locale.
|
|
16
|
+
*/ export function makeAutoTranslateHook(deps) {
|
|
17
|
+
const { resolvePolicy, schemaMap, taskRunnerFactory } = deps;
|
|
18
|
+
const hook = async ({ doc, previousDoc, req, collection })=>{
|
|
19
|
+
try {
|
|
20
|
+
if (req.context?.[AUTO_TRANSLATE_SKIP_CONTEXT_KEY]) return doc;
|
|
21
|
+
const policy = resolvePolicy(collection.slug, doc);
|
|
22
|
+
if (!policy) return doc;
|
|
23
|
+
const localization = req.payload.config.localization;
|
|
24
|
+
const sourceLocale = policy.sourceLocale ?? (localization ? localization.defaultLocale : undefined);
|
|
25
|
+
if (!sourceLocale) {
|
|
26
|
+
req.payload.logger.warn({
|
|
27
|
+
collection: collection.slug,
|
|
28
|
+
documentId: String(doc.id),
|
|
29
|
+
msg: "translator: auto-translate skipped — no source locale resolvable (set localization.defaultLocale or a per-collection sourceLocale)"
|
|
30
|
+
});
|
|
31
|
+
return doc;
|
|
32
|
+
}
|
|
33
|
+
if (req.locale !== sourceLocale) return doc;
|
|
34
|
+
const hasDrafts = Boolean(collection.versions && collection.versions.drafts);
|
|
35
|
+
if (!passesPublishGate(doc, hasDrafts)) return doc;
|
|
36
|
+
const schema = schemaMap.get(collection.slug);
|
|
37
|
+
if (schema && !hasSourceContentChanged(previousDoc, doc, schema)) return doc;
|
|
38
|
+
const tasks = buildAutoTranslateTasks({
|
|
39
|
+
policy,
|
|
40
|
+
collectionSlug: collection.slug,
|
|
41
|
+
documentId: String(doc.id),
|
|
42
|
+
sourceLocale,
|
|
43
|
+
doc,
|
|
44
|
+
hasDrafts,
|
|
45
|
+
now: Date.now()
|
|
46
|
+
});
|
|
47
|
+
if (tasks.length === 0) return doc;
|
|
48
|
+
await taskRunnerFactory.create(req.payload).enqueue(tasks);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
req.payload.logger.error({
|
|
51
|
+
err: error,
|
|
52
|
+
collection: collection.slug,
|
|
53
|
+
documentId: String(doc.id),
|
|
54
|
+
msg: "translator: auto-translate hook failed"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return doc;
|
|
58
|
+
};
|
|
59
|
+
hook.__translatorAutoTranslate = true;
|
|
60
|
+
return hook;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Attach the auto-translate hook to every enabled collection on `config`, appending to any
|
|
64
|
+
* consumer-supplied `afterChange` array. Idempotent: a collection that already carries the marked hook
|
|
65
|
+
* is skipped, so a repeated `init()` never stacks duplicates.
|
|
66
|
+
*/ export function injectAutoTranslateHook(config, enabledSlugs, hook) {
|
|
67
|
+
for (const collection of config.collections ?? []){
|
|
68
|
+
if (!enabledSlugs.has(collection.slug)) continue;
|
|
69
|
+
collection.hooks ??= {};
|
|
70
|
+
collection.hooks.afterChange ??= [];
|
|
71
|
+
const alreadyInjected = collection.hooks.afterChange.some((existing)=>existing.__translatorAutoTranslate === true);
|
|
72
|
+
if (!alreadyInjected) collection.hooks.afterChange.push(hook);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Propagate each enabled collection's resolved policy onto the REGISTERED collection's `custom` bag, so
|
|
77
|
+
* the admin UI can read the opt-in back via `getAutoTranslateConfig`. This is required because
|
|
78
|
+
* `withAutoTranslate` stamps `custom` on the object passed to the plugin's `collections` param, which
|
|
79
|
+
* can be a DIFFERENT object than the one registered in `buildConfig.collections` (the reader would
|
|
80
|
+
* otherwise see no config and the indicator would disagree with the behaviour). Idempotent + additive:
|
|
81
|
+
* re-stamping the same value is a no-op and the behaviour wiring still reads from the plugin param.
|
|
82
|
+
*/ export function propagateAutoTranslateCustom(config, enabledSlugs, policies) {
|
|
83
|
+
for (const collection of config.collections ?? []){
|
|
84
|
+
if (!enabledSlugs.has(collection.slug)) continue;
|
|
85
|
+
const policy = policies.get(collection.slug);
|
|
86
|
+
if (!policy) continue;
|
|
87
|
+
collection.custom = {
|
|
88
|
+
...collection.custom ?? {},
|
|
89
|
+
[AUTO_TRANSLATE_CUSTOM_KEY]: policy
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=AutoTranslateEnqueue.hook.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
2
|
+
export type { AutoTranslateModule } from "./AutoTranslate.wiring";
|
|
3
|
+
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
4
|
+
export type { AutoTranslatePolicyResolver, NormalizedAutoTranslatePolicy, } from "./AutoTranslate.policy";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Auto-translate adapter (Payload-backed). The payload-free drift predicate + config reader live in
|
|
2
|
+
// the core (src/core/auto-translate*, src/core/content-projection); this module is the config-time
|
|
3
|
+
// wiring + the afterChange hook that enqueues translations on a source-locale change (#51).
|
|
4
|
+
export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
5
|
+
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { normalizeJob } from "./normalizeJob";
|
|
2
|
+
// A translation job's supersession identity: same document AND same target locale. IDs are
|
|
3
|
+
// String()-normalized to match the stored (string) form, so a number id compares equal to its
|
|
4
|
+
// persisted job.
|
|
5
|
+
const documentLocaleKey = (collectionId, targetLng)=>`${String(collectionId)}:${targetLng}`;
|
|
2
6
|
/**
|
|
3
7
|
* TaskRunner implementation using Payload Jobs.
|
|
4
8
|
*
|
|
@@ -15,13 +19,22 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
15
19
|
for (const [collectionSlug, items] of byCollection){
|
|
16
20
|
const documentIds = items.map((t)=>t.collectionId);
|
|
17
21
|
const existing = await this.findByCollection(collectionSlug, documentIds);
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// Supersede only jobs for the SAME (document, target locale) being re-enqueued — never a
|
|
23
|
+
// concurrent job for a *different* locale of the same document. Cancelling per-document would
|
|
24
|
+
// kill an in-flight translation of another locale (the concurrent re-translate bug).
|
|
25
|
+
const supersededKeys = new Set(items.map((t)=>documentLocaleKey(t.collectionId, t.targetLng)));
|
|
26
|
+
const toCancel = existing.filter((t)=>supersededKeys.has(documentLocaleKey(t.input.collectionId, t.input.targetLng)));
|
|
27
|
+
if (toCancel.length > 0) {
|
|
28
|
+
await this.cancelInternal(toCancel.map((t)=>t.id));
|
|
20
29
|
}
|
|
21
30
|
}
|
|
22
31
|
await Promise.all(tasks.map((task)=>this.payload.jobs.queue({
|
|
23
32
|
task: this.config.taskName,
|
|
24
33
|
queue: this.config.queueName,
|
|
34
|
+
// Debounce: when set, Payload holds the job until this instant. A superseding enqueue for
|
|
35
|
+
// the same (document, targetLng) cancels the pending delayed job first (see enqueue above),
|
|
36
|
+
// so rapid source edits coalesce to the final one. Undefined for the manual path.
|
|
37
|
+
waitUntil: task.waitUntil,
|
|
25
38
|
input: {
|
|
26
39
|
// Flat text reference (ID-agnostic). Stored as a string — no
|
|
27
40
|
// relationship type validation against the collection's ID type,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
async enqueue(inputs) {
|
|
16
16
|
for (const input of inputs){
|
|
17
|
-
const key = this.getKey(input.collectionSlug, input.collectionId);
|
|
17
|
+
const key = this.getKey(input.collectionSlug, input.collectionId, input.targetLng);
|
|
18
18
|
const now = new Date().toISOString();
|
|
19
19
|
const task = {
|
|
20
20
|
id: crypto.randomUUID(),
|
|
@@ -65,8 +65,10 @@
|
|
|
65
65
|
}
|
|
66
66
|
return results;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Keyed by (document, target locale) so translating a second locale of the same document does not
|
|
69
|
+
// evict the first — findByCollection must be able to return one task per locale.
|
|
70
|
+
getKey(collectionSlug, collectionId, targetLng) {
|
|
71
|
+
return `${collectionSlug}:${collectionId}:${targetLng}`;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -23,6 +23,13 @@ export type TaskInput = {
|
|
|
23
23
|
targetLng: string;
|
|
24
24
|
strategy: "overwrite" | "skip_existing";
|
|
25
25
|
publishOnTranslation: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Optional scheduled-run time (debounce). When set, the job runs no earlier than this instant;
|
|
28
|
+
* omitted/`undefined` = run as soon as the runner picks it up (every existing caller). Honored by
|
|
29
|
+
* the Payload Jobs runner via `payload.jobs.queue({ waitUntil })`; the sync (dev) runner ignores it
|
|
30
|
+
* and runs immediately.
|
|
31
|
+
*/
|
|
32
|
+
waitUntil?: Date;
|
|
26
33
|
};
|
|
27
34
|
/**
|
|
28
35
|
* Normalized task representation
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `req.context` flag the translator sets on its OWN document writes so the auto-translate
|
|
3
|
+
* `afterChange` hook skips them — the loop guard's second barrier (#51 D5). A single exported constant
|
|
4
|
+
* shared by the setter (`TranslateDocumentHandler.saveTranslatedDocument`) and the reader (the
|
|
5
|
+
* auto-translate hook), so the set-side and honor-side keys can never diverge.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `types/` (a leaf contract) so both `server/features/translate-document` and
|
|
8
|
+
* `server/modules/auto-translate` import it without creating a cross-module edge.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AUTO_TRANSLATE_SKIP_CONTEXT_KEY = "translatorSkipAutoTranslate";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `req.context` flag the translator sets on its OWN document writes so the auto-translate
|
|
3
|
+
* `afterChange` hook skips them — the loop guard's second barrier (#51 D5). A single exported constant
|
|
4
|
+
* shared by the setter (`TranslateDocumentHandler.saveTranslatedDocument`) and the reader (the
|
|
5
|
+
* auto-translate hook), so the set-side and honor-side keys can never diverge.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `types/` (a leaf contract) so both `server/features/translate-document` and
|
|
8
|
+
* `server/modules/auto-translate` import it without creating a cross-module edge.
|
|
9
|
+
*/ export const AUTO_TRANSLATE_SKIP_CONTEXT_KEY = "translatorSkipAutoTranslate";
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=AutoTranslateContext.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.9.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",
|