@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
@@ -31,56 +31,49 @@
31
31
  }
32
32
 
33
33
  &__bars {
34
- display: grid;
35
- gap: 7px;
34
+ display: flex;
36
35
  align-items: center;
37
36
 
38
- &_small {
39
- grid-template-columns: 1fr 1fr;
37
+ // Gap tightens with size so a small control's bars don't read as scattered.
38
+ &_sm {
39
+ gap: 4px;
40
40
  }
41
41
 
42
- &_medium {
43
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
42
+ &_md {
43
+ gap: 6px;
44
+ }
45
+
46
+ &_lg {
47
+ gap: 7px;
44
48
  }
45
49
  }
46
50
 
47
51
  &__bar {
48
52
  background-color: currentColor;
53
+ width: 2px;
49
54
 
50
- &_size-small {
51
- height: 10px;
52
- width: 2px;
55
+ &_size-sm {
56
+ height: 9px;
53
57
  }
54
58
 
55
- &_size-medium {
56
- height: 15px;
57
- width: 2px;
58
- }
59
-
60
- &:nth-child(1) {
61
- transform: translateY(0);
62
- animation: animate-bar--odd 1.25s infinite;
59
+ &_size-md {
60
+ height: 12px;
63
61
  }
64
62
 
65
- &:nth-child(2) {
66
- transform: translateY(-2px);
67
- animation: animate-bar--even 1.25s infinite;
63
+ &_size-lg {
64
+ height: 15px;
68
65
  }
69
66
 
70
- &:nth-child(3) {
67
+ // Neighbouring bars swing in opposition, regardless of how many there are.
68
+ &:nth-child(odd) {
71
69
  transform: translateY(0);
72
70
  animation: animate-bar--odd 1.25s infinite;
73
71
  }
74
72
 
75
- &:nth-child(4) {
73
+ &:nth-child(even) {
76
74
  transform: translateY(-2px);
77
75
  animation: animate-bar--even 1.25s infinite;
78
76
  }
79
-
80
- &:nth-child(5) {
81
- transform: translateY(0);
82
- animation: animate-bar--odd 1.25s infinite;
83
- }
84
77
  }
85
78
 
86
79
  &__text {
@@ -15,6 +15,9 @@ function Popup({ open, children, onOpenChange, $trigger, $align, $side, onOpenAu
15
15
  sideOffset: 8,
16
16
  side: $side,
17
17
  align: $align,
18
+ // Keep a gap from the viewport edges so the popup never sticks to the screen border —
19
+ // Radix shifts it inward to preserve this padding.
20
+ collisionPadding: 12,
18
21
  className: style.popover,
19
22
  onOpenAutoFocus: onOpenAutoFocus,
20
23
  children: children
@@ -5,7 +5,7 @@ import styles from "./styles.module.scss";
5
5
  const sizes = {
6
6
  sm: {
7
7
  height: "24px",
8
- paddingInline: "0.25rem",
8
+ paddingInline: "0.5rem",
9
9
  fontSize: "0.875rem"
10
10
  },
11
11
  md: {
@@ -78,6 +78,7 @@ export default function FormSelect({ name, required, options, label, description
78
78
  return /*#__PURE__*/ _jsxs(Label, {
79
79
  className: className,
80
80
  label: label,
81
+ $size: size,
81
82
  children: [
82
83
  selectElement,
83
84
  errorAndDescription
@@ -3,13 +3,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { toast, useLocale, useSelection } from "@payloadcms/ui";
4
4
  import { SelectAllStatus } from "@payloadcms/ui/providers/Selection";
5
5
  import { useEffect, useMemo } from "react";
6
- import { TranslationsApi } from "../../../entities/translation";
6
+ import { deriveCollectionPanelStatus, TranslationsApi } from "../../../entities/translation";
7
7
  import { CollectionTranslationForm, FORM_FIELDS } from "../../../features/collection-translation-form";
8
8
  import { CollectionTranslationFormModel } from "../../../features/collection-translation-form/index.client";
9
9
  import CollectionTranslationPopup from "../../../features/collection-translation-popup";
10
10
  import { CollectionTranslationProgress } from "../../../features/collection-translation-progress";
11
11
  import { handleFormError } from "../../../shared/lib/forms/handle-form-error";
12
12
  import { useCollectionDashboardUrlParams } from "../../../shared/lib/payload/hooks/useCollectionDashboardUrlParams";
13
+ import styles from "./styles.module.scss";
13
14
  export default function BulkTranslationDashboard({ hasDrafts }) {
14
15
  const locale = useLocale();
15
16
  const { collection } = useCollectionDashboardUrlParams();
@@ -38,7 +39,10 @@ export default function BulkTranslationDashboard({ hasDrafts }) {
38
39
  });
39
40
  const selectAll = documentsSelection.selectAll === SelectAllStatus.AllAvailable;
40
41
  const selectedCount = selectAll ? documentsSelection.totalDocs : documentsSelection.count;
41
- const translationInProgress = !!(collectionTranslationApi.data?.pending.length || collectionTranslationApi.data?.running.length);
42
+ const status = collectionTranslationApi.data;
43
+ const panelStatus = deriveCollectionPanelStatus(status);
44
+ const hasAnyCount = !!(status && (status.completed.length || status.failed.length || status.running.length || status.pending.length));
45
+ const showStatus = collectionTranslationApi.isPending || hasAnyCount;
42
46
  const handleSubmit = async (formData)=>{
43
47
  try {
44
48
  await queueTranslationApi.mutateAsync({
@@ -51,25 +55,41 @@ export default function BulkTranslationDashboard({ hasDrafts }) {
51
55
  }
52
56
  };
53
57
  return /*#__PURE__*/ _jsxs(CollectionTranslationPopup, {
54
- translationInProgress: translationInProgress,
58
+ status: panelStatus,
55
59
  selectedCount: selectedCount,
56
60
  children: [
57
- /*#__PURE__*/ _jsx("h3", {
58
- children: "Bulk Translation Dashboard"
61
+ /*#__PURE__*/ _jsx("h4", {
62
+ className: styles.title,
63
+ children: "Bulk translation"
59
64
  }),
60
- /*#__PURE__*/ _jsx("p", {
61
- children: "Translate multiple documents at once using AI. Select the records you want to translate on the collection dashboard, then configure your translation settings here."
65
+ /*#__PURE__*/ _jsxs("section", {
66
+ className: styles.section,
67
+ children: [
68
+ /*#__PURE__*/ _jsx("h5", {
69
+ className: styles.eyebrow,
70
+ children: "Translate"
71
+ }),
72
+ /*#__PURE__*/ _jsx(CollectionTranslationForm, {
73
+ form: form,
74
+ onSubmit: handleSubmit,
75
+ selectedCount: selectedCount,
76
+ hasDrafts: hasDrafts
77
+ })
78
+ ]
62
79
  }),
63
- /*#__PURE__*/ _jsx(CollectionTranslationForm, {
64
- form: form,
65
- onSubmit: handleSubmit,
66
- selectedCount: selectedCount,
67
- hasDrafts: hasDrafts
68
- }),
69
- /*#__PURE__*/ _jsx(CollectionTranslationProgress, {
70
- collection: collection,
71
- data: collectionTranslationApi.data,
72
- isPending: collectionTranslationApi.isPending
80
+ showStatus && /*#__PURE__*/ _jsxs("section", {
81
+ className: `${styles.section} ${styles["section--status"]}`,
82
+ children: [
83
+ /*#__PURE__*/ _jsx("h5", {
84
+ className: styles.eyebrow,
85
+ children: "Status"
86
+ }),
87
+ /*#__PURE__*/ _jsx(CollectionTranslationProgress, {
88
+ collection: collection,
89
+ data: collectionTranslationApi.data,
90
+ isPending: collectionTranslationApi.isPending
91
+ })
92
+ ]
73
93
  })
74
94
  ]
75
95
  });
@@ -0,0 +1,27 @@
1
+ .title {
2
+ margin: 0;
3
+ font-size: 0.8125rem;
4
+ font-weight: 600;
5
+ color: var(--theme-text, #222);
6
+ }
7
+
8
+ .section {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 0.5rem;
12
+ }
13
+
14
+ // One eyebrow style shared by every section header, so the sections read as one system.
15
+ .eyebrow {
16
+ margin: 0;
17
+ font-size: 0.6875rem;
18
+ font-weight: 600;
19
+ text-transform: uppercase;
20
+ letter-spacing: 0.04em;
21
+ color: var(--theme-elevation-500, #8c8c8c);
22
+ }
23
+
24
+ .section--status {
25
+ padding-top: 0.75rem;
26
+ border-top: 1px solid var(--theme-elevation-100, #e6e6e6);
27
+ }
@@ -2,20 +2,19 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { toast, useLocale } from "@payloadcms/ui";
4
4
  import { useEffect, useMemo } from "react";
5
- import { CompletedTranslationStatus, TranslationsApi } from "../../../entities/translation";
5
+ import { buildTranslationStatusRows, derivePanelStatus, TranslationsApi, TranslationStatusList } from "../../../entities/translation";
6
6
  import { OpenDocumentTranslationPopup } from "../../../features/open-document-translation-popup";
7
7
  import { DocumentTranslationForm, FORM_FIELDS } from "../../../features/translate-document-form";
8
8
  import { DocumentTranslationFormModel } from "../../../features/translate-document-form/index.client";
9
9
  import { handleFormError } from "../../../shared/lib/forms/handle-form-error";
10
10
  import { useCollectionDocumentUrlParams } from "../../../shared/lib/payload/hooks/useCollectionDocumentUrlParams";
11
- import { DocumentTranslationProgressFailed } from "../../../features/document-translation-progress-failed";
12
- import { DocumentTranslationProgressRunning } from "../../../features/document-translation-progress-running";
13
- import { DocumentTranslationProgressPending } from "../../../features/document-translation-progress-pending";
11
+ import styles from "./styles.module.scss";
14
12
  const TranslateDocument = ({ hasDrafts })=>{
15
13
  const locale = useLocale();
16
14
  const params = useCollectionDocumentUrlParams();
17
15
  const queueTranslationApi = TranslationsApi.useQueueDocumentTranslation();
18
16
  const { data, error, isLoading } = TranslationsApi.useDocumentTranslation(params);
17
+ const { data: staleness } = TranslationsApi.useDocumentStaleness(params);
19
18
  useEffect(()=>{
20
19
  if (error) {
21
20
  toast.error(error.message);
@@ -23,6 +22,24 @@ const TranslateDocument = ({ hasDrafts })=>{
23
22
  }, [
24
23
  error
25
24
  ]);
25
+ const staleLocales = useMemo(()=>(staleness?.locales ?? []).filter((l)=>l.is_stale).map((l)=>l.target_lng), [
26
+ staleness
27
+ ]);
28
+ // The panel trigger shows one aggregate marker; the popup shows the full per-locale list.
29
+ const panelStatus = useMemo(()=>derivePanelStatus({
30
+ runStatus: data?.status,
31
+ staleLocales
32
+ }), [
33
+ data?.status,
34
+ staleLocales
35
+ ]);
36
+ const statusRows = useMemo(()=>buildTranslationStatusRows({
37
+ staleness,
38
+ run: data
39
+ }), [
40
+ staleness,
41
+ data
42
+ ]);
26
43
  const initialValues = useMemo(()=>({
27
44
  [FORM_FIELDS.SOURCE_LNG]: locale.code,
28
45
  [FORM_FIELDS.HIDDEN_COLLECTION_SLUG]: params.collection,
@@ -48,38 +65,47 @@ const TranslateDocument = ({ hasDrafts })=>{
48
65
  handleFormError(e, form);
49
66
  }
50
67
  };
51
- return /*#__PURE__*/ _jsxs(_Fragment, {
52
- children: [
53
- /*#__PURE__*/ _jsx(OpenDocumentTranslationPopup, {
54
- isLoading: isLoading,
55
- children: ({ close })=>/*#__PURE__*/ _jsxs(_Fragment, {
68
+ // Vertical stack: title → Translate (form) → Status (unified per-locale list). No columns, no
69
+ // corner chip — one consistent system so the popup stays calm as the plugin grows.
70
+ return /*#__PURE__*/ _jsx(OpenDocumentTranslationPopup, {
71
+ isLoading: isLoading,
72
+ status: panelStatus,
73
+ children: ({ close })=>/*#__PURE__*/ _jsxs(_Fragment, {
74
+ children: [
75
+ /*#__PURE__*/ _jsx("h4", {
76
+ className: styles.title,
77
+ children: "Document translation"
78
+ }),
79
+ /*#__PURE__*/ _jsxs("section", {
80
+ className: styles.section,
56
81
  children: [
57
- /*#__PURE__*/ _jsx("h4", {
58
- children: "Document Translation"
82
+ /*#__PURE__*/ _jsx("h5", {
83
+ className: styles.eyebrow,
84
+ children: "Translate"
59
85
  }),
60
86
  /*#__PURE__*/ _jsx(DocumentTranslationForm, {
61
87
  form: form,
62
- onSubmit: (data)=>handleSubmit(data, close),
88
+ onSubmit: (formData)=>handleSubmit(formData, close),
63
89
  hasDrafts: hasDrafts
64
90
  })
65
91
  ]
92
+ }),
93
+ statusRows.length > 0 && /*#__PURE__*/ _jsxs("section", {
94
+ className: `${styles.section} ${styles["section--status"]}`,
95
+ children: [
96
+ /*#__PURE__*/ _jsx("h5", {
97
+ className: styles.eyebrow,
98
+ children: "Status"
99
+ }),
100
+ /*#__PURE__*/ _jsx(TranslationStatusList, {
101
+ rows: statusRows,
102
+ collection: params.collection,
103
+ id: params.id
104
+ })
105
+ ]
66
106
  })
67
- }),
68
- data?.status === "completed" && /*#__PURE__*/ _jsx(CompletedTranslationStatus, {
69
- sourceLocale: data.input.source_lng,
70
- targetLocale: data.input.target_lng,
71
- completed_at: data.completed_at
72
- }),
73
- data?.status === "failed" && /*#__PURE__*/ _jsx(DocumentTranslationProgressFailed, {
74
- data: data
75
- }),
76
- data?.status === "running" && /*#__PURE__*/ _jsx(DocumentTranslationProgressRunning, {
77
- data: data
78
- }),
79
- data?.status === "pending" && /*#__PURE__*/ _jsx(DocumentTranslationProgressPending, {
80
- data: data
107
+ ]
81
108
  })
82
- ]
83
109
  });
84
110
  };
85
111
  export default TranslateDocument;
@@ -0,0 +1,27 @@
1
+ .title {
2
+ margin: 0;
3
+ font-size: 0.8125rem;
4
+ font-weight: 600;
5
+ color: var(--theme-text, #222);
6
+ }
7
+
8
+ .section {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 0.5rem;
12
+ }
13
+
14
+ // One eyebrow style shared by every section header, so the sections read as one system.
15
+ .eyebrow {
16
+ margin: 0;
17
+ font-size: 0.6875rem;
18
+ font-weight: 600;
19
+ text-transform: uppercase;
20
+ letter-spacing: 0.04em;
21
+ color: var(--theme-elevation-500, #8c8c8c);
22
+ }
23
+
24
+ .section--status {
25
+ padding-top: 0.75rem;
26
+ border-top: 1px solid var(--theme-elevation-100, #e6e6e6);
27
+ }
@@ -1,8 +1,9 @@
1
1
  import type { LevelContext } from "../../server/modules/translation-levels";
2
2
  /**
3
- * Contribute the runner-agnostic document-translation API — the shared 6-route
3
+ * Contribute the runner-agnostic document-translation API — the shared route
4
4
  * bundle, bound to the level context's runner. Both `documentLevel` and
5
5
  * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
- * path, so the bundle registers exactly once.
6
+ * path, so the bundle registers exactly once. The schema map and provenance
7
+ * store factory ride along for the staleness endpoints.
7
8
  */
8
9
  export declare function useDocTranslationApi(ctx: LevelContext): void;
@@ -1,9 +1,10 @@
1
1
  import { createTranslationRoutes } from "../../server/features/createTranslationRoutes";
2
2
  /**
3
- * Contribute the runner-agnostic document-translation API — the shared 6-route
3
+ * Contribute the runner-agnostic document-translation API — the shared route
4
4
  * bundle, bound to the level context's runner. Both `documentLevel` and
5
5
  * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
- * path, so the bundle registers exactly once.
6
+ * path, so the bundle registers exactly once. The schema map and provenance
7
+ * store factory ride along for the staleness endpoints.
7
8
  */ export function useDocTranslationApi(ctx) {
8
9
  ctx.addEndpoints(createTranslationRoutes({
9
10
  taskRunnerFactory: ctx.taskRunnerFactory,
@@ -11,7 +12,9 @@ import { createTranslationRoutes } from "../../server/features/createTranslation
11
12
  availableCollections: new Set(ctx.collections.map((collection)=>collection.slug))
12
13
  },
13
14
  access: ctx.access,
14
- basePath: ctx.basePath
15
+ basePath: ctx.basePath,
16
+ schemaMap: ctx.schemaMap,
17
+ provenanceStoreFactory: ctx.provenanceStoreFactory
15
18
  }));
16
19
  }
17
20
 
@@ -88,6 +88,24 @@ export interface ProvenanceStore {
88
88
  * @returns The stored record, or `null` if none exists.
89
89
  */
90
90
  find(key: ProvenanceKey): Promise<TranslationProvenanceRecord | null>;
91
+ /**
92
+ * List every record for a document — one per translated target locale. Backs #50's per-locale
93
+ * staleness read for a single document panel.
94
+ *
95
+ * @param collectionSlug - Slug of the translated document's collection.
96
+ * @param documentId - Stringified id of the translated document.
97
+ * @returns All provenance receipts for the document (empty when none exist).
98
+ */
99
+ findByDocument(collectionSlug: string, documentId: string): Promise<TranslationProvenanceRecord[]>;
100
+ /**
101
+ * Acknowledge the current source drift for one locale without re-translating (#50's dismiss):
102
+ * persist `dismissedFingerprint` so the indicator hides until the source changes again. No-op if
103
+ * the key has no record.
104
+ *
105
+ * @param key - The `(collectionSlug, documentId, targetLocale)` identity to dismiss.
106
+ * @param dismissedFingerprint - The current source fingerprint being acknowledged.
107
+ */
108
+ dismiss(key: ProvenanceKey, dismissedFingerprint: string): Promise<void>;
91
109
  /**
92
110
  * Delete every record for a document (all target locales). Used to cascade-clean when the source
93
111
  * document is deleted.
@@ -1 +1,2 @@
1
1
  export type { ProvenanceKey, ProvenanceStore, TranslationProvenanceRecord, } from "./ProvenanceStore.interface";
2
+ export { isRecordStale } from "./staleness";
@@ -1,5 +1,5 @@
1
1
  // Provenance contracts (port + record types) only — payload-free. The Payload-backed store lives
2
2
  // in the plugin (src/server/modules/provenance), outside the core.
3
- export { };
3
+ export { isRecordStale } from "./staleness";
4
4
 
5
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ import type { TranslationProvenanceRecord } from "./ProvenanceStore.interface";
2
+ /**
3
+ * Whether a translated locale is out of date relative to its source — the #50 rule, in one place.
4
+ *
5
+ * A record is stale when the current source fingerprint differs from the one the translation was
6
+ * derived from (`record.sourceFingerprint`) **and** the editor has not acknowledged that exact drift
7
+ * (`record.dismissedFingerprint`). Dismissing sets `dismissedFingerprint` to the current fingerprint,
8
+ * so the indicator hides until the source changes again (a new fingerprint no longer matches the
9
+ * dismissed one). `dismissedFingerprint` is `null` until the first dismiss, in which case the check
10
+ * reduces to a plain source-drift comparison.
11
+ *
12
+ * Pure and payload-free so the staleness contract is testable without a database; the server handler
13
+ * supplies `currentFingerprint` via {@link computeSourceFingerprint} on the live source document.
14
+ *
15
+ * @param record - The stored provenance receipt for one `(collection, document, targetLocale)`.
16
+ * @param currentFingerprint - `computeSourceFingerprint` of the source document right now.
17
+ * @returns `true` when the target locale is out of date and not dismissed.
18
+ */
19
+ export declare function isRecordStale(record: TranslationProvenanceRecord, currentFingerprint: string): boolean;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Whether a translated locale is out of date relative to its source — the #50 rule, in one place.
3
+ *
4
+ * A record is stale when the current source fingerprint differs from the one the translation was
5
+ * derived from (`record.sourceFingerprint`) **and** the editor has not acknowledged that exact drift
6
+ * (`record.dismissedFingerprint`). Dismissing sets `dismissedFingerprint` to the current fingerprint,
7
+ * so the indicator hides until the source changes again (a new fingerprint no longer matches the
8
+ * dismissed one). `dismissedFingerprint` is `null` until the first dismiss, in which case the check
9
+ * reduces to a plain source-drift comparison.
10
+ *
11
+ * Pure and payload-free so the staleness contract is testable without a database; the server handler
12
+ * supplies `currentFingerprint` via {@link computeSourceFingerprint} on the live source document.
13
+ *
14
+ * @param record - The stored provenance receipt for one `(collection, document, targetLocale)`.
15
+ * @param currentFingerprint - `computeSourceFingerprint` of the source document right now.
16
+ * @returns `true` when the target locale is out of date and not dismissed.
17
+ */ export function isRecordStale(record, currentFingerprint) {
18
+ return currentFingerprint !== record.sourceFingerprint && currentFingerprint !== record.dismissedFingerprint;
19
+ }
20
+
21
+ //# sourceMappingURL=staleness.js.map
package/dist/plugin.js CHANGED
@@ -87,7 +87,8 @@ import { normalizePath } from "./server/shared";
87
87
  access,
88
88
  taskRunnerFactory,
89
89
  schemaMap,
90
- translationProvider
90
+ translationProvider,
91
+ provenanceStoreFactory
91
92
  });
92
93
  for (const level of activeLevels)level.extend(builder);
93
94
  builder.addConfigModifier(runnerConfigModifier);
@@ -0,0 +1,9 @@
1
+ import type { CollectionSlug, Payload } from "payload";
2
+ /**
3
+ * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
4
+ * (`depth: 0`, the given locale). Shared by the translation **write** path (which fingerprints the
5
+ * source it just translated) and the staleness **read** path (which re-fingerprints the live source),
6
+ * so the fingerprint baseline can never drift between the two — the single biggest correctness trap in
7
+ * staleness detection is thus enforced structurally, not by a comment.
8
+ */
9
+ export declare function fetchSourceDocument(payload: Payload, collection: CollectionSlug, id: string, locale: string): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
3
+ * (`depth: 0`, the given locale). Shared by the translation **write** path (which fingerprints the
4
+ * source it just translated) and the staleness **read** path (which re-fingerprints the live source),
5
+ * so the fingerprint baseline can never drift between the two — the single biggest correctness trap in
6
+ * staleness detection is thus enforced structurally, not by a comment.
7
+ */ export function fetchSourceDocument(payload, collection, id, locale) {
8
+ return payload.findByID({
9
+ collection,
10
+ id,
11
+ locale,
12
+ depth: 0
13
+ });
14
+ }
15
+
16
+ //# sourceMappingURL=sourceDocument.js.map
@@ -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
+ }