@focus-reactive/payload-plugin-translator 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +18 -3
  2. package/dist/client/entities/translation/api/mutations/useDismissStaleness.d.ts +12 -0
  3. package/dist/client/entities/translation/api/mutations/useDismissStaleness.js +40 -0
  4. package/dist/client/entities/translation/api/mutations/useQueueDocumentTranslation.js +6 -0
  5. package/dist/client/entities/translation/api/mutations/useRunDocumentTranslation.js +6 -0
  6. package/dist/client/entities/translation/api/queries/useDocumentStaleness.d.ts +17 -0
  7. package/dist/client/entities/translation/api/queries/useDocumentStaleness.js +35 -0
  8. package/dist/client/entities/translation/api/queries/useDocumentTranslation.js +23 -1
  9. package/dist/client/entities/translation/index.d.ts +12 -5
  10. package/dist/client/entities/translation/index.js +10 -5
  11. package/dist/client/entities/translation/model/panelStatus.d.ts +60 -0
  12. package/dist/client/entities/translation/model/panelStatus.js +101 -0
  13. package/dist/client/entities/translation/model/statusRows.d.ts +42 -0
  14. package/dist/client/entities/translation/model/statusRows.js +79 -0
  15. package/dist/client/entities/translation/model/types.d.ts +11 -0
  16. package/dist/client/entities/translation/model/types.js +1 -1
  17. package/dist/client/entities/translation/ui/ActionButton/ActionButton.d.ts +14 -0
  18. package/dist/client/entities/translation/ui/ActionButton/ActionButton.js +28 -0
  19. package/dist/client/entities/translation/ui/ActionButton/index.d.ts +1 -0
  20. package/dist/client/entities/translation/ui/ActionButton/index.js +3 -0
  21. package/dist/client/entities/translation/ui/ActionButton/styles.module.scss +11 -0
  22. package/dist/client/entities/translation/ui/PanelStatusMarker/PanelStatusMarker.d.ts +12 -0
  23. package/dist/client/entities/translation/ui/PanelStatusMarker/PanelStatusMarker.js +26 -0
  24. package/dist/client/entities/translation/ui/PanelStatusMarker/index.d.ts +1 -0
  25. package/dist/client/entities/translation/ui/PanelStatusMarker/index.js +3 -0
  26. package/dist/client/entities/translation/ui/PanelStatusMarker/styles.module.scss +12 -0
  27. package/dist/client/entities/translation/ui/TranslationDirection/styles.module.scss +2 -2
  28. package/dist/client/entities/translation/ui/TranslationStatusList/StatusBadge.d.ts +10 -0
  29. package/dist/client/entities/translation/ui/TranslationStatusList/StatusBadge.js +27 -0
  30. package/dist/client/entities/translation/ui/TranslationStatusList/TranslationStatusList.d.ts +14 -0
  31. package/dist/client/entities/translation/ui/TranslationStatusList/TranslationStatusList.js +194 -0
  32. package/dist/client/entities/translation/ui/TranslationStatusList/index.d.ts +1 -0
  33. package/dist/client/entities/translation/ui/TranslationStatusList/index.js +3 -0
  34. package/dist/client/entities/translation/ui/TranslationStatusList/styles.module.scss +72 -0
  35. package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +17 -21
  36. package/dist/client/features/collection-translation-form/ui/styles.module.scss +14 -14
  37. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts +4 -2
  38. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.js +9 -9
  39. package/dist/client/features/collection-translation-popup/ui/styles.module.scss +5 -3
  40. package/dist/client/features/collection-translation-progress/ui/CollectionTranslationProgress.d.ts +5 -1
  41. package/dist/client/features/collection-translation-progress/ui/CollectionTranslationProgress.js +100 -93
  42. package/dist/client/features/collection-translation-progress/ui/styles.module.scss +52 -3
  43. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.d.ts +4 -1
  44. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js +16 -5
  45. package/dist/client/features/open-document-translation-popup/ui/styles.module.scss +5 -3
  46. package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +5 -2
  47. package/dist/client/features/translate-document-form/ui/styles.module.scss +8 -2
  48. package/dist/client/shared/lib/assets/icons/AlertTriangleIcon.d.ts +1 -0
  49. package/dist/client/shared/lib/assets/icons/AlertTriangleIcon.js +14 -0
  50. package/dist/client/shared/lib/assets/icons/CheckIcon.d.ts +1 -0
  51. package/dist/client/shared/lib/assets/icons/CheckIcon.js +17 -0
  52. package/dist/client/shared/lib/assets/icons/CloseIcon.d.ts +1 -0
  53. package/dist/client/shared/lib/assets/icons/CloseIcon.js +16 -0
  54. package/dist/client/shared/ui/Button/Button.js +2 -1
  55. package/dist/client/shared/ui/ColorIndicator/ColorIndicator.d.ts +1 -1
  56. package/dist/client/shared/ui/ColorIndicator/styles.module.scss +4 -0
  57. package/dist/client/shared/ui/Label/Label.d.ts +2 -0
  58. package/dist/client/shared/ui/Label/Label.js +3 -2
  59. package/dist/client/shared/ui/Label/styles.module.scss +11 -0
  60. package/dist/client/shared/ui/Loading/Loading.d.ts +6 -2
  61. package/dist/client/shared/ui/Loading/Loading.js +4 -2
  62. package/dist/client/shared/ui/Loading/barCount.d.ts +3 -0
  63. package/dist/client/shared/ui/Loading/barCount.js +11 -0
  64. package/dist/client/shared/ui/Loading/styles.module.scss +20 -27
  65. package/dist/client/shared/ui/Popup/Popup.js +3 -0
  66. package/dist/client/shared/ui/Select/Select.js +1 -1
  67. package/dist/client/shared/ui/form/FormSelect/FormSelect.js +1 -0
  68. package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.js +37 -17
  69. package/dist/client/widgets/bulk-translation-dashboard/ui/styles.module.scss +27 -0
  70. package/dist/client/widgets/translate-document/ui/TranslateDocument.js +53 -27
  71. package/dist/client/widgets/translate-document/ui/styles.module.scss +27 -0
  72. package/dist/composition/levels/useDocTranslationApi.d.ts +3 -2
  73. package/dist/composition/levels/useDocTranslationApi.js +6 -3
  74. package/dist/core/field-traversal/types.d.ts +4 -2
  75. package/dist/core/provenance/ProvenanceStore.interface.d.ts +18 -0
  76. package/dist/core/provenance/index.d.ts +1 -0
  77. package/dist/core/provenance/index.js +1 -1
  78. package/dist/core/provenance/staleness.d.ts +19 -0
  79. package/dist/core/provenance/staleness.js +21 -0
  80. package/dist/core/translation-pipeline/stages/data-reconciler/DataReconciler.js +33 -10
  81. package/dist/plugin.js +2 -1
  82. package/dist/server/features/_lib/sourceDocument.d.ts +9 -0
  83. package/dist/server/features/_lib/sourceDocument.js +16 -0
  84. package/dist/server/features/createTranslationRoutes.d.ts +12 -4
  85. package/dist/server/features/createTranslationRoutes.js +15 -5
  86. package/dist/server/features/get-document-status/model.js +7 -2
  87. package/dist/server/features/staleness/dismissStaleness.handler.d.ts +8 -0
  88. package/dist/server/features/staleness/dismissStaleness.handler.js +27 -0
  89. package/dist/server/features/staleness/getDocumentStaleness.handler.d.ts +12 -0
  90. package/dist/server/features/staleness/getDocumentStaleness.handler.js +43 -0
  91. package/dist/server/features/staleness/index.d.ts +2 -0
  92. package/dist/server/features/staleness/index.js +3 -0
  93. package/dist/server/features/staleness/model.d.ts +52 -0
  94. package/dist/server/features/staleness/model.js +17 -0
  95. package/dist/server/features/staleness/route.d.ts +7 -0
  96. package/dist/server/features/staleness/route.js +21 -0
  97. package/dist/server/features/staleness/service.d.ts +15 -0
  98. package/dist/server/features/staleness/service.js +78 -0
  99. package/dist/server/features/translate-document/handler.js +22 -8
  100. package/dist/server/modules/provenance/PayloadProvenanceStore.d.ts +2 -0
  101. package/dist/server/modules/provenance/PayloadProvenanceStore.js +37 -14
  102. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +3 -0
  103. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -0
  104. package/dist/server/modules/translation-levels/types.d.ts +3 -0
  105. package/dist/server/shared/http/index.d.ts +1 -0
  106. package/dist/server/shared/http/index.js +1 -0
  107. package/dist/server/shared/http/toClientErrorMessage.d.ts +15 -0
  108. package/dist/server/shared/http/toClientErrorMessage.js +26 -0
  109. package/dist/server/shared/index.d.ts +1 -1
  110. package/dist/server/shared/index.js +1 -1
  111. package/package.json +1 -1
  112. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.d.ts +0 -7
  113. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.js +0 -45
  114. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.d.ts +0 -1
  115. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.js +0 -3
  116. package/dist/client/entities/translation/ui/CompletedTranslationStatus/styles.module.scss +0 -3
  117. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.d.ts +0 -9
  118. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.js +0 -54
  119. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.d.ts +0 -1
  120. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.js +0 -3
  121. package/dist/client/entities/translation/ui/FailedTranslationStatus/styles.module.scss +0 -3
  122. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.d.ts +0 -11
  123. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.js +0 -83
  124. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.d.ts +0 -1
  125. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.js +0 -3
  126. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.d.ts +0 -10
  127. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.js +0 -66
  128. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.d.ts +0 -1
  129. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.js +0 -3
  130. package/dist/client/features/document-translation-progress-failed/index.d.ts +0 -1
  131. package/dist/client/features/document-translation-progress-failed/index.js +0 -3
  132. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.d.ts +0 -6
  133. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.js +0 -28
  134. package/dist/client/features/document-translation-progress-pending/index.d.ts +0 -1
  135. package/dist/client/features/document-translation-progress-pending/index.js +0 -3
  136. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.d.ts +0 -6
  137. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.js +0 -44
  138. package/dist/client/features/document-translation-progress-running/index.d.ts +0 -1
  139. package/dist/client/features/document-translation-progress-running/index.js +0 -3
  140. package/dist/client/features/document-translation-progress-running/ui/DocumentTranslationProgressRunning.d.ts +0 -6
  141. package/dist/client/features/document-translation-progress-running/ui/DocumentTranslationProgressRunning.js +0 -28
@@ -1,6 +1,8 @@
1
1
  import type { CollectionSlug, Endpoint } from "payload";
2
2
  import type { AccessGuard } from "../shared";
3
3
  import type { TaskRunnerFactory } from "../modules/task-runner";
4
+ import type { CollectionSchemaMap } from "../../types/CollectionSchemaMap";
5
+ import type { ProvenanceStoreFactory } from "../modules/provenance";
4
6
  export type TranslationRoutesDeps = {
5
7
  taskRunnerFactory: TaskRunnerFactory;
6
8
  /** Collections the plugin manages — gates which slugs the routes accept. */
@@ -9,15 +11,21 @@ export type TranslationRoutesDeps = {
9
11
  };
10
12
  access?: AccessGuard;
11
13
  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;
12
18
  };
13
19
  /**
14
- * The translator's HTTP surface as one shared bundle: the 6 job-API endpoints
20
+ * The translator's HTTP surface as one shared bundle: the job-API endpoints
15
21
  * (enqueue / run / cancel / cancel-by-collection / document-status /
16
- * collection-status), all wired to the same runner factory and access guard.
22
+ * collection-status) plus the provenance-backed staleness endpoints
23
+ * (document-staleness / dismiss), all wired to the same access guard.
17
24
  *
18
25
  * Extracted from the plugin's `init()` so registration is a single unit. The
19
26
  * document and collection levels each contribute this bundle via the level
20
27
  * context; the plugin deduplicates by method + path, so it registers exactly
21
- * once. Behaviour-preserving: same endpoints, same order, same wiring.
28
+ * once. The staleness endpoints always register; when provenance is disabled
29
+ * (`provenanceStoreFactory` absent) they simply report no staleness.
22
30
  */
23
- export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, }: TranslationRoutesDeps): Endpoint[];
31
+ export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, schemaMap, provenanceStoreFactory, }: TranslationRoutesDeps): Endpoint[];
@@ -4,23 +4,33 @@ import { createCancelRoute } from "./cancel";
4
4
  import { createCancelByCollectionRoute } from "./cancel-by-collection";
5
5
  import { createGetDocumentStatusRoute } from "./get-document-status";
6
6
  import { createGetCollectionStatusRoute } from "./get-collection-status";
7
+ import { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./staleness";
7
8
  /**
8
- * The translator's HTTP surface as one shared bundle: the 6 job-API endpoints
9
+ * The translator's HTTP surface as one shared bundle: the job-API endpoints
9
10
  * (enqueue / run / cancel / cancel-by-collection / document-status /
10
- * collection-status), all wired to the same runner factory and access guard.
11
+ * collection-status) plus the provenance-backed staleness endpoints
12
+ * (document-staleness / dismiss), all wired to the same access guard.
11
13
  *
12
14
  * Extracted from the plugin's `init()` so registration is a single unit. The
13
15
  * document and collection levels each contribute this bundle via the level
14
16
  * context; the plugin deduplicates by method + path, so it registers exactly
15
- * once. Behaviour-preserving: same endpoints, same order, same wiring.
16
- */ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath }) {
17
+ * once. The staleness endpoints always register; when provenance is disabled
18
+ * (`provenanceStoreFactory` absent) they simply report no staleness.
19
+ */ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, schemaMap, provenanceStoreFactory }) {
20
+ const stalenessConfig = {
21
+ availableCollections: collectionConfig.availableCollections,
22
+ schemaMap,
23
+ provenanceStoreFactory
24
+ };
17
25
  return [
18
26
  createEnqueueRoute(taskRunnerFactory, collectionConfig, access, basePath),
19
27
  createRunRoute(taskRunnerFactory, access, basePath),
20
28
  createCancelRoute(taskRunnerFactory, access, basePath),
21
29
  createCancelByCollectionRoute(collectionConfig, taskRunnerFactory, access, basePath),
22
30
  createGetDocumentStatusRoute(collectionConfig, taskRunnerFactory, access, basePath),
23
- createGetCollectionStatusRoute(collectionConfig, taskRunnerFactory, access, basePath)
31
+ createGetCollectionStatusRoute(collectionConfig, taskRunnerFactory, access, basePath),
32
+ createGetDocumentStalenessRoute(stalenessConfig, access, basePath),
33
+ createDismissStalenessRoute(stalenessConfig, access, basePath)
24
34
  ];
25
35
  }
26
36
 
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { JobIdSchema } from "../../shared";
2
+ import { JobIdSchema, toClientErrorMessage } from "../../shared";
3
3
  /**
4
4
  * Input validation schema.
5
5
  *
@@ -28,7 +28,12 @@ import { JobIdSchema } from "../../shared";
28
28
  target_lng: task.input.targetLng,
29
29
  strategy: task.input.strategy
30
30
  },
31
- error: task.error,
31
+ // Never ship the raw provider/runtime error to the browser outside development — it can leak
32
+ // implementation detail and secrets (e.g. a partial API key). The full error stays in the job
33
+ // record + server logs.
34
+ error: task.error ? {
35
+ message: toClientErrorMessage(task.error.message)
36
+ } : undefined,
32
37
  cancelled: task.cancelled
33
38
  };
34
39
  }
@@ -0,0 +1,8 @@
1
+ import type { PayloadRequest } from "payload";
2
+ import type { StalenessConfig } from "./model";
3
+ /** Dismisses (acknowledges) staleness of one target locale for a document. */
4
+ export declare class DismissStalenessHandler {
5
+ private readonly config;
6
+ constructor(config: StalenessConfig);
7
+ handle(req: PayloadRequest): Promise<Response>;
8
+ }
@@ -0,0 +1,27 @@
1
+ import { ServerResponse } from "../../shared";
2
+ import { isCollectionAvailable } from "../_lib/collection-utils";
3
+ import { DismissStalenessInputSchema } from "./model";
4
+ import { dismissLocaleStaleness } from "./service";
5
+ /** Dismisses (acknowledges) staleness of one target locale for a document. */ export class DismissStalenessHandler {
6
+ config;
7
+ constructor(config){
8
+ this.config = config;
9
+ }
10
+ async handle(req) {
11
+ const validationResult = DismissStalenessInputSchema.safeParse(await req.json?.());
12
+ if (validationResult.error) {
13
+ return ServerResponse.validationError(validationResult.error.issues);
14
+ }
15
+ const { collection_slug, collection_id, target_lng } = validationResult.data;
16
+ const collectionSlug = isCollectionAvailable(collection_slug, this.config.availableCollections);
17
+ if (!collectionSlug) {
18
+ return ServerResponse.badRequest("Collection not available for translation");
19
+ }
20
+ await dismissLocaleStaleness(req.payload, this.config, collectionSlug, collection_id, target_lng);
21
+ return ServerResponse.success({
22
+ success: true
23
+ });
24
+ }
25
+ }
26
+
27
+ //# sourceMappingURL=dismissStaleness.handler.js.map
@@ -0,0 +1,12 @@
1
+ import type { PayloadRequest } from "payload";
2
+ import type { StalenessConfig } from "./model";
3
+ /**
4
+ * Reads per-locale staleness for a single document. Best-effort: a recompute failure (e.g. the
5
+ * consumer enabled provenance but has not run the SQL migration, so the sidecar table is missing)
6
+ * degrades to an empty result and a log line — it must never 500 the document panel.
7
+ */
8
+ export declare class GetDocumentStalenessHandler {
9
+ private readonly config;
10
+ constructor(config: StalenessConfig);
11
+ handle(req: PayloadRequest): Promise<Response>;
12
+ }
@@ -0,0 +1,43 @@
1
+ import { ServerResponse } from "../../shared";
2
+ import { isCollectionAvailable } from "../_lib/collection-utils";
3
+ import { GetDocumentStalenessInputSchema } from "./model";
4
+ import { computeDocumentStaleness } from "./service";
5
+ /**
6
+ * Reads per-locale staleness for a single document. Best-effort: a recompute failure (e.g. the
7
+ * consumer enabled provenance but has not run the SQL migration, so the sidecar table is missing)
8
+ * degrades to an empty result and a log line — it must never 500 the document panel.
9
+ */ export class GetDocumentStalenessHandler {
10
+ config;
11
+ constructor(config){
12
+ this.config = config;
13
+ }
14
+ async handle(req) {
15
+ const validationResult = GetDocumentStalenessInputSchema.safeParse(req.routeParams);
16
+ if (validationResult.error) {
17
+ return ServerResponse.validationError(validationResult.error.issues);
18
+ }
19
+ const { collection_slug, collection_id } = validationResult.data;
20
+ const collectionSlug = isCollectionAvailable(collection_slug, this.config.availableCollections);
21
+ if (!collectionSlug) {
22
+ return ServerResponse.badRequest("Collection not available for translation");
23
+ }
24
+ try {
25
+ const locales = await computeDocumentStaleness(req.payload, this.config, collectionSlug, collection_id);
26
+ return ServerResponse.success({
27
+ locales
28
+ });
29
+ } catch (error) {
30
+ req.payload.logger.error({
31
+ err: error,
32
+ collection: collectionSlug,
33
+ documentId: collection_id,
34
+ msg: "translator: failed to compute translation staleness"
35
+ });
36
+ return ServerResponse.success({
37
+ locales: []
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ //# sourceMappingURL=getDocumentStaleness.handler.js.map
@@ -0,0 +1,2 @@
1
+ export { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./route";
2
+ export type { StalenessConfig, StalenessLocaleOutput } from "./model";
@@ -0,0 +1,3 @@
1
+ export { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./route";
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import type { CollectionSlug } from "payload";
3
+ import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
4
+ import type { ProvenanceStoreFactory } from "../../modules/provenance";
5
+ /**
6
+ * Route params for reading a document's per-locale staleness.
7
+ *
8
+ * `collection_id` accepts any shape Payload allows as a document id — see {@link JobIdSchema}.
9
+ */
10
+ export declare const GetDocumentStalenessInputSchema: z.ZodObject<{
11
+ collection_id: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
12
+ collection_slug: z.ZodString;
13
+ }, "strip", z.ZodTypeAny, {
14
+ collection_slug: string;
15
+ collection_id: string;
16
+ }, {
17
+ collection_slug: string;
18
+ collection_id: string | number;
19
+ }>;
20
+ /** Body for dismissing (acknowledging) staleness of one target locale. */
21
+ export declare const DismissStalenessInputSchema: z.ZodObject<{
22
+ collection_id: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
23
+ collection_slug: z.ZodString;
24
+ target_lng: z.ZodString;
25
+ }, "strip", z.ZodTypeAny, {
26
+ collection_slug: string;
27
+ collection_id: string;
28
+ target_lng: string;
29
+ }, {
30
+ collection_slug: string;
31
+ collection_id: string | number;
32
+ target_lng: string;
33
+ }>;
34
+ /**
35
+ * Per-locale staleness for one document (snake_case for client compatibility, matching the other
36
+ * translation endpoints). One entry per target locale that has a provenance record.
37
+ */
38
+ export type StalenessLocaleOutput = {
39
+ target_lng: string;
40
+ source_lng: string;
41
+ is_stale: boolean;
42
+ translated_at: string;
43
+ };
44
+ /**
45
+ * Handler configuration. `provenanceStoreFactory` is absent when provenance is disabled — the
46
+ * handlers then report no staleness (empty), so the endpoint contract stays stable either way.
47
+ */
48
+ export type StalenessConfig = {
49
+ availableCollections: Set<CollectionSlug>;
50
+ schemaMap: CollectionSchemaMap;
51
+ provenanceStoreFactory?: ProvenanceStoreFactory;
52
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { JobIdSchema } from "../../shared";
3
+ /**
4
+ * Route params for reading a document's per-locale staleness.
5
+ *
6
+ * `collection_id` accepts any shape Payload allows as a document id — see {@link JobIdSchema}.
7
+ */ export const GetDocumentStalenessInputSchema = z.object({
8
+ collection_id: JobIdSchema,
9
+ collection_slug: z.string().nonempty()
10
+ });
11
+ /** Body for dismissing (acknowledging) staleness of one target locale. */ export const DismissStalenessInputSchema = z.object({
12
+ collection_id: JobIdSchema,
13
+ collection_slug: z.string().nonempty(),
14
+ target_lng: z.string().nonempty()
15
+ });
16
+
17
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,7 @@
1
+ import type { Endpoint } from "payload";
2
+ import type { AccessGuard } from "../../shared";
3
+ import type { StalenessConfig } from "./model";
4
+ /** GET per-locale staleness for one document. */
5
+ export declare function createGetDocumentStalenessRoute(config: StalenessConfig, access?: AccessGuard, basePath?: string): Endpoint;
6
+ /** POST to dismiss (acknowledge) staleness of one target locale. */
7
+ export declare function createDismissStalenessRoute(config: StalenessConfig, access?: AccessGuard, basePath?: string): Endpoint;
@@ -0,0 +1,21 @@
1
+ import { withErrorHandler, withAccessCheck } from "../../shared";
2
+ import { GetDocumentStalenessHandler } from "./getDocumentStaleness.handler";
3
+ import { DismissStalenessHandler } from "./dismissStaleness.handler";
4
+ /** GET per-locale staleness for one document. */ export function createGetDocumentStalenessRoute(config, access, basePath = "/translate") {
5
+ const handler = new GetDocumentStalenessHandler(config);
6
+ return {
7
+ path: `${basePath}/stale/:collection_slug/:collection_id`,
8
+ method: "get",
9
+ handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
10
+ };
11
+ }
12
+ /** POST to dismiss (acknowledge) staleness of one target locale. */ export function createDismissStalenessRoute(config, access, basePath = "/translate") {
13
+ const handler = new DismissStalenessHandler(config);
14
+ return {
15
+ path: `${basePath}/stale/dismiss`,
16
+ method: "post",
17
+ handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
18
+ };
19
+ }
20
+
21
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,15 @@
1
+ import type { CollectionSlug, Payload } from "payload";
2
+ import type { StalenessConfig, StalenessLocaleOutput } from "./model";
3
+ /**
4
+ * Per-locale staleness for one document: read every provenance record, recompute the current source
5
+ * fingerprint, and mark each locale stale when the source drifted and the drift was not dismissed.
6
+ * Returns `[]` when provenance is disabled or the collection has no schema — a normal condition, not
7
+ * an error (genuine store/fetch failures propagate for the caller to log).
8
+ */
9
+ export declare function computeDocumentStaleness(payload: Payload, config: StalenessConfig, collection: CollectionSlug, documentId: string): Promise<StalenessLocaleOutput[]>;
10
+ /**
11
+ * Acknowledge the current source drift for one target locale: recompute the current source
12
+ * fingerprint (write-path-identical) and persist it as the dismissed fingerprint, so the indicator
13
+ * hides until the source changes again. No-op when provenance is disabled or the locale has no record.
14
+ */
15
+ export declare function dismissLocaleStaleness(payload: Payload, config: StalenessConfig, collection: CollectionSlug, documentId: string, targetLocale: string): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import { computeSourceFingerprint } from "../../../core/content-projection/computeSourceFingerprint";
2
+ import { isRecordStale } from "../../../core/provenance";
3
+ import { fetchSourceDocument } from "../_lib/sourceDocument";
4
+ /**
5
+ * Recompute the current source fingerprint the **same way the write path does**. Both sides fetch via
6
+ * the shared {@link fetchSourceDocument} (identical `depth`/locale shape) and hash with the shared
7
+ * {@link computeSourceFingerprint} + the same `schemaMap` entry, so an untouched document can never
8
+ * report stale. Cached per source locale so a document translated from one source into N locales
9
+ * fetches the source once.
10
+ */ function makeCurrentFingerprint(payload, collection, documentId, schema) {
11
+ const cache = new Map();
12
+ return async (sourceLocale)=>{
13
+ const cached = cache.get(sourceLocale);
14
+ if (cached !== undefined) return cached;
15
+ const sourceData = await fetchSourceDocument(payload, collection, documentId, sourceLocale);
16
+ const fingerprint = computeSourceFingerprint(sourceData, schema);
17
+ cache.set(sourceLocale, fingerprint);
18
+ return fingerprint;
19
+ };
20
+ }
21
+ /**
22
+ * Per-locale staleness for one document: read every provenance record, recompute the current source
23
+ * fingerprint, and mark each locale stale when the source drifted and the drift was not dismissed.
24
+ * Returns `[]` when provenance is disabled or the collection has no schema — a normal condition, not
25
+ * an error (genuine store/fetch failures propagate for the caller to log).
26
+ */ export async function computeDocumentStaleness(payload, config, collection, documentId) {
27
+ const schema = config.schemaMap.get(collection);
28
+ if (!config.provenanceStoreFactory || !schema) return [];
29
+ const store = config.provenanceStoreFactory(payload);
30
+ const records = await store.findByDocument(collection, documentId);
31
+ if (records.length === 0) return [];
32
+ const currentFingerprint = makeCurrentFingerprint(payload, collection, documentId, schema);
33
+ const locales = [];
34
+ for (const record of records){
35
+ try {
36
+ const current = await currentFingerprint(record.sourceLocale);
37
+ locales.push({
38
+ target_lng: record.targetLocale,
39
+ source_lng: record.sourceLocale,
40
+ is_stale: isRecordStale(record, current),
41
+ translated_at: record.translatedAt
42
+ });
43
+ } catch (error) {
44
+ // Isolate per-locale failures (e.g. the record's source locale was removed from Payload's
45
+ // localization config): skip just this locale so one bad record can't blank the staleness of
46
+ // every other, still-valid locale on the document.
47
+ payload.logger.error({
48
+ err: error,
49
+ collection,
50
+ documentId,
51
+ targetLocale: record.targetLocale,
52
+ sourceLocale: record.sourceLocale,
53
+ msg: "translator: failed to compute staleness for locale"
54
+ });
55
+ }
56
+ }
57
+ return locales;
58
+ }
59
+ /**
60
+ * Acknowledge the current source drift for one target locale: recompute the current source
61
+ * fingerprint (write-path-identical) and persist it as the dismissed fingerprint, so the indicator
62
+ * hides until the source changes again. No-op when provenance is disabled or the locale has no record.
63
+ */ export async function dismissLocaleStaleness(payload, config, collection, documentId, targetLocale) {
64
+ const schema = config.schemaMap.get(collection);
65
+ if (!config.provenanceStoreFactory || !schema) return;
66
+ const store = config.provenanceStoreFactory(payload);
67
+ const key = {
68
+ collectionSlug: collection,
69
+ documentId,
70
+ targetLocale
71
+ };
72
+ const record = await store.find(key);
73
+ if (!record) return;
74
+ const currentFingerprint = makeCurrentFingerprint(payload, collection, documentId, schema);
75
+ await store.dismiss(key, await currentFingerprint(record.sourceLocale));
76
+ }
77
+
78
+ //# sourceMappingURL=service.js.map
@@ -1,6 +1,7 @@
1
1
  import { APIError } from "payload";
2
2
  import { translateContent } from "../../../core/translation-pipeline";
3
3
  import { computeSourceFingerprint } from "../../../core/content-projection/computeSourceFingerprint";
4
+ import { fetchSourceDocument } from "../_lib/sourceDocument";
4
5
  /**
5
6
  * Translates a single document from source language to target language
6
7
  */ export class TranslateDocumentHandler {
@@ -17,12 +18,25 @@ import { computeSourceFingerprint } from "../../../core/content-projection/compu
17
18
  // Get original schema (preserves localized: true on nested fields)
18
19
  const schema = this.schemaMap.get(collection);
19
20
  if (!schema) throw new APIError(`Collection "${collection}" not found in schemaMap`, 400);
20
- const sourceData = await payload.findByID({
21
- collection,
22
- id: collectionId,
23
- locale: sourceLng,
24
- depth: 0
25
- });
21
+ const sourceData = await fetchSourceDocument(payload, collection, collectionId, sourceLng);
22
+ // Capture the staleness baseline from the PRISTINE source NOW, before the pipeline runs. The
23
+ // pipeline translates in place and shares object-valued source leaves (e.g. richText nodes) by
24
+ // reference with `sourceData`, so fingerprinting after `translateContent` would hash the target
25
+ // translation — making every fresh translation look immediately stale. Best-effort: a fingerprint
26
+ // failure logs and skips provenance rather than breaking the translation.
27
+ let sourceFingerprint = null;
28
+ if (this.provenanceStoreFactory) {
29
+ try {
30
+ sourceFingerprint = computeSourceFingerprint(sourceData, schema);
31
+ } catch (error) {
32
+ payload.logger.error({
33
+ err: error,
34
+ collection,
35
+ documentId: String(collectionId),
36
+ msg: "translator: failed to fingerprint source for provenance"
37
+ });
38
+ }
39
+ }
26
40
  const targetData = await payload.findByID({
27
41
  collection,
28
42
  id: collectionId,
@@ -44,7 +58,7 @@ import { computeSourceFingerprint } from "../../../core/content-projection/compu
44
58
  };
45
59
  const collectionConfig = payload.collections[collection].config;
46
60
  await this.saveTranslatedDocument(payload, collection, collectionId, translatedData, targetLng, sourceLng, collectionConfig, publishOnTranslation);
47
- if (this.provenanceStoreFactory) {
61
+ if (this.provenanceStoreFactory && sourceFingerprint !== null) {
48
62
  const store = this.provenanceStoreFactory(payload);
49
63
  try {
50
64
  await store.upsert({
@@ -52,7 +66,7 @@ import { computeSourceFingerprint } from "../../../core/content-projection/compu
52
66
  documentId: String(collectionId),
53
67
  targetLocale: targetLng,
54
68
  sourceLocale: sourceLng,
55
- sourceFingerprint: computeSourceFingerprint(sourceData, schema),
69
+ sourceFingerprint,
56
70
  translatedAt: new Date().toISOString(),
57
71
  dismissedFingerprint: null
58
72
  });
@@ -16,6 +16,8 @@ export declare class PayloadProvenanceStore implements ProvenanceStore {
16
16
  constructor(payload: Payload, slug: string);
17
17
  upsert(record: TranslationProvenanceRecord): Promise<void>;
18
18
  find(key: ProvenanceKey): Promise<TranslationProvenanceRecord | null>;
19
+ findByDocument(collectionSlug: string, documentId: string): Promise<TranslationProvenanceRecord[]>;
20
+ dismiss(key: ProvenanceKey, dismissedFingerprint: string): Promise<void>;
19
21
  deleteByDocument(collectionSlug: string, documentId: string): Promise<void>;
20
22
  private findDoc;
21
23
  }
@@ -1,3 +1,19 @@
1
+ function documentWhere(collectionSlug, documentId) {
2
+ return {
3
+ and: [
4
+ {
5
+ collectionSlug: {
6
+ equals: collectionSlug
7
+ }
8
+ },
9
+ {
10
+ documentId: {
11
+ equals: documentId
12
+ }
13
+ }
14
+ ]
15
+ };
16
+ }
1
17
  function keyWhere(key) {
2
18
  return {
3
19
  and: [
@@ -75,23 +91,30 @@ function toRecord(doc) {
75
91
  const doc = await this.findDoc(key);
76
92
  return doc === null ? null : toRecord(doc);
77
93
  }
94
+ async findByDocument(collectionSlug, documentId) {
95
+ const result = await this.payload.find({
96
+ collection: this.collection,
97
+ where: documentWhere(collectionSlug, documentId),
98
+ depth: 0,
99
+ pagination: false
100
+ });
101
+ return result.docs.map(toRecord);
102
+ }
103
+ async dismiss(key, dismissedFingerprint) {
104
+ const existing = await this.findDoc(key);
105
+ if (existing === null) return;
106
+ await this.payload.update({
107
+ collection: this.collection,
108
+ id: existing.id,
109
+ data: {
110
+ dismissedFingerprint
111
+ }
112
+ });
113
+ }
78
114
  async deleteByDocument(collectionSlug, documentId) {
79
115
  await this.payload.delete({
80
116
  collection: this.collection,
81
- where: {
82
- and: [
83
- {
84
- collectionSlug: {
85
- equals: collectionSlug
86
- }
87
- },
88
- {
89
- documentId: {
90
- equals: documentId
91
- }
92
- }
93
- ]
94
- }
117
+ where: documentWhere(collectionSlug, documentId)
95
118
  });
96
119
  }
97
120
  async findDoc(key) {
@@ -4,6 +4,7 @@ import type { RawPayloadComponentExport } from "../../../types/PayloadComponentE
4
4
  import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
5
5
  import type { TranslationProvider } from "../../../core/translation-providers";
6
6
  import type { TaskRunnerFactory } from "../task-runner";
7
+ import type { ProvenanceStoreFactory } from "../provenance";
7
8
  import type { CollectionAdminSlot, LevelContext } from "./types";
8
9
  type ConfigModifier = (config: Config) => Config;
9
10
  export type PluginConfigBuilderDeps = {
@@ -13,6 +14,7 @@ export type PluginConfigBuilderDeps = {
13
14
  taskRunnerFactory: TaskRunnerFactory;
14
15
  schemaMap: CollectionSchemaMap;
15
16
  translationProvider: TranslationProvider;
17
+ provenanceStoreFactory?: ProvenanceStoreFactory;
16
18
  };
17
19
  /**
18
20
  * The single place that mutates the Payload `config`. Levels (through the narrow
@@ -32,6 +34,7 @@ export declare class PluginConfigBuilder implements LevelContext {
32
34
  readonly taskRunnerFactory: TaskRunnerFactory;
33
35
  readonly schemaMap: CollectionSchemaMap;
34
36
  readonly translationProvider: TranslationProvider;
37
+ readonly provenanceStoreFactory?: ProvenanceStoreFactory;
35
38
  private readonly endpoints;
36
39
  private readonly collectionComponents;
37
40
  private readonly adminProviders;
@@ -34,6 +34,7 @@ function attachToSlot(collection, slot, component) {
34
34
  taskRunnerFactory;
35
35
  schemaMap;
36
36
  translationProvider;
37
+ provenanceStoreFactory;
37
38
  endpoints = [];
38
39
  collectionComponents = [];
39
40
  adminProviders = [];
@@ -45,6 +46,7 @@ function attachToSlot(collection, slot, component) {
45
46
  this.taskRunnerFactory = deps.taskRunnerFactory;
46
47
  this.schemaMap = deps.schemaMap;
47
48
  this.translationProvider = deps.translationProvider;
49
+ this.provenanceStoreFactory = deps.provenanceStoreFactory;
48
50
  }
49
51
  addEndpoints(endpoints) {
50
52
  this.endpoints.push(...endpoints);
@@ -4,6 +4,7 @@ import type { RawPayloadComponentExport } from "../../../types/PayloadComponentE
4
4
  import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
5
5
  import type { TranslationProvider } from "../../../core/translation-providers";
6
6
  import type { TaskRunnerFactory } from "../task-runner";
7
+ import type { ProvenanceStoreFactory } from "../provenance";
7
8
  export type CollectionAdminSlot = "beforeDocumentControls" | "beforeListTable";
8
9
  /**
9
10
  * A composable translation surface (document / collection / field).
@@ -40,6 +41,8 @@ export interface LevelContext {
40
41
  readonly schemaMap: CollectionSchemaMap;
41
42
  /** The configured translation backend (used by the synchronous field level). */
42
43
  readonly translationProvider: TranslationProvider;
44
+ /** Builds a provenance store; absent when provenance is disabled (staleness then reports empty). */
45
+ readonly provenanceStoreFactory?: ProvenanceStoreFactory;
43
46
  /** Register endpoints. Deduplicated by method + path when applied. */
44
47
  addEndpoints(endpoints: Endpoint[]): void;
45
48
  /** Attach an admin component to a slot on every managed collection. */
@@ -1,3 +1,4 @@
1
1
  export { ServerResponse } from "./ServerResponse";
2
2
  export { withErrorHandler } from "./withErrorHandler";
3
3
  export { withAccessCheck } from "./withAccessCheck";
4
+ export { toClientErrorMessage, GENERIC_TRANSLATION_ERROR } from "./toClientErrorMessage";
@@ -1,5 +1,6 @@
1
1
  export { ServerResponse } from "./ServerResponse";
2
2
  export { withErrorHandler } from "./withErrorHandler";
3
3
  export { withAccessCheck } from "./withAccessCheck";
4
+ export { toClientErrorMessage, GENERIC_TRANSLATION_ERROR } from "./toClientErrorMessage";
4
5
 
5
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ /** Shown to the browser instead of a raw provider/runtime error outside development. */
2
+ export declare const GENERIC_TRANSLATION_ERROR = "Translation failed. See the server logs for details.";
3
+ /**
4
+ * Collapse a server-side error message to something safe to send to the browser.
5
+ *
6
+ * Provider/runtime errors (e.g. `401 Incorrect API key provided: sk-proj-…`) leak implementation
7
+ * detail — and sometimes secrets — so the client must not see them in production. The full error
8
+ * still lives in the job record and server logs for debugging; only the client-facing copy is
9
+ * collapsed. Fail-safe: the raw message passes through solely in `development`/`test`; every other
10
+ * environment (including an unset `NODE_ENV`) returns {@link GENERIC_TRANSLATION_ERROR}.
11
+ *
12
+ * @param message - The raw server-side error message, if any.
13
+ * @returns The raw message in a debug environment, otherwise the generic text.
14
+ */
15
+ export declare function toClientErrorMessage(message?: string): string;