@focus-reactive/payload-plugin-translator 0.7.2 → 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 (139) 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/provenance/ProvenanceStore.interface.d.ts +18 -0
  75. package/dist/core/provenance/index.d.ts +1 -0
  76. package/dist/core/provenance/index.js +1 -1
  77. package/dist/core/provenance/staleness.d.ts +19 -0
  78. package/dist/core/provenance/staleness.js +21 -0
  79. package/dist/plugin.js +2 -1
  80. package/dist/server/features/_lib/sourceDocument.d.ts +9 -0
  81. package/dist/server/features/_lib/sourceDocument.js +16 -0
  82. package/dist/server/features/createTranslationRoutes.d.ts +12 -4
  83. package/dist/server/features/createTranslationRoutes.js +15 -5
  84. package/dist/server/features/get-document-status/model.js +7 -2
  85. package/dist/server/features/staleness/dismissStaleness.handler.d.ts +8 -0
  86. package/dist/server/features/staleness/dismissStaleness.handler.js +27 -0
  87. package/dist/server/features/staleness/getDocumentStaleness.handler.d.ts +12 -0
  88. package/dist/server/features/staleness/getDocumentStaleness.handler.js +43 -0
  89. package/dist/server/features/staleness/index.d.ts +2 -0
  90. package/dist/server/features/staleness/index.js +3 -0
  91. package/dist/server/features/staleness/model.d.ts +52 -0
  92. package/dist/server/features/staleness/model.js +17 -0
  93. package/dist/server/features/staleness/route.d.ts +7 -0
  94. package/dist/server/features/staleness/route.js +21 -0
  95. package/dist/server/features/staleness/service.d.ts +15 -0
  96. package/dist/server/features/staleness/service.js +78 -0
  97. package/dist/server/features/translate-document/handler.js +22 -8
  98. package/dist/server/modules/provenance/PayloadProvenanceStore.d.ts +2 -0
  99. package/dist/server/modules/provenance/PayloadProvenanceStore.js +37 -14
  100. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +3 -0
  101. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -0
  102. package/dist/server/modules/translation-levels/types.d.ts +3 -0
  103. package/dist/server/shared/http/index.d.ts +1 -0
  104. package/dist/server/shared/http/index.js +1 -0
  105. package/dist/server/shared/http/toClientErrorMessage.d.ts +15 -0
  106. package/dist/server/shared/http/toClientErrorMessage.js +26 -0
  107. package/dist/server/shared/index.d.ts +1 -1
  108. package/dist/server/shared/index.js +1 -1
  109. package/package.json +1 -1
  110. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.d.ts +0 -7
  111. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.js +0 -45
  112. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.d.ts +0 -1
  113. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.js +0 -3
  114. package/dist/client/entities/translation/ui/CompletedTranslationStatus/styles.module.scss +0 -3
  115. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.d.ts +0 -9
  116. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.js +0 -54
  117. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.d.ts +0 -1
  118. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.js +0 -3
  119. package/dist/client/entities/translation/ui/FailedTranslationStatus/styles.module.scss +0 -3
  120. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.d.ts +0 -11
  121. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.js +0 -83
  122. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.d.ts +0 -1
  123. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.js +0 -3
  124. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.d.ts +0 -10
  125. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.js +0 -66
  126. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.d.ts +0 -1
  127. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.js +0 -3
  128. package/dist/client/features/document-translation-progress-failed/index.d.ts +0 -1
  129. package/dist/client/features/document-translation-progress-failed/index.js +0 -3
  130. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.d.ts +0 -6
  131. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.js +0 -28
  132. package/dist/client/features/document-translation-progress-pending/index.d.ts +0 -1
  133. package/dist/client/features/document-translation-progress-pending/index.js +0 -3
  134. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.d.ts +0 -6
  135. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.js +0 -44
  136. package/dist/client/features/document-translation-progress-running/index.d.ts +0 -1
  137. package/dist/client/features/document-translation-progress-running/index.js +0 -3
  138. package/dist/client/features/document-translation-progress-running/ui/DocumentTranslationProgressRunning.d.ts +0 -6
  139. package/dist/client/features/document-translation-progress-running/ui/DocumentTranslationProgressRunning.js +0 -28
@@ -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;
@@ -0,0 +1,26 @@
1
+ /** Shown to the browser instead of a raw provider/runtime error outside development. */ export const GENERIC_TRANSLATION_ERROR = "Translation failed. See the server logs for details.";
2
+ // Only these environments get the raw message as a debug aid. Anything else — production, an unset
3
+ // or misconfigured NODE_ENV — is treated as "not debug", so the default is the safe, generic text.
4
+ const DEBUG_ENVS = new Set([
5
+ "development",
6
+ "test"
7
+ ]);
8
+ /**
9
+ * Collapse a server-side error message to something safe to send to the browser.
10
+ *
11
+ * Provider/runtime errors (e.g. `401 Incorrect API key provided: sk-proj-…`) leak implementation
12
+ * detail — and sometimes secrets — so the client must not see them in production. The full error
13
+ * still lives in the job record and server logs for debugging; only the client-facing copy is
14
+ * collapsed. Fail-safe: the raw message passes through solely in `development`/`test`; every other
15
+ * environment (including an unset `NODE_ENV`) returns {@link GENERIC_TRANSLATION_ERROR}.
16
+ *
17
+ * @param message - The raw server-side error message, if any.
18
+ * @returns The raw message in a debug environment, otherwise the generic text.
19
+ */ export function toClientErrorMessage(message) {
20
+ if (DEBUG_ENVS.has(process.env.NODE_ENV ?? "")) {
21
+ return message?.trim() || GENERIC_TRANSLATION_ERROR;
22
+ }
23
+ return GENERIC_TRANSLATION_ERROR;
24
+ }
25
+
26
+ //# sourceMappingURL=toClientErrorMessage.js.map
@@ -1,4 +1,4 @@
1
- export { ServerResponse, withErrorHandler, withAccessCheck } from "./http";
1
+ export { ServerResponse, withErrorHandler, withAccessCheck, toClientErrorMessage, GENERIC_TRANSLATION_ERROR, } from "./http";
2
2
  export { AnyAccessGuard } from "./access";
3
3
  export type { AccessGuard, AccessGuardRequest, Handler } from "./access";
4
4
  export { isEmpty, isObject, normalizePath, pipe, getByPath, setByPath, filterLocalizedFields, } from "./utils";
@@ -1,5 +1,5 @@
1
1
  // HTTP utilities
2
- export { ServerResponse, withErrorHandler, withAccessCheck } from "./http";
2
+ export { ServerResponse, withErrorHandler, withAccessCheck, toClientErrorMessage, GENERIC_TRANSLATION_ERROR } from "./http";
3
3
  // Access control
4
4
  export { AnyAccessGuard } from "./access";
5
5
  // General utilities
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- type CompletedTranslationStatusProps = {
2
- completed_at: string;
3
- sourceLocale: string;
4
- targetLocale: string;
5
- };
6
- export declare function CompletedTranslationStatus({ completed_at, sourceLocale, targetLocale, }: CompletedTranslationStatusProps): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,45 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { LanguageTranslateIcon } from "../../../../shared/lib/assets/icons/LanguageTranslateIcon";
3
- import Button from "../../../../shared/ui/Button";
4
- import StatusIndicator from "../../../../shared/ui/StatusIndicator";
5
- import Tooltip from "../../../../shared/ui/Tooltip";
6
- import { TranslationDirection } from "../TranslationDirection";
7
- import styles from "./styles.module.scss";
8
- export function CompletedTranslationStatus({ completed_at, sourceLocale, targetLocale }) {
9
- return /*#__PURE__*/ _jsxs(StatusIndicator, {
10
- className: styles.indicator,
11
- $color: "green",
12
- title: "Completed",
13
- children: [
14
- /*#__PURE__*/ _jsx(Tooltip, {
15
- sideOffset: 12,
16
- side: "bottom",
17
- content: /*#__PURE__*/ _jsxs("time", {
18
- dateTime: completed_at,
19
- children: [
20
- "Completed at:",
21
- " ",
22
- new Date(completed_at).toLocaleString(undefined, {
23
- year: "numeric",
24
- month: "short",
25
- day: "numeric",
26
- hour: "2-digit",
27
- minute: "2-digit"
28
- })
29
- ]
30
- }),
31
- children: /*#__PURE__*/ _jsx(Button, {
32
- type: "button",
33
- $variant: "unstyled",
34
- children: /*#__PURE__*/ _jsx(LanguageTranslateIcon, {})
35
- })
36
- }),
37
- /*#__PURE__*/ _jsx(TranslationDirection, {
38
- sourceLocale: sourceLocale,
39
- targetLocale: targetLocale
40
- })
41
- ]
42
- });
43
- }
44
-
45
- //# sourceMappingURL=CompletedTranslationStatus.js.map
@@ -1 +0,0 @@
1
- export { CompletedTranslationStatus } from "./CompletedTranslationStatus";
@@ -1,3 +0,0 @@
1
- export { CompletedTranslationStatus } from "./CompletedTranslationStatus";
2
-
3
- //# sourceMappingURL=index.js.map
@@ -1,3 +0,0 @@
1
- .indicator {
2
- font-size: 1.3rem;
3
- }
@@ -1,9 +0,0 @@
1
- type FailedTranslationStatusProps = {
2
- message: string;
3
- sourceLocale: string;
4
- targetLocale: string;
5
- isLoading?: boolean;
6
- onRetry: () => void;
7
- };
8
- export declare function FailedTranslationStatus({ onRetry, message, isLoading, sourceLocale, targetLocale, }: FailedTranslationStatusProps): import("react/jsx-runtime").JSX.Element;
9
- export {};