@focus-reactive/payload-plugin-translator 0.8.0 → 0.8.2
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/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 +1 -1
- package/dist/client/entities/translation/index.js +1 -1
- 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/widgets/translate-document/ui/TranslateDocument.js +4 -4
- package/dist/composition/levels/useDocTranslationApi.d.ts +2 -2
- package/dist/composition/levels/useDocTranslationApi.js +3 -4
- package/dist/plugin.js +21 -74
- package/dist/server/features/createTranslationRoutes.d.ts +4 -13
- package/dist/server/features/createTranslationRoutes.js +3 -4
- 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/staleness/dismissStaleness.handler.js +6 -2
- package/dist/server/features/staleness/getDocumentStaleness.handler.js +2 -2
- package/dist/server/features/staleness/index.d.ts +1 -1
- package/dist/server/features/staleness/model.d.ts +4 -16
- package/dist/server/features/translate-document/handler.d.ts +6 -8
- package/dist/server/features/translate-document/handler.js +20 -46
- package/dist/server/features/translate-document/index.d.ts +1 -0
- package/dist/server/features/translate-document/index.js +1 -0
- package/dist/server/features/translate-document/wireTranslateRunner.d.ts +28 -0
- package/dist/server/features/translate-document/wireTranslateRunner.js +49 -0
- package/dist/server/modules/provenance/{provenanceCollection.d.ts → Provenance.collection.d.ts} +17 -9
- package/dist/server/modules/provenance/{provenanceCollection.js → Provenance.collection.js} +23 -9
- package/dist/server/modules/provenance/Provenance.service.d.ts +56 -0
- package/dist/server/modules/provenance/Provenance.service.js +124 -0
- package/dist/server/modules/provenance/Provenance.shapes.d.ts +26 -0
- package/dist/server/modules/provenance/Provenance.shapes.js +7 -0
- package/dist/server/modules/provenance/{PayloadProvenanceStore.js → Provenance.store.js} +1 -1
- package/dist/server/modules/provenance/Provenance.wiring.d.ts +23 -0
- package/dist/server/modules/provenance/Provenance.wiring.js +43 -0
- package/dist/server/modules/provenance/{provenanceCleanupHook.d.ts → ProvenanceCleanup.hook.d.ts} +4 -3
- package/dist/server/modules/provenance/{provenanceCleanupHook.js → ProvenanceCleanup.hook.js} +1 -1
- package/dist/server/modules/provenance/index.d.ts +8 -4
- package/dist/server/modules/provenance/index.js +6 -4
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +11 -2
- package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.js +5 -3
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -14
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -2
- package/dist/server/modules/translation-levels/index.d.ts +1 -1
- package/dist/server/modules/translation-levels/types.d.ts +20 -12
- package/dist/server/shared/payload/sourceDocument.d.ts +8 -0
- package/dist/server/shared/payload/sourceDocument.js +15 -0
- package/dist/types/ConfigModifier.d.ts +10 -0
- package/dist/types/ConfigModifier.js +10 -0
- package/package.json +1 -1
- package/dist/server/features/_lib/sourceDocument.d.ts +0 -9
- package/dist/server/features/_lib/sourceDocument.js +0 -16
- package/dist/server/features/staleness/service.d.ts +0 -15
- package/dist/server/features/staleness/service.js +0 -78
- /package/dist/server/modules/provenance/{PayloadProvenanceStore.d.ts → Provenance.store.d.ts} +0 -0
|
@@ -9,7 +9,7 @@ type Options = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function useDocumentTranslation({ collection, id }: Props, options?: Options): {
|
|
11
11
|
invalidate: () => void;
|
|
12
|
-
data: NoInfer<DocumentTranslation>;
|
|
12
|
+
data: NoInfer<DocumentTranslation[]>;
|
|
13
13
|
error: Error;
|
|
14
14
|
isError: true;
|
|
15
15
|
isPending: false;
|
|
@@ -32,12 +32,12 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
32
32
|
isRefetching: boolean;
|
|
33
33
|
isStale: boolean;
|
|
34
34
|
isEnabled: boolean;
|
|
35
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
35
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
36
36
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
37
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
37
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
38
38
|
} | {
|
|
39
39
|
invalidate: () => void;
|
|
40
|
-
data: NoInfer<DocumentTranslation>;
|
|
40
|
+
data: NoInfer<DocumentTranslation[]>;
|
|
41
41
|
error: null;
|
|
42
42
|
isError: false;
|
|
43
43
|
isPending: false;
|
|
@@ -60,9 +60,9 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
60
60
|
isRefetching: boolean;
|
|
61
61
|
isStale: boolean;
|
|
62
62
|
isEnabled: boolean;
|
|
63
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
63
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
64
64
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
65
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
65
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
66
66
|
} | {
|
|
67
67
|
invalidate: () => void;
|
|
68
68
|
data: undefined;
|
|
@@ -88,9 +88,9 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
88
88
|
isRefetching: boolean;
|
|
89
89
|
isStale: boolean;
|
|
90
90
|
isEnabled: boolean;
|
|
91
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
91
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
92
92
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
93
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
93
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
94
94
|
} | {
|
|
95
95
|
invalidate: () => void;
|
|
96
96
|
data: undefined;
|
|
@@ -116,9 +116,9 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
116
116
|
isRefetching: boolean;
|
|
117
117
|
isStale: boolean;
|
|
118
118
|
isEnabled: boolean;
|
|
119
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
119
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
120
120
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
121
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
121
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
122
122
|
} | {
|
|
123
123
|
invalidate: () => void;
|
|
124
124
|
data: undefined;
|
|
@@ -144,12 +144,12 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
144
144
|
isRefetching: boolean;
|
|
145
145
|
isStale: boolean;
|
|
146
146
|
isEnabled: boolean;
|
|
147
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
147
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
148
148
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
149
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
149
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
150
150
|
} | {
|
|
151
151
|
invalidate: () => void;
|
|
152
|
-
data: NoInfer<DocumentTranslation>;
|
|
152
|
+
data: NoInfer<DocumentTranslation[]>;
|
|
153
153
|
isError: false;
|
|
154
154
|
error: null;
|
|
155
155
|
isPending: false;
|
|
@@ -172,8 +172,8 @@ export declare function useDocumentTranslation({ collection, id }: Props, option
|
|
|
172
172
|
isRefetching: boolean;
|
|
173
173
|
isStale: boolean;
|
|
174
174
|
isEnabled: boolean;
|
|
175
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation>, Error>>;
|
|
175
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<DocumentTranslation[]>, Error>>;
|
|
176
176
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
177
|
-
promise: Promise<NoInfer<DocumentTranslation>>;
|
|
177
|
+
promise: Promise<NoInfer<DocumentTranslation[]>>;
|
|
178
178
|
};
|
|
179
179
|
export {};
|
|
@@ -26,12 +26,16 @@ export function useDocumentTranslation({ collection, id }, options) {
|
|
|
26
26
|
method: "get",
|
|
27
27
|
signal
|
|
28
28
|
});
|
|
29
|
-
|
|
29
|
+
// Normalize to an array defensively: a stale admin tab from before the object→array response
|
|
30
|
+
// change (deploy skew) would otherwise feed a non-array here and break the polling callback.
|
|
31
|
+
return Array.isArray(response.data) ? response.data : [];
|
|
30
32
|
});
|
|
31
33
|
},
|
|
32
34
|
enabled: options?.enabled,
|
|
35
|
+
// Keep polling while any locale's job is still in flight; stop once every job is terminal.
|
|
33
36
|
refetchInterval: (query)=>{
|
|
34
|
-
|
|
37
|
+
const active = (query.state.data ?? []).some((job)=>job.status === "pending" || job.status === "running");
|
|
38
|
+
return active ? POLLING_INTERVAL_MILLISECONDS : false;
|
|
35
39
|
}
|
|
36
40
|
});
|
|
37
41
|
const invalidate = useCallback(()=>{
|
|
@@ -49,23 +53,35 @@ export function useDocumentTranslation({ collection, id }, options) {
|
|
|
49
53
|
// When a queued (async) translation transitions to "completed" in-session, the provenance
|
|
50
54
|
// record's sourceFingerprint has just been updated server-side — invalidate staleness so the
|
|
51
55
|
// "Out of date" notice clears without waiting for the next focus/remount refetch (#50).
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
56
|
+
// Tracked as a *set* of completed target locales (the feed is now per-locale): fire whenever a
|
|
57
|
+
// locale newly reaches "completed". The snapshot is tagged with the document key and reset when
|
|
58
|
+
// the document changes, so a cold mount of an already-completed document — or navigating to a
|
|
59
|
+
// different document within the same component instance — does NOT fire a spurious extra refetch.
|
|
60
|
+
const docKey = `${collection}:${id}`;
|
|
61
|
+
const previousCompletedRef = useRef(undefined);
|
|
55
62
|
useEffect(()=>{
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
const jobs = query.data;
|
|
64
|
+
if (!jobs) return;
|
|
65
|
+
const completed = new Set(jobs.filter((job)=>job.status === "completed").map((job)=>job.input.target_lng));
|
|
66
|
+
const previous = previousCompletedRef.current;
|
|
67
|
+
const sameDoc = previous?.docKey === docKey;
|
|
68
|
+
if (sameDoc && [
|
|
69
|
+
...completed
|
|
70
|
+
].some((locale)=>!previous.locales.has(locale))) {
|
|
59
71
|
queryClient.invalidateQueries({
|
|
60
72
|
queryKey: [
|
|
61
73
|
DOCUMENT_STALENESS_QUERY_KEY
|
|
62
74
|
]
|
|
63
75
|
});
|
|
64
76
|
}
|
|
65
|
-
|
|
77
|
+
previousCompletedRef.current = {
|
|
78
|
+
docKey,
|
|
79
|
+
locales: completed
|
|
80
|
+
};
|
|
66
81
|
}, [
|
|
67
|
-
query.data
|
|
68
|
-
queryClient
|
|
82
|
+
query.data,
|
|
83
|
+
queryClient,
|
|
84
|
+
docKey
|
|
69
85
|
]);
|
|
70
86
|
return {
|
|
71
87
|
...query,
|
|
@@ -19,7 +19,7 @@ export declare const TranslationsApi: {
|
|
|
19
19
|
};
|
|
20
20
|
export type { DocumentTranslation, DocumentTranslationCompleted, DocumentTranslationFailed, DocumentTranslationPending, DocumentTranslationRunning, CollectionTranslationStatus, CollectionTranslationStatusItem, GroupedCollectionTranslationStatus, DocumentStaleness, StalenessLocale, } from "./model/types";
|
|
21
21
|
export { DocumentTranslationStatus } from "./model/enums";
|
|
22
|
-
export { derivePanelStatus, deriveCollectionPanelStatus, describePanelStatus, } from "./model/panelStatus";
|
|
22
|
+
export { derivePanelStatus, deriveCollectionPanelStatus, deriveDocumentRunStatus, describePanelStatus, } from "./model/panelStatus";
|
|
23
23
|
export type { PanelStatus, MarkerTone } from "./model/panelStatus";
|
|
24
24
|
export { PanelStatusMarker } from "./ui/PanelStatusMarker";
|
|
25
25
|
export { ActionButton } from "./ui/ActionButton";
|
|
@@ -18,7 +18,7 @@ export const TranslationsApi = {
|
|
|
18
18
|
useDismissStaleness
|
|
19
19
|
};
|
|
20
20
|
export { DocumentTranslationStatus } from "./model/enums";
|
|
21
|
-
export { derivePanelStatus, deriveCollectionPanelStatus, describePanelStatus } from "./model/panelStatus";
|
|
21
|
+
export { derivePanelStatus, deriveCollectionPanelStatus, deriveDocumentRunStatus, describePanelStatus } from "./model/panelStatus";
|
|
22
22
|
export { PanelStatusMarker } from "./ui/PanelStatusMarker";
|
|
23
23
|
export { ActionButton } from "./ui/ActionButton";
|
|
24
24
|
export { buildTranslationStatusRows } from "./model/statusRows";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DocumentTranslationStatus } from "./enums";
|
|
2
2
|
import type { StatusDotColor } from "./statusRows";
|
|
3
|
-
import type { GroupedCollectionTranslationStatus } from "./types";
|
|
3
|
+
import type { DocumentTranslation, GroupedCollectionTranslationStatus } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* The single aggregate signal shown on a translate trigger (document or collection). Detail lives in
|
|
6
6
|
* the popup; the panel only surfaces the most urgent state at a glance (see the panel-UI design doc).
|
|
@@ -37,8 +37,14 @@ export declare function describePanelStatus(status: PanelStatus | undefined, fal
|
|
|
37
37
|
tone?: MarkerTone;
|
|
38
38
|
title: string;
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Collapse the per-locale job feed into the single most-urgent run status for the trigger marker.
|
|
42
|
+
* Returns `undefined` when there are no jobs. The panel shows one aggregate signal; the popup's list
|
|
43
|
+
* carries the full per-locale detail.
|
|
44
|
+
*/
|
|
45
|
+
export declare function deriveDocumentRunStatus(runs: DocumentTranslation[] | undefined): DocumentTranslationStatus | undefined;
|
|
40
46
|
type PanelStatusInput = {
|
|
41
|
-
/** Status of the
|
|
47
|
+
/** Status of the most-urgent translation job across locales, if any (see `deriveDocumentRunStatus`). */
|
|
42
48
|
runStatus?: DocumentTranslationStatus | null;
|
|
43
49
|
/** Target locales whose translation is out of date (from staleness detection). */
|
|
44
50
|
staleLocales: string[];
|
|
@@ -50,6 +50,22 @@ import { DocumentTranslationStatus } from "./enums";
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
// The most-urgent job status wins the single aggregate marker, in the order `derivePanelStatus`
|
|
54
|
+
// itself ranks them: a failure to surface, then in-flight work, then a queued job, then completed.
|
|
55
|
+
const RUN_STATUS_PRIORITY = [
|
|
56
|
+
DocumentTranslationStatus.FAILED,
|
|
57
|
+
DocumentTranslationStatus.RUNNING,
|
|
58
|
+
DocumentTranslationStatus.PENDING,
|
|
59
|
+
DocumentTranslationStatus.COMPLETED
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Collapse the per-locale job feed into the single most-urgent run status for the trigger marker.
|
|
63
|
+
* Returns `undefined` when there are no jobs. The panel shows one aggregate signal; the popup's list
|
|
64
|
+
* carries the full per-locale detail.
|
|
65
|
+
*/ export function deriveDocumentRunStatus(runs) {
|
|
66
|
+
const present = new Set(runs?.map((run)=>run.status));
|
|
67
|
+
return RUN_STATUS_PRIORITY.find((status)=>present.has(status));
|
|
68
|
+
}
|
|
53
69
|
/**
|
|
54
70
|
* Collapse the latest run status + per-locale staleness into one panel marker, by priority:
|
|
55
71
|
* `failed → running → stale → fresh → none`. A more urgent transient state (a failed or in-flight
|
|
@@ -29,14 +29,17 @@ export declare const STATE_DOT: Record<TranslationRowState, {
|
|
|
29
29
|
animated?: boolean;
|
|
30
30
|
}>;
|
|
31
31
|
/**
|
|
32
|
-
* Build the unified status rows from per-locale staleness + the
|
|
32
|
+
* Build the unified status rows from per-locale staleness + the latest job per target locale.
|
|
33
33
|
*
|
|
34
|
-
* Rows come from `staleness.locales` (one per translated target: `stale` or `translated`).
|
|
35
|
-
*
|
|
36
|
-
* the durable signal for that locale, and a
|
|
37
|
-
*
|
|
34
|
+
* Rows come from `staleness.locales` (one per translated target: `stale` or `translated`). Each job in
|
|
35
|
+
* `runs` (one per target locale — see `useDocumentTranslation`) is overlaid onto its target locale: a
|
|
36
|
+
* **transient** job state (failed/running/pending) wins over the durable signal for that locale, and a
|
|
37
|
+
* job for a target with no provenance row yet adds a row. Overlaying *every* job (not just one) is what
|
|
38
|
+
* lets several concurrent re-translations each show their own live state instead of the last one
|
|
39
|
+
* appearing to overwrite the rest. Sorted by the fixed source→target locale pair (not by state), so a
|
|
40
|
+
* row keeps its place when its state changes — re-translating a locale never reorders the list.
|
|
38
41
|
*/
|
|
39
42
|
export declare function buildTranslationStatusRows(input: {
|
|
40
43
|
staleness?: DocumentStaleness | null;
|
|
41
|
-
|
|
44
|
+
runs?: DocumentTranslation[];
|
|
42
45
|
}): TranslationStatusRow[];
|
|
@@ -20,32 +20,30 @@
|
|
|
20
20
|
color: "green"
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
// Most urgent first. Transient job states outrank the durable stale/translated signal.
|
|
24
|
-
const ORDER = {
|
|
25
|
-
failed: 0,
|
|
26
|
-
running: 1,
|
|
27
|
-
pending: 2,
|
|
28
|
-
stale: 3,
|
|
29
|
-
translated: 4
|
|
30
|
-
};
|
|
31
23
|
const TRANSIENT = new Set([
|
|
32
24
|
"failed",
|
|
33
25
|
"running",
|
|
34
26
|
"pending"
|
|
35
27
|
]);
|
|
28
|
+
// A row's permanent identity for ordering: the source→target locale pair (unique per row, never
|
|
29
|
+
// changes when the row's state does).
|
|
30
|
+
const localePairKey = (row)=>`${row.sourceLocale}:${row.targetLocale}`;
|
|
36
31
|
// Maps the closed 4-member job-status set to a row state. "completed" → "translated"; the transient
|
|
37
32
|
// three pass through. If a new DocumentTranslationStatus is ever added, extend this explicitly —
|
|
38
33
|
// otherwise it would silently render as "translated".
|
|
39
34
|
const toRowState = (jobStatus)=>jobStatus === "failed" || jobStatus === "running" || jobStatus === "pending" ? jobStatus : "translated";
|
|
40
35
|
/**
|
|
41
|
-
* Build the unified status rows from per-locale staleness + the
|
|
36
|
+
* Build the unified status rows from per-locale staleness + the latest job per target locale.
|
|
42
37
|
*
|
|
43
|
-
* Rows come from `staleness.locales` (one per translated target: `stale` or `translated`).
|
|
44
|
-
*
|
|
45
|
-
* the durable signal for that locale, and a
|
|
46
|
-
*
|
|
38
|
+
* Rows come from `staleness.locales` (one per translated target: `stale` or `translated`). Each job in
|
|
39
|
+
* `runs` (one per target locale — see `useDocumentTranslation`) is overlaid onto its target locale: a
|
|
40
|
+
* **transient** job state (failed/running/pending) wins over the durable signal for that locale, and a
|
|
41
|
+
* job for a target with no provenance row yet adds a row. Overlaying *every* job (not just one) is what
|
|
42
|
+
* lets several concurrent re-translations each show their own live state instead of the last one
|
|
43
|
+
* appearing to overwrite the rest. Sorted by the fixed source→target locale pair (not by state), so a
|
|
44
|
+
* row keeps its place when its state changes — re-translating a locale never reorders the list.
|
|
47
45
|
*/ export function buildTranslationStatusRows(input) {
|
|
48
|
-
const { staleness,
|
|
46
|
+
const { staleness, runs } = input;
|
|
49
47
|
const byTarget = new Map();
|
|
50
48
|
for (const locale of staleness?.locales ?? []){
|
|
51
49
|
byTarget.set(locale.target_lng, {
|
|
@@ -55,7 +53,7 @@ const toRowState = (jobStatus)=>jobStatus === "failed" || jobStatus === "running
|
|
|
55
53
|
at: locale.translated_at
|
|
56
54
|
});
|
|
57
55
|
}
|
|
58
|
-
|
|
56
|
+
for (const run of runs ?? []){
|
|
59
57
|
const state = toRowState(run.status);
|
|
60
58
|
const target = run.input.target_lng;
|
|
61
59
|
const existing = byTarget.get(target);
|
|
@@ -71,9 +69,13 @@ const toRowState = (jobStatus)=>jobStatus === "failed" || jobStatus === "running
|
|
|
71
69
|
});
|
|
72
70
|
}
|
|
73
71
|
}
|
|
72
|
+
// Stable order by the source→target locale pair — the one thing about a row that never changes when
|
|
73
|
+
// its state does. Sorting by state/time instead would make a row jump (e.g. to the top) the moment
|
|
74
|
+
// you re-translate it; the locale pair keeps every row in a fixed place, and progress is conveyed by
|
|
75
|
+
// the badge/dot, not the position. The pair is unique (one row per target), so no tie-break needed.
|
|
74
76
|
return [
|
|
75
77
|
...byTarget.values()
|
|
76
|
-
].sort((a, b)=>
|
|
78
|
+
].sort((a, b)=>localePairKey(a).localeCompare(localePairKey(b)));
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
//# sourceMappingURL=statusRows.js.map
|
|
@@ -35,7 +35,12 @@ export type DocumentTranslationCompleted = {
|
|
|
35
35
|
completed_at: string;
|
|
36
36
|
input: InputData;
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* One translation job for a single target locale. The document status feed is an array of these —
|
|
40
|
+
* the latest job per target locale (see `useDocumentTranslation`), because re-translate queues an
|
|
41
|
+
* independent job per locale.
|
|
42
|
+
*/
|
|
43
|
+
export type DocumentTranslation = DocumentTranslationCompleted | DocumentTranslationRunning | DocumentTranslationFailed | DocumentTranslationPending;
|
|
39
44
|
export type CollectionTranslationStatusItem = {
|
|
40
45
|
id: string;
|
|
41
46
|
status: DocumentTranslationStatus;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { toast, useLocale } from "@payloadcms/ui";
|
|
4
4
|
import { useEffect, useMemo } from "react";
|
|
5
|
-
import { buildTranslationStatusRows, derivePanelStatus, TranslationsApi, TranslationStatusList } from "../../../entities/translation";
|
|
5
|
+
import { buildTranslationStatusRows, deriveDocumentRunStatus, derivePanelStatus, TranslationsApi, TranslationStatusList } from "../../../entities/translation";
|
|
6
6
|
import { OpenDocumentTranslationPopup } from "../../../features/open-document-translation-popup";
|
|
7
7
|
import { DocumentTranslationForm, FORM_FIELDS } from "../../../features/translate-document-form";
|
|
8
8
|
import { DocumentTranslationFormModel } from "../../../features/translate-document-form/index.client";
|
|
@@ -27,15 +27,15 @@ const TranslateDocument = ({ hasDrafts })=>{
|
|
|
27
27
|
]);
|
|
28
28
|
// The panel trigger shows one aggregate marker; the popup shows the full per-locale list.
|
|
29
29
|
const panelStatus = useMemo(()=>derivePanelStatus({
|
|
30
|
-
runStatus: data
|
|
30
|
+
runStatus: deriveDocumentRunStatus(data),
|
|
31
31
|
staleLocales
|
|
32
32
|
}), [
|
|
33
|
-
data
|
|
33
|
+
data,
|
|
34
34
|
staleLocales
|
|
35
35
|
]);
|
|
36
36
|
const statusRows = useMemo(()=>buildTranslationStatusRows({
|
|
37
37
|
staleness,
|
|
38
|
-
|
|
38
|
+
runs: data
|
|
39
39
|
}), [
|
|
40
40
|
staleness,
|
|
41
41
|
data
|
|
@@ -3,7 +3,7 @@ import type { LevelContext } from "../../server/modules/translation-levels";
|
|
|
3
3
|
* Contribute the runner-agnostic document-translation API — the shared route
|
|
4
4
|
* bundle, bound to the level context's runner. Both `documentLevel` and
|
|
5
5
|
* `collectionLevel` call this; the plugin deduplicates the endpoints by method +
|
|
6
|
-
* path, so the bundle registers exactly once. The
|
|
7
|
-
*
|
|
6
|
+
* path, so the bundle registers exactly once. The provenance service factory
|
|
7
|
+
* rides along for the staleness endpoints (its schema lives inside the service).
|
|
8
8
|
*/
|
|
9
9
|
export declare function useDocTranslationApi(ctx: LevelContext): void;
|
|
@@ -3,8 +3,8 @@ import { createTranslationRoutes } from "../../server/features/createTranslation
|
|
|
3
3
|
* Contribute the runner-agnostic document-translation API — the shared route
|
|
4
4
|
* bundle, bound to the level context's runner. Both `documentLevel` and
|
|
5
5
|
* `collectionLevel` call this; the plugin deduplicates the endpoints by method +
|
|
6
|
-
* path, so the bundle registers exactly once. The
|
|
7
|
-
*
|
|
6
|
+
* path, so the bundle registers exactly once. The provenance service factory
|
|
7
|
+
* rides along for the staleness endpoints (its schema lives inside the service).
|
|
8
8
|
*/ export function useDocTranslationApi(ctx) {
|
|
9
9
|
ctx.addEndpoints(createTranslationRoutes({
|
|
10
10
|
taskRunnerFactory: ctx.taskRunnerFactory,
|
|
@@ -13,8 +13,7 @@ import { createTranslationRoutes } from "../../server/features/createTranslation
|
|
|
13
13
|
},
|
|
14
14
|
access: ctx.access,
|
|
15
15
|
basePath: ctx.basePath,
|
|
16
|
-
|
|
17
|
-
provenanceStoreFactory: ctx.provenanceStoreFactory
|
|
16
|
+
provenanceServiceFactory: ctx.provenanceServiceFactory
|
|
18
17
|
}));
|
|
19
18
|
}
|
|
20
19
|
|
package/dist/plugin.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { LifecycleNotifier, taskFromHandlerInput, withQueuedNotification } from "./server/modules/lifecycle";
|
|
2
|
+
import { configureProvenance } from "./server/modules/provenance";
|
|
3
|
+
import { wireTranslateRunner } from "./server/features/translate-document";
|
|
5
4
|
import { documentLevel, collectionLevel } from "./composition/levels";
|
|
6
5
|
import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
|
|
7
6
|
import { normalizePath } from "./server/shared";
|
|
8
|
-
/**
|
|
9
|
-
* Resolve the opt-in `provenance` config to a sidecar slug, or `null` when disabled.
|
|
10
|
-
* `false`/omitted → off; `true` or `{}` → on with the default slug; `{ slug }` → on with that slug.
|
|
11
|
-
*/ function resolveProvenanceSlug(provenance) {
|
|
12
|
-
if (!provenance) return null;
|
|
13
|
-
if (provenance === true) return DEFAULT_PROVENANCE_SLUG;
|
|
14
|
-
// `||` (not `??`) so an empty/blank slug falls back to the default instead of silently disabling.
|
|
15
|
-
return provenance.slug || DEFAULT_PROVENANCE_SLUG;
|
|
16
|
-
}
|
|
17
7
|
/** @deprecated Use `translatorPlugin` function instead */ export class TranslateCollectionPlugin {
|
|
18
8
|
pluginConfig;
|
|
19
9
|
constructor(pluginConfig){
|
|
@@ -22,61 +12,30 @@ import { normalizePath } from "./server/shared";
|
|
|
22
12
|
init() {
|
|
23
13
|
return async (config)=>{
|
|
24
14
|
const { access, translationProvider, runner, collections, levels, provenance, lifecycle, basePath: rawBasePath = "/translate" } = this.pluginConfig;
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// to make the contract explicit and avoid reliance on JSON round-trip.
|
|
15
|
+
// Build schema map from deep-cloned collections.
|
|
16
|
+
// Deep clone is required because Payload mutates the original collection objects, removing
|
|
17
|
+
// `localized: true` from nested fields during sanitization. JSON round-trip (not
|
|
18
|
+
// structuredClone) because Lexical editor configs contain async functions structuredClone
|
|
19
|
+
// cannot handle.
|
|
20
|
+
// TODO: Consider introducing a FieldLike interface with only the properties used by the
|
|
21
|
+
// pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
|
|
22
|
+
// and avoid reliance on JSON round-trip.
|
|
34
23
|
const schemaMap = new Map(collections.map((col)=>[
|
|
35
24
|
col.slug,
|
|
36
25
|
JSON.parse(JSON.stringify(col.fields))
|
|
37
26
|
]));
|
|
38
27
|
const collectionSlugs = new Set(schemaMap.keys());
|
|
39
28
|
const basePath = normalizePath(rawBasePath);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
handler: async (payload, input)=>{
|
|
49
|
-
const notifier = new LifecycleNotifier(lifecycleCallbacks, payload.logger);
|
|
50
|
-
const task = taskFromHandlerInput(input);
|
|
51
|
-
try {
|
|
52
|
-
await translateHandler.handle(payload, {
|
|
53
|
-
collection: input.collection,
|
|
54
|
-
collectionId: input.collectionId,
|
|
55
|
-
sourceLng: input.sourceLng,
|
|
56
|
-
targetLng: input.targetLng,
|
|
57
|
-
strategy: input.strategy,
|
|
58
|
-
publishOnTranslation: input.publishOnTranslation
|
|
59
|
-
});
|
|
60
|
-
} catch (error) {
|
|
61
|
-
await notifier.failed(task, error);
|
|
62
|
-
throw error; // rethrow so the runner marks the job failed
|
|
63
|
-
}
|
|
64
|
-
await notifier.completed(task);
|
|
65
|
-
},
|
|
29
|
+
// Each concern owns its own config-time wiring and exposes it uniformly; init() just composes.
|
|
30
|
+
const provenanceModule = configureProvenance(provenance, schemaMap);
|
|
31
|
+
const { taskRunnerFactory, configModifier: runnerConfigModifier } = wireTranslateRunner({
|
|
32
|
+
translationProvider,
|
|
33
|
+
schemaMap,
|
|
34
|
+
provenanceServiceFactory: provenanceModule.serviceFactory,
|
|
35
|
+
runner,
|
|
36
|
+
lifecycle: lifecycle ?? {},
|
|
66
37
|
collections: Array.from(collectionSlugs)
|
|
67
|
-
};
|
|
68
|
-
const runnerConfigModifier = runner.configure(runnerContext);
|
|
69
|
-
// Bind the context once so routes receive a self-sufficient factory: the
|
|
70
|
-
// runner needs no mutable per-instance handler state and create() has no
|
|
71
|
-
// "configure() must run first" ordering coupling (translator plan, 0c).
|
|
72
|
-
// When an `onQueued` callback is set, decorate the runner so `enqueue` fires it.
|
|
73
|
-
const taskRunnerFactory = {
|
|
74
|
-
create: (payload)=>{
|
|
75
|
-
const taskRunner = runner.create(payload, runnerContext.handler);
|
|
76
|
-
if (!lifecycleCallbacks.onQueued) return taskRunner;
|
|
77
|
-
return withQueuedNotification(taskRunner, new LifecycleNotifier(lifecycleCallbacks, payload.logger));
|
|
78
|
-
}
|
|
79
|
-
};
|
|
38
|
+
});
|
|
80
39
|
const activeLevels = levels ?? [
|
|
81
40
|
documentLevel(),
|
|
82
41
|
collectionLevel()
|
|
@@ -88,23 +47,11 @@ import { normalizePath } from "./server/shared";
|
|
|
88
47
|
taskRunnerFactory,
|
|
89
48
|
schemaMap,
|
|
90
49
|
translationProvider,
|
|
91
|
-
|
|
50
|
+
provenanceServiceFactory: provenanceModule.serviceFactory
|
|
92
51
|
});
|
|
93
52
|
for (const level of activeLevels)level.extend(builder);
|
|
94
53
|
builder.addConfigModifier(runnerConfigModifier);
|
|
95
|
-
|
|
96
|
-
builder.addConfigModifier((cfg)=>{
|
|
97
|
-
const alreadyAdded = cfg.collections?.some((collection)=>collection.slug === provenanceSlug && isProvenanceCollection(collection));
|
|
98
|
-
if (!alreadyAdded) {
|
|
99
|
-
cfg.collections = [
|
|
100
|
-
...cfg.collections ?? [],
|
|
101
|
-
makeProvenanceCollection(provenanceSlug)
|
|
102
|
-
];
|
|
103
|
-
}
|
|
104
|
-
injectProvenanceCleanup(cfg, collectionSlugs, provenanceStoreFactory, provenanceSlug);
|
|
105
|
-
return cfg;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
54
|
+
builder.addConfigModifier(provenanceModule.configure(collectionSlugs));
|
|
108
55
|
builder.addAdminProvider(new CacheProviderExport(basePath));
|
|
109
56
|
// The single place the Payload config is mutated.
|
|
110
57
|
return builder.applyTo(config);
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import type { CollectionSlug, Endpoint } from "payload";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
import type { CollectionSchemaMap } from "../../types/CollectionSchemaMap";
|
|
5
|
-
import type { ProvenanceStoreFactory } from "../modules/provenance";
|
|
6
|
-
export type TranslationRoutesDeps = {
|
|
7
|
-
taskRunnerFactory: TaskRunnerFactory;
|
|
2
|
+
import type { TranslationContext } from "../modules/translation-levels";
|
|
3
|
+
export type TranslationRoutesDeps = Pick<TranslationContext, "taskRunnerFactory" | "access" | "provenanceServiceFactory"> & {
|
|
8
4
|
/** Collections the plugin manages — gates which slugs the routes accept. */
|
|
9
5
|
collectionConfig: {
|
|
10
6
|
availableCollections: Set<CollectionSlug>;
|
|
11
7
|
};
|
|
12
|
-
access?: AccessGuard;
|
|
13
8
|
basePath?: string;
|
|
14
|
-
/** Original per-collection field schema — needed to recompute source fingerprints for staleness. */
|
|
15
|
-
schemaMap: CollectionSchemaMap;
|
|
16
|
-
/** Builds a provenance store; absent when provenance is disabled (staleness then reports empty). */
|
|
17
|
-
provenanceStoreFactory?: ProvenanceStoreFactory;
|
|
18
9
|
};
|
|
19
10
|
/**
|
|
20
11
|
* The translator's HTTP surface as one shared bundle: the job-API endpoints
|
|
@@ -26,6 +17,6 @@ export type TranslationRoutesDeps = {
|
|
|
26
17
|
* document and collection levels each contribute this bundle via the level
|
|
27
18
|
* context; the plugin deduplicates by method + path, so it registers exactly
|
|
28
19
|
* once. The staleness endpoints always register; when provenance is disabled
|
|
29
|
-
* (`
|
|
20
|
+
* (`provenanceServiceFactory` absent) they simply report no staleness.
|
|
30
21
|
*/
|
|
31
|
-
export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath,
|
|
22
|
+
export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, provenanceServiceFactory, }: TranslationRoutesDeps): Endpoint[];
|
|
@@ -15,12 +15,11 @@ import { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./
|
|
|
15
15
|
* document and collection levels each contribute this bundle via the level
|
|
16
16
|
* context; the plugin deduplicates by method + path, so it registers exactly
|
|
17
17
|
* once. The staleness endpoints always register; when provenance is disabled
|
|
18
|
-
* (`
|
|
19
|
-
*/ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath,
|
|
18
|
+
* (`provenanceServiceFactory` absent) they simply report no staleness.
|
|
19
|
+
*/ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, provenanceServiceFactory }) {
|
|
20
20
|
const stalenessConfig = {
|
|
21
21
|
availableCollections: collectionConfig.availableCollections,
|
|
22
|
-
|
|
23
|
-
provenanceStoreFactory
|
|
22
|
+
provenanceServiceFactory
|
|
24
23
|
};
|
|
25
24
|
return [
|
|
26
25
|
createEnqueueRoute(taskRunnerFactory, collectionConfig, access, basePath),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ServerResponse } from "../../shared";
|
|
2
2
|
import { isCollectionAvailable } from "../_lib/collection-utils";
|
|
3
|
-
import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from "./model";
|
|
3
|
+
import { GetDocumentStatusInputSchema, latestTaskPerTargetLocale, taskToJobStatusOutput } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets the translation status for a specific document
|
|
6
6
|
*/ export class GetDocumentStatusHandler {
|
|
@@ -20,7 +20,9 @@ import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from "./model";
|
|
|
20
20
|
const tasks = await runner.findByCollection(collectionSlug, [
|
|
21
21
|
collection_id
|
|
22
22
|
]);
|
|
23
|
-
|
|
23
|
+
// One job per target locale (latest), not a single job for the whole document — re-translate
|
|
24
|
+
// queues an independent job per locale, and the status panel renders a row per locale.
|
|
25
|
+
return ServerResponse.success(latestTaskPerTargetLocale(tasks).map(taskToJobStatusOutput));
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -56,6 +56,16 @@ export type JobStatusOutput = {
|
|
|
56
56
|
export type GetDocumentStatusConfig = {
|
|
57
57
|
availableCollections: Set<CollectionSlug>;
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Reduce a document's jobs to the latest one per target locale.
|
|
61
|
+
*
|
|
62
|
+
* `findByCollection` returns every job for the document across all target locales (plus any superseded
|
|
63
|
+
* jobs not yet cancelled). The status panel shows one row per target locale, so we keep — per
|
|
64
|
+
* `targetLng` — the most recently created job (tie-break: most recently updated). Without this the
|
|
65
|
+
* caller sees a single arbitrary job and every other in-flight locale looks idle, which is the
|
|
66
|
+
* concurrent re-translate bug (a second re-translate appearing to overwrite the first's status).
|
|
67
|
+
*/
|
|
68
|
+
export declare function latestTaskPerTargetLocale(tasks: Task[]): Task[];
|
|
59
69
|
/**
|
|
60
70
|
* Transforms a Task to API output format (snake_case for client compatibility)
|
|
61
71
|
*/
|