@focus-reactive/payload-plugin-translator 0.7.2 → 0.8.1

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 (157) 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 +5 -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 +22 -74
  80. package/dist/server/features/createTranslationRoutes.d.ts +8 -9
  81. package/dist/server/features/createTranslationRoutes.js +14 -5
  82. package/dist/server/features/get-document-status/model.js +7 -2
  83. package/dist/server/features/staleness/dismissStaleness.handler.d.ts +8 -0
  84. package/dist/server/features/staleness/dismissStaleness.handler.js +31 -0
  85. package/dist/server/features/staleness/getDocumentStaleness.handler.d.ts +12 -0
  86. package/dist/server/features/staleness/getDocumentStaleness.handler.js +43 -0
  87. package/dist/server/features/staleness/index.d.ts +2 -0
  88. package/dist/server/features/staleness/index.js +3 -0
  89. package/dist/server/features/staleness/model.d.ts +40 -0
  90. package/dist/server/features/staleness/model.js +17 -0
  91. package/dist/server/features/staleness/route.d.ts +7 -0
  92. package/dist/server/features/staleness/route.js +21 -0
  93. package/dist/server/features/translate-document/handler.d.ts +6 -8
  94. package/dist/server/features/translate-document/handler.js +21 -33
  95. package/dist/server/features/translate-document/index.d.ts +1 -0
  96. package/dist/server/features/translate-document/index.js +1 -0
  97. package/dist/server/features/translate-document/wireTranslateRunner.d.ts +28 -0
  98. package/dist/server/features/translate-document/wireTranslateRunner.js +49 -0
  99. package/dist/server/modules/provenance/{provenanceCollection.d.ts → Provenance.collection.d.ts} +17 -9
  100. package/dist/server/modules/provenance/{provenanceCollection.js → Provenance.collection.js} +23 -9
  101. package/dist/server/modules/provenance/Provenance.service.d.ts +56 -0
  102. package/dist/server/modules/provenance/Provenance.service.js +124 -0
  103. package/dist/server/modules/provenance/Provenance.shapes.d.ts +26 -0
  104. package/dist/server/modules/provenance/Provenance.shapes.js +7 -0
  105. package/dist/server/modules/provenance/{PayloadProvenanceStore.d.ts → Provenance.store.d.ts} +2 -0
  106. package/dist/server/modules/provenance/{PayloadProvenanceStore.js → Provenance.store.js} +38 -15
  107. package/dist/server/modules/provenance/Provenance.wiring.d.ts +23 -0
  108. package/dist/server/modules/provenance/Provenance.wiring.js +43 -0
  109. package/dist/server/modules/provenance/{provenanceCleanupHook.d.ts → ProvenanceCleanup.hook.d.ts} +4 -3
  110. package/dist/server/modules/provenance/{provenanceCleanupHook.js → ProvenanceCleanup.hook.js} +1 -1
  111. package/dist/server/modules/provenance/index.d.ts +8 -4
  112. package/dist/server/modules/provenance/index.js +6 -4
  113. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -11
  114. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -0
  115. package/dist/server/modules/translation-levels/index.d.ts +1 -1
  116. package/dist/server/modules/translation-levels/types.d.ts +20 -9
  117. package/dist/server/shared/http/index.d.ts +1 -0
  118. package/dist/server/shared/http/index.js +1 -0
  119. package/dist/server/shared/http/toClientErrorMessage.d.ts +15 -0
  120. package/dist/server/shared/http/toClientErrorMessage.js +26 -0
  121. package/dist/server/shared/index.d.ts +1 -1
  122. package/dist/server/shared/index.js +1 -1
  123. package/dist/server/shared/payload/sourceDocument.d.ts +8 -0
  124. package/dist/server/shared/payload/sourceDocument.js +15 -0
  125. package/dist/types/ConfigModifier.d.ts +10 -0
  126. package/dist/types/ConfigModifier.js +10 -0
  127. package/package.json +1 -1
  128. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.d.ts +0 -7
  129. package/dist/client/entities/translation/ui/CompletedTranslationStatus/CompletedTranslationStatus.js +0 -45
  130. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.d.ts +0 -1
  131. package/dist/client/entities/translation/ui/CompletedTranslationStatus/index.js +0 -3
  132. package/dist/client/entities/translation/ui/CompletedTranslationStatus/styles.module.scss +0 -3
  133. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.d.ts +0 -9
  134. package/dist/client/entities/translation/ui/FailedTranslationStatus/FailedTranslationStatus.js +0 -54
  135. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.d.ts +0 -1
  136. package/dist/client/entities/translation/ui/FailedTranslationStatus/index.js +0 -3
  137. package/dist/client/entities/translation/ui/FailedTranslationStatus/styles.module.scss +0 -3
  138. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.d.ts +0 -11
  139. package/dist/client/entities/translation/ui/PendingTranslationStatus/PendingTranslationStatus.js +0 -83
  140. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.d.ts +0 -1
  141. package/dist/client/entities/translation/ui/PendingTranslationStatus/index.js +0 -3
  142. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.d.ts +0 -10
  143. package/dist/client/entities/translation/ui/RunningTranslationStatus/RunningTranslationStatus.js +0 -66
  144. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.d.ts +0 -1
  145. package/dist/client/entities/translation/ui/RunningTranslationStatus/index.js +0 -3
  146. package/dist/client/features/document-translation-progress-failed/index.d.ts +0 -1
  147. package/dist/client/features/document-translation-progress-failed/index.js +0 -3
  148. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.d.ts +0 -6
  149. package/dist/client/features/document-translation-progress-failed/ui/DocumentTranslationProgressFailed.js +0 -28
  150. package/dist/client/features/document-translation-progress-pending/index.d.ts +0 -1
  151. package/dist/client/features/document-translation-progress-pending/index.js +0 -3
  152. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.d.ts +0 -6
  153. package/dist/client/features/document-translation-progress-pending/ui/DocumentTranslationProgressPending.js +0 -44
  154. package/dist/client/features/document-translation-progress-running/index.d.ts +0 -1
  155. package/dist/client/features/document-translation-progress-running/index.js +0 -3
  156. package/dist/client/features/document-translation-progress-running/ui/DocumentTranslationProgressRunning.d.ts +0 -6
  157. 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 provenance service factory
7
+ * rides along for the staleness endpoints (its schema lives inside the service).
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 provenance service factory
7
+ * rides along for the staleness endpoints (its schema lives inside the service).
7
8
  */ export function useDocTranslationApi(ctx) {
8
9
  ctx.addEndpoints(createTranslationRoutes({
9
10
  taskRunnerFactory: ctx.taskRunnerFactory,
@@ -11,7 +12,8 @@ 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
+ provenanceServiceFactory: ctx.provenanceServiceFactory
15
17
  }));
16
18
  }
17
19
 
@@ -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
@@ -1,19 +1,9 @@
1
1
  import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
2
- import { DEFAULT_PROVENANCE_SLUG, PayloadProvenanceStore, assertProvenanceSlugFree, injectProvenanceCleanup, isProvenanceCollection, makeProvenanceCollection } from "./server/modules/provenance";
3
- import { TranslateDocumentHandler } from "./server/features/translate-document";
4
- import { LifecycleNotifier, taskFromHandlerInput, withQueuedNotification } from "./server/modules/lifecycle";
2
+ import { configureProvenance } from "./server/modules/provenance";
3
+ import { wireTranslateRunner } from "./server/features/translate-document";
5
4
  import { documentLevel, collectionLevel } from "./composition/levels";
6
5
  import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
7
6
  import { normalizePath } from "./server/shared";
8
- /**
9
- * Resolve the opt-in `provenance` config to a sidecar slug, or `null` when disabled.
10
- * `false`/omitted → off; `true` or `{}` → on with the default slug; `{ slug }` → on with that slug.
11
- */ function resolveProvenanceSlug(provenance) {
12
- if (!provenance) return null;
13
- if (provenance === true) return DEFAULT_PROVENANCE_SLUG;
14
- // `||` (not `??`) so an empty/blank slug falls back to the default instead of silently disabling.
15
- return provenance.slug || DEFAULT_PROVENANCE_SLUG;
16
- }
17
7
  /** @deprecated Use `translatorPlugin` function instead */ export class TranslateCollectionPlugin {
18
8
  pluginConfig;
19
9
  constructor(pluginConfig){
@@ -22,61 +12,30 @@ import { normalizePath } from "./server/shared";
22
12
  init() {
23
13
  return async (config)=>{
24
14
  const { access, translationProvider, runner, collections, levels, provenance, lifecycle, basePath: rawBasePath = "/translate" } = this.pluginConfig;
25
- const lifecycleCallbacks = lifecycle ?? {};
26
- // Build schema map from deep-cloned collections
27
- // Deep clone is required because Payload mutates the original collection objects,
28
- // removing `localized: true` from nested fields during sanitization.
29
- // We use JSON round-trip instead of structuredClone because Lexical editor
30
- // configs contain async functions that structuredClone cannot handle.
31
- // TODO: Consider introducing a FieldLike interface with only the properties
32
- // used by the pipeline (name, type, localized, fields, blocks, tabs, custom)
33
- // to make the contract explicit and avoid reliance on JSON round-trip.
15
+ // Build schema map from deep-cloned collections.
16
+ // Deep clone is required because Payload mutates the original collection objects, removing
17
+ // `localized: true` from nested fields during sanitization. JSON round-trip (not
18
+ // structuredClone) because Lexical editor configs contain async functions structuredClone
19
+ // cannot handle.
20
+ // TODO: Consider introducing a FieldLike interface with only the properties used by the
21
+ // pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
22
+ // and avoid reliance on JSON round-trip.
34
23
  const schemaMap = new Map(collections.map((col)=>[
35
24
  col.slug,
36
25
  JSON.parse(JSON.stringify(col.fields))
37
26
  ]));
38
27
  const collectionSlugs = new Set(schemaMap.keys());
39
28
  const basePath = normalizePath(rawBasePath);
40
- const provenanceSlug = resolveProvenanceSlug(provenance);
41
- if (provenanceSlug) {
42
- const existing = (config.collections ?? []).filter((collection)=>!isProvenanceCollection(collection));
43
- assertProvenanceSlugFree(provenanceSlug, existing);
44
- }
45
- const provenanceStoreFactory = provenanceSlug ? (p)=>new PayloadProvenanceStore(p, provenanceSlug) : undefined;
46
- const translateHandler = new TranslateDocumentHandler(translationProvider, schemaMap, provenanceStoreFactory);
47
- const runnerContext = {
48
- handler: async (payload, input)=>{
49
- const notifier = new LifecycleNotifier(lifecycleCallbacks, payload.logger);
50
- const task = taskFromHandlerInput(input);
51
- try {
52
- await translateHandler.handle(payload, {
53
- collection: input.collection,
54
- collectionId: input.collectionId,
55
- sourceLng: input.sourceLng,
56
- targetLng: input.targetLng,
57
- strategy: input.strategy,
58
- publishOnTranslation: input.publishOnTranslation
59
- });
60
- } catch (error) {
61
- await notifier.failed(task, error);
62
- throw error; // rethrow so the runner marks the job failed
63
- }
64
- await notifier.completed(task);
65
- },
29
+ // Each concern owns its own config-time wiring and exposes it uniformly; init() just composes.
30
+ const provenanceModule = configureProvenance(provenance, schemaMap);
31
+ const { taskRunnerFactory, configModifier: runnerConfigModifier } = wireTranslateRunner({
32
+ translationProvider,
33
+ schemaMap,
34
+ provenanceServiceFactory: provenanceModule.serviceFactory,
35
+ runner,
36
+ lifecycle: lifecycle ?? {},
66
37
  collections: Array.from(collectionSlugs)
67
- };
68
- const runnerConfigModifier = runner.configure(runnerContext);
69
- // Bind the context once so routes receive a self-sufficient factory: the
70
- // runner needs no mutable per-instance handler state and create() has no
71
- // "configure() must run first" ordering coupling (translator plan, 0c).
72
- // When an `onQueued` callback is set, decorate the runner so `enqueue` fires it.
73
- const taskRunnerFactory = {
74
- create: (payload)=>{
75
- const taskRunner = runner.create(payload, runnerContext.handler);
76
- if (!lifecycleCallbacks.onQueued) return taskRunner;
77
- return withQueuedNotification(taskRunner, new LifecycleNotifier(lifecycleCallbacks, payload.logger));
78
- }
79
- };
38
+ });
80
39
  const activeLevels = levels ?? [
81
40
  documentLevel(),
82
41
  collectionLevel()
@@ -87,23 +46,12 @@ import { normalizePath } from "./server/shared";
87
46
  access,
88
47
  taskRunnerFactory,
89
48
  schemaMap,
90
- translationProvider
49
+ translationProvider,
50
+ provenanceServiceFactory: provenanceModule.serviceFactory
91
51
  });
92
52
  for (const level of activeLevels)level.extend(builder);
93
53
  builder.addConfigModifier(runnerConfigModifier);
94
- if (provenanceSlug && provenanceStoreFactory) {
95
- builder.addConfigModifier((cfg)=>{
96
- const alreadyAdded = cfg.collections?.some((collection)=>collection.slug === provenanceSlug && isProvenanceCollection(collection));
97
- if (!alreadyAdded) {
98
- cfg.collections = [
99
- ...cfg.collections ?? [],
100
- makeProvenanceCollection(provenanceSlug)
101
- ];
102
- }
103
- injectProvenanceCleanup(cfg, collectionSlugs, provenanceStoreFactory, provenanceSlug);
104
- return cfg;
105
- });
106
- }
54
+ builder.addConfigModifier(provenanceModule.configure(collectionSlugs));
107
55
  builder.addAdminProvider(new CacheProviderExport(basePath));
108
56
  // The single place the Payload config is mutated.
109
57
  return builder.applyTo(config);