@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
package/README.md
CHANGED
|
@@ -196,6 +196,43 @@ Dismiss acknowledges the drift without re-translating; the marker stays hidden u
|
|
|
196
196
|
changes again. When `provenance` is disabled nothing is shown. Note the fingerprint is text-only, so
|
|
197
197
|
formatting-only edits to rich text do not mark a locale stale.
|
|
198
198
|
|
|
199
|
+
### Auto-translate on source change
|
|
200
|
+
|
|
201
|
+
_Since v0.9.0._
|
|
202
|
+
|
|
203
|
+
Opt in per collection with `withAutoTranslate` and the plugin queues translations automatically when a
|
|
204
|
+
document's source-locale content changes — no manual trigger. Off by default; a collection is enabled
|
|
205
|
+
only by wrapping it.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { translatorPlugin, withAutoTranslate, createOpenAIProvider, createPayloadJobsRunner } from "@focus-reactive/payload-plugin-translator";
|
|
209
|
+
|
|
210
|
+
translatorPlugin({
|
|
211
|
+
collections: [withAutoTranslate(Posts, { targets: ["de", "fr"], debounceMs: 2000 })],
|
|
212
|
+
translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
|
|
213
|
+
runner: createPayloadJobsRunner(),
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Option | Type | Default | Meaning |
|
|
218
|
+
| ------ | ---- | ------- | ------- |
|
|
219
|
+
| `targets` | `string[]` | — | Locales to translate into. The source locale is always excluded. |
|
|
220
|
+
| `strategy` | `"overwrite" \| "skip_existing"` | `"overwrite"` | How target content is written. |
|
|
221
|
+
| `debounceMs` | `number` | `0` | Delay before the job runs, coalescing rapid edits (see below). |
|
|
222
|
+
| `sourceLocale` | `string` | `localization.defaultLocale` | Override the source locale for this collection. |
|
|
223
|
+
|
|
224
|
+
Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
|
|
225
|
+
without drafts treats every save as published); skips when no translatable content actually changed
|
|
226
|
+
(same fingerprint as stale-detection); coalesces rapid edits via `debounceMs`; the translation is saved
|
|
227
|
+
with the source document's status (published source → published translation); never re-triggers on its
|
|
228
|
+
own translation writes; and never fails the editor's save (best-effort — failures are logged).
|
|
229
|
+
|
|
230
|
+
> **Requires a working job runner.** Auto-translate only **enqueues** jobs — they run via the task
|
|
231
|
+
> runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as **Vercel**,
|
|
232
|
+
> cron-based autorun may not run automatically, so enqueued translations can sit unexecuted until
|
|
233
|
+
> triggered — e.g. an external cron hitting the run endpoint, or a self-hosted worker. Make sure your
|
|
234
|
+
> deployment actually executes queued jobs before relying on auto-translate.
|
|
235
|
+
|
|
199
236
|
### Lifecycle callbacks
|
|
200
237
|
|
|
201
238
|
_Since v0.7.0._
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CollectionConfig } from "payload";
|
|
2
|
+
import type { AutoTranslateConfig } from "./core/auto-translate-config";
|
|
3
|
+
export type { AutoTranslateConfig };
|
|
4
|
+
export type { AutoTranslateStrategy } from "./core/auto-translate-config";
|
|
5
|
+
/**
|
|
6
|
+
* Enable opt-in **auto-translate** for a collection: when a document's source-locale content changes
|
|
7
|
+
* (and is published), the plugin automatically queues translations into the configured target locales.
|
|
8
|
+
* Off by default — a collection is opted in only by wrapping it with this helper. The rule is stamped
|
|
9
|
+
* onto `collection.custom` (the input is not mutated; a new collection is returned), mirroring
|
|
10
|
+
* `withFieldTranslation` at the field level.
|
|
11
|
+
*
|
|
12
|
+
* Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
|
|
13
|
+
* without drafts treats every save as published); skips when no translatable content actually changed
|
|
14
|
+
* (drift-gate); coalesces rapid edits via `debounceMs`; never re-triggers on its own translation
|
|
15
|
+
* writes; and never fails the editor's save (best-effort).
|
|
16
|
+
*
|
|
17
|
+
* **Requires a working job runner.** Auto-translate only ENQUEUES jobs — they run via the plugin's
|
|
18
|
+
* task runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as
|
|
19
|
+
* **Vercel**, cron-based autorun may not run automatically, so enqueued translations can sit
|
|
20
|
+
* unexecuted until triggered — e.g. an external cron hitting the run-translation endpoint, or a
|
|
21
|
+
* self-hosted worker. Ensure your deployment actually executes queued jobs before relying on this.
|
|
22
|
+
*
|
|
23
|
+
* @param collection - The collection to opt in.
|
|
24
|
+
* @param config - Target locales (+ optional strategy, debounce, source-locale override).
|
|
25
|
+
* @returns A new collection with the auto-translate rule applied (the input is not mutated).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { withAutoTranslate } from '@focus-reactive/payload-plugin-translator'
|
|
30
|
+
*
|
|
31
|
+
* translatorPlugin({
|
|
32
|
+
* collections: [withAutoTranslate(Posts, { targets: ['de', 'fr'], debounceMs: 2000 })],
|
|
33
|
+
* translationProvider,
|
|
34
|
+
* runner,
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @since 0.9.0
|
|
39
|
+
*/
|
|
40
|
+
export declare function withAutoTranslate(collection: CollectionConfig, config: AutoTranslateConfig): CollectionConfig;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AUTO_TRANSLATE_CUSTOM_KEY } from "./core/auto-translate-config";
|
|
2
|
+
/**
|
|
3
|
+
* Enable opt-in **auto-translate** for a collection: when a document's source-locale content changes
|
|
4
|
+
* (and is published), the plugin automatically queues translations into the configured target locales.
|
|
5
|
+
* Off by default — a collection is opted in only by wrapping it with this helper. The rule is stamped
|
|
6
|
+
* onto `collection.custom` (the input is not mutated; a new collection is returned), mirroring
|
|
7
|
+
* `withFieldTranslation` at the field level.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
|
|
10
|
+
* without drafts treats every save as published); skips when no translatable content actually changed
|
|
11
|
+
* (drift-gate); coalesces rapid edits via `debounceMs`; never re-triggers on its own translation
|
|
12
|
+
* writes; and never fails the editor's save (best-effort).
|
|
13
|
+
*
|
|
14
|
+
* **Requires a working job runner.** Auto-translate only ENQUEUES jobs — they run via the plugin's
|
|
15
|
+
* task runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as
|
|
16
|
+
* **Vercel**, cron-based autorun may not run automatically, so enqueued translations can sit
|
|
17
|
+
* unexecuted until triggered — e.g. an external cron hitting the run-translation endpoint, or a
|
|
18
|
+
* self-hosted worker. Ensure your deployment actually executes queued jobs before relying on this.
|
|
19
|
+
*
|
|
20
|
+
* @param collection - The collection to opt in.
|
|
21
|
+
* @param config - Target locales (+ optional strategy, debounce, source-locale override).
|
|
22
|
+
* @returns A new collection with the auto-translate rule applied (the input is not mutated).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { withAutoTranslate } from '@focus-reactive/payload-plugin-translator'
|
|
27
|
+
*
|
|
28
|
+
* translatorPlugin({
|
|
29
|
+
* collections: [withAutoTranslate(Posts, { targets: ['de', 'fr'], debounceMs: 2000 })],
|
|
30
|
+
* translationProvider,
|
|
31
|
+
* runner,
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @since 0.9.0
|
|
36
|
+
*/ export function withAutoTranslate(collection, config) {
|
|
37
|
+
return {
|
|
38
|
+
...collection,
|
|
39
|
+
custom: {
|
|
40
|
+
...collection.custom ?? {},
|
|
41
|
+
[AUTO_TRANSLATE_CUSTOM_KEY]: config
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=auto-translate-config.js.map
|
|
@@ -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,
|
|
@@ -7,6 +7,7 @@ import { useCollectionTranslationStatus } from "./api/queries/useCollectionTrans
|
|
|
7
7
|
import { useDocumentStaleness } from "./api/queries/useDocumentStaleness";
|
|
8
8
|
import { useDocumentTranslation } from "./api/queries/useDocumentTranslation";
|
|
9
9
|
export { TranslationStatusList } from "./ui/TranslationStatusList";
|
|
10
|
+
export { AutoTranslateMarker } from "./ui/AutoTranslateMarker";
|
|
10
11
|
export declare const TranslationsApi: {
|
|
11
12
|
useRunDocumentTranslation: typeof useRunDocumentTranslation;
|
|
12
13
|
useQueueDocumentTranslation: typeof useQueueDocumentTranslation;
|
|
@@ -19,7 +20,7 @@ export declare const TranslationsApi: {
|
|
|
19
20
|
};
|
|
20
21
|
export type { DocumentTranslation, DocumentTranslationCompleted, DocumentTranslationFailed, DocumentTranslationPending, DocumentTranslationRunning, CollectionTranslationStatus, CollectionTranslationStatusItem, GroupedCollectionTranslationStatus, DocumentStaleness, StalenessLocale, } from "./model/types";
|
|
21
22
|
export { DocumentTranslationStatus } from "./model/enums";
|
|
22
|
-
export { derivePanelStatus, deriveCollectionPanelStatus, describePanelStatus, } from "./model/panelStatus";
|
|
23
|
+
export { derivePanelStatus, deriveCollectionPanelStatus, deriveDocumentRunStatus, describePanelStatus, } from "./model/panelStatus";
|
|
23
24
|
export type { PanelStatus, MarkerTone } from "./model/panelStatus";
|
|
24
25
|
export { PanelStatusMarker } from "./ui/PanelStatusMarker";
|
|
25
26
|
export { ActionButton } from "./ui/ActionButton";
|
|
@@ -7,6 +7,7 @@ import { useCollectionTranslationStatus } from "./api/queries/useCollectionTrans
|
|
|
7
7
|
import { useDocumentStaleness } from "./api/queries/useDocumentStaleness";
|
|
8
8
|
import { useDocumentTranslation } from "./api/queries/useDocumentTranslation";
|
|
9
9
|
export { TranslationStatusList } from "./ui/TranslationStatusList";
|
|
10
|
+
export { AutoTranslateMarker } from "./ui/AutoTranslateMarker";
|
|
10
11
|
export const TranslationsApi = {
|
|
11
12
|
useRunDocumentTranslation,
|
|
12
13
|
useQueueDocumentTranslation,
|
|
@@ -18,7 +19,7 @@ export const TranslationsApi = {
|
|
|
18
19
|
useDismissStaleness
|
|
19
20
|
};
|
|
20
21
|
export { DocumentTranslationStatus } from "./model/enums";
|
|
21
|
-
export { derivePanelStatus, deriveCollectionPanelStatus, describePanelStatus } from "./model/panelStatus";
|
|
22
|
+
export { derivePanelStatus, deriveCollectionPanelStatus, deriveDocumentRunStatus, describePanelStatus } from "./model/panelStatus";
|
|
22
23
|
export { PanelStatusMarker } from "./ui/PanelStatusMarker";
|
|
23
24
|
export { ActionButton } from "./ui/ActionButton";
|
|
24
25
|
export { buildTranslationStatusRows } from "./model/statusRows";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The auto-translate facts the marker renders — resolved from a collection's opt-in config. */
|
|
2
|
+
export type AutoTranslateSummary = {
|
|
3
|
+
targets: string[];
|
|
4
|
+
sourceLocale: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a collection's auto-translate summary for the popup marker, or `null` when it should not
|
|
8
|
+
* render. Reads the opt-in from the collection's `custom` (propagated onto the registered collection at
|
|
9
|
+
* init), resolves the source locale (per-collection override else the config default), and drops the
|
|
10
|
+
* source from the displayed targets. Returns `null` when off, when no source locale is resolvable, or
|
|
11
|
+
* when no target remains — so the caller renders the marker only when it is both enabled and meaningful.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveAutoTranslateSummary(collection: {
|
|
14
|
+
custom?: Record<string, unknown>;
|
|
15
|
+
} | undefined, defaultLocale: string | undefined): AutoTranslateSummary | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getAutoTranslateConfig } from "../../../../core/auto-translate-config";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a collection's auto-translate summary for the popup marker, or `null` when it should not
|
|
4
|
+
* render. Reads the opt-in from the collection's `custom` (propagated onto the registered collection at
|
|
5
|
+
* init), resolves the source locale (per-collection override else the config default), and drops the
|
|
6
|
+
* source from the displayed targets. Returns `null` when off, when no source locale is resolvable, or
|
|
7
|
+
* when no target remains — so the caller renders the marker only when it is both enabled and meaningful.
|
|
8
|
+
*/ export function resolveAutoTranslateSummary(collection, defaultLocale) {
|
|
9
|
+
if (!collection) return null;
|
|
10
|
+
const config = getAutoTranslateConfig(collection);
|
|
11
|
+
if (!config) return null;
|
|
12
|
+
const sourceLocale = config.sourceLocale ?? defaultLocale;
|
|
13
|
+
if (!sourceLocale) return null;
|
|
14
|
+
const targets = config.targets.filter((target)=>target !== sourceLocale);
|
|
15
|
+
if (targets.length === 0) return null;
|
|
16
|
+
return {
|
|
17
|
+
targets,
|
|
18
|
+
sourceLocale
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=autoTranslateSummary.js.map
|
|
@@ -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;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type AutoTranslateMarkerProps = {
|
|
2
|
+
/** Target locale codes this collection auto-translates into (source already excluded). */
|
|
3
|
+
targets: string[];
|
|
4
|
+
/** The resolved source locale code changes are watched on. */
|
|
5
|
+
sourceLocale: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A quiet, off-to-the-side marker in the translation popups' header (document + collection): a single
|
|
9
|
+
* muted icon that a collection is opted into auto-translate. The detail lives in the tooltip, not the
|
|
10
|
+
* layout — this is ambient config the editor rarely needs, so it stays out of the popup's main vertical
|
|
11
|
+
* flow (title / Translate / Status). Rendered only when auto-translate is enabled.
|
|
12
|
+
*/
|
|
13
|
+
export declare function AutoTranslateMarker({ targets, sourceLocale }: AutoTranslateMarkerProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AutoTranslateIcon } from "../../../../shared/lib/assets/icons/AutoTranslateIcon";
|
|
4
|
+
import Tooltip from "../../../../shared/ui/Tooltip";
|
|
5
|
+
import styles from "./styles.module.scss";
|
|
6
|
+
/**
|
|
7
|
+
* A quiet, off-to-the-side marker in the translation popups' header (document + collection): a single
|
|
8
|
+
* muted icon that a collection is opted into auto-translate. The detail lives in the tooltip, not the
|
|
9
|
+
* layout — this is ambient config the editor rarely needs, so it stays out of the popup's main vertical
|
|
10
|
+
* flow (title / Translate / Status). Rendered only when auto-translate is enabled.
|
|
11
|
+
*/ export function AutoTranslateMarker({ targets, sourceLocale }) {
|
|
12
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
13
|
+
content: /*#__PURE__*/ _jsxs("span", {
|
|
14
|
+
className: styles.tip,
|
|
15
|
+
children: [
|
|
16
|
+
"Auto-translate is on. Publishing changes to the source (",
|
|
17
|
+
/*#__PURE__*/ _jsx("code", {
|
|
18
|
+
children: sourceLocale
|
|
19
|
+
}),
|
|
20
|
+
") content queues translations into ",
|
|
21
|
+
targets.join(" · "),
|
|
22
|
+
"."
|
|
23
|
+
]
|
|
24
|
+
}),
|
|
25
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
26
|
+
className: styles.marker,
|
|
27
|
+
tabIndex: 0,
|
|
28
|
+
"aria-label": "Auto-translate enabled",
|
|
29
|
+
children: /*#__PURE__*/ _jsx(AutoTranslateIcon, {})
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=AutoTranslateMarker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AutoTranslateMarker } from "./AutoTranslateMarker";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A muted, low-emphasis header marker — no border, no fill; it sits at the edge of the title row and
|
|
2
|
+
// carries all detail in its tooltip, so it never competes with the popup's main flow.
|
|
3
|
+
.marker {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
color: var(--theme-elevation-400, #a5a5a5);
|
|
8
|
+
cursor: help;
|
|
9
|
+
outline: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.marker:hover {
|
|
13
|
+
color: var(--theme-elevation-600, #6d6d6d);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.marker:focus-visible {
|
|
17
|
+
outline: 2px solid var(--theme-elevation-400, #a5a5a5);
|
|
18
|
+
outline-offset: 2px;
|
|
19
|
+
border-radius: 3px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tip {
|
|
23
|
+
display: block;
|
|
24
|
+
max-width: 15rem;
|
|
25
|
+
line-height: 1.35;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tip code {
|
|
29
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
30
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** A lightning bolt — connotes "fires automatically" (on the publish event). Used by the
|
|
2
|
+
* auto-translate marker to distinguish automatic translation from the manual translate control. */
|
|
3
|
+
export declare const AutoTranslateIcon: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A lightning bolt — connotes "fires automatically" (on the publish event). Used by the
|
|
2
|
+
* auto-translate marker to distinguish automatic translation from the manual translate control. */ import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
export const AutoTranslateIcon = ()=>/*#__PURE__*/ _jsx("svg", {
|
|
4
|
+
fill: "currentColor",
|
|
5
|
+
width: "1em",
|
|
6
|
+
height: "1em",
|
|
7
|
+
viewBox: "0 0 24 24",
|
|
8
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
9
|
+
children: /*#__PURE__*/ _jsx("path", {
|
|
10
|
+
d: "M13 2 L4.5 13.5 H11 L10 22 L19.5 10 H12.5 Z"
|
|
11
|
+
})
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=AutoTranslateIcon.js.map
|