@purposeinplay/payload-ai-translate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { readResponseError } from '../shared/fetch-error-body.js';
|
|
5
|
+
import { formatCost } from '../shared/format.js';
|
|
6
|
+
import { BulkTranslatePostEnqueueTransition } from './BulkTranslatePostEnqueueTransition.js';
|
|
7
|
+
import { useFocusTrap } from './useFocusTrap.js';
|
|
8
|
+
const OVERLAY_STYLE = {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
inset: 0,
|
|
11
|
+
background: 'rgba(0,0,0,0.45)',
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
padding: '2rem 1rem',
|
|
16
|
+
zIndex: 200
|
|
17
|
+
};
|
|
18
|
+
const MODAL_STYLE = {
|
|
19
|
+
width: '100%',
|
|
20
|
+
maxWidth: '480px',
|
|
21
|
+
maxHeight: 'calc(100vh - 4rem)',
|
|
22
|
+
overflowY: 'auto',
|
|
23
|
+
background: 'var(--theme-elevation-0)',
|
|
24
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
25
|
+
borderRadius: '6px',
|
|
26
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
|
|
27
|
+
padding: '1.5rem',
|
|
28
|
+
color: 'var(--theme-elevation-1000)'
|
|
29
|
+
};
|
|
30
|
+
const SECTION_LABEL = {
|
|
31
|
+
fontSize: '0.6875rem',
|
|
32
|
+
fontWeight: 600,
|
|
33
|
+
textTransform: 'uppercase',
|
|
34
|
+
letterSpacing: '0.05em',
|
|
35
|
+
color: 'var(--theme-elevation-500)',
|
|
36
|
+
margin: '0 0 0.35rem'
|
|
37
|
+
};
|
|
38
|
+
const SKELETON_STYLE = {
|
|
39
|
+
height: '1.25rem',
|
|
40
|
+
width: '6rem',
|
|
41
|
+
background: 'var(--theme-elevation-100)',
|
|
42
|
+
borderRadius: '4px',
|
|
43
|
+
opacity: 0.7
|
|
44
|
+
};
|
|
45
|
+
const COST_TEXT_STYLE = {
|
|
46
|
+
fontSize: '1.25rem',
|
|
47
|
+
fontWeight: 600,
|
|
48
|
+
color: 'var(--theme-elevation-1000)'
|
|
49
|
+
};
|
|
50
|
+
const INPUT_STYLE = {
|
|
51
|
+
width: '100%',
|
|
52
|
+
padding: '0.4rem 0.6rem',
|
|
53
|
+
background: 'var(--theme-elevation-0)',
|
|
54
|
+
border: '1px solid var(--theme-elevation-300)',
|
|
55
|
+
borderRadius: '4px',
|
|
56
|
+
color: 'var(--theme-elevation-1000)',
|
|
57
|
+
fontSize: '0.875rem',
|
|
58
|
+
fontFamily: 'inherit'
|
|
59
|
+
};
|
|
60
|
+
const ERROR_TEXT = {
|
|
61
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
62
|
+
fontSize: '0.8125rem',
|
|
63
|
+
marginTop: '0.4rem'
|
|
64
|
+
};
|
|
65
|
+
const TIMEOUT_MS = 10_000;
|
|
66
|
+
/** Pure helper, exported for tests. */ export function deriveCostState(preflight, timedOut) {
|
|
67
|
+
if (!preflight) {
|
|
68
|
+
return timedOut ? {
|
|
69
|
+
kind: 'timeout'
|
|
70
|
+
} : {
|
|
71
|
+
kind: 'loading'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (typeof preflight.estimatedCostUsd === 'number') {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'ready',
|
|
77
|
+
value: preflight.estimatedCostUsd
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
kind: 'unavailable'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Pure helper, exported for tests. */ export function isCapBlocked(preflight) {
|
|
85
|
+
if (!preflight) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const estimate = typeof preflight.estimatedCostUsd === 'number' ? preflight.estimatedCostUsd : 0;
|
|
89
|
+
return preflight.dailySpend.spentUsd + estimate > preflight.dailySpend.capUsd;
|
|
90
|
+
}
|
|
91
|
+
export const BulkTranslatePreflightModal = ({ basePath, onClose })=>{
|
|
92
|
+
const dialogRef = useRef(null);
|
|
93
|
+
const [preflight, setPreflight] = useState(null);
|
|
94
|
+
const [loadError, setLoadError] = useState(null);
|
|
95
|
+
const [timedOut, setTimedOut] = useState(false);
|
|
96
|
+
const [retryNonce, setRetryNonce] = useState(0);
|
|
97
|
+
const [force, setForce] = useState(false);
|
|
98
|
+
const [totp, setTotp] = useState('');
|
|
99
|
+
const [submitError, setSubmitError] = useState(null);
|
|
100
|
+
const [submitting, setSubmitting] = useState(false);
|
|
101
|
+
const [postEnqueueVisible, setPostEnqueueVisible] = useState(false);
|
|
102
|
+
// Locale selection — initialized once when preflight arrives. Empty
|
|
103
|
+
// until the preflight resolves; defaults to "all selected" on first
|
|
104
|
+
// load. The operator can toggle individual locales off to narrow the
|
|
105
|
+
// run; the displayed cost scales linearly with selected count.
|
|
106
|
+
const [selectedLocales, setSelectedLocales] = useState([]);
|
|
107
|
+
// v1.2.7: scope selection — same pattern as locales. Editor can
|
|
108
|
+
// narrow the run to specific collections / globals. Defaults to "all
|
|
109
|
+
// selected" on first load. The enqueue endpoint already supports
|
|
110
|
+
// narrowed scope via `scope.collections` / `scope.globals` (see
|
|
111
|
+
// endpoints/translation-hub/enqueue.ts:459-463) — when the array is
|
|
112
|
+
// non-empty it overrides the plugin-config defaults.
|
|
113
|
+
const [selectedCollections, setSelectedCollections] = useState([]);
|
|
114
|
+
const [selectedGlobals, setSelectedGlobals] = useState([]);
|
|
115
|
+
useEffect(()=>{
|
|
116
|
+
if (preflight && selectedLocales.length === 0) {
|
|
117
|
+
setSelectedLocales(preflight.scope.locales);
|
|
118
|
+
}
|
|
119
|
+
if (preflight && selectedCollections.length === 0 && selectedGlobals.length === 0) {
|
|
120
|
+
setSelectedCollections(preflight.scope.collections);
|
|
121
|
+
setSelectedGlobals(preflight.scope.globals);
|
|
122
|
+
}
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, [
|
|
125
|
+
preflight
|
|
126
|
+
]);
|
|
127
|
+
const toggleLocale = (code)=>{
|
|
128
|
+
setSelectedLocales((prev)=>prev.includes(code) ? prev.filter((c)=>c !== code) : [
|
|
129
|
+
...prev,
|
|
130
|
+
code
|
|
131
|
+
]);
|
|
132
|
+
};
|
|
133
|
+
const toggleCollection = (slug)=>{
|
|
134
|
+
setSelectedCollections((prev)=>prev.includes(slug) ? prev.filter((s)=>s !== slug) : [
|
|
135
|
+
...prev,
|
|
136
|
+
slug
|
|
137
|
+
]);
|
|
138
|
+
};
|
|
139
|
+
const toggleGlobal = (slug)=>{
|
|
140
|
+
setSelectedGlobals((prev)=>prev.includes(slug) ? prev.filter((s)=>s !== slug) : [
|
|
141
|
+
...prev,
|
|
142
|
+
slug
|
|
143
|
+
]);
|
|
144
|
+
};
|
|
145
|
+
useFocusTrap(dialogRef, {
|
|
146
|
+
enabled: !postEnqueueVisible,
|
|
147
|
+
onEscape: onClose
|
|
148
|
+
});
|
|
149
|
+
useEffect(()=>{
|
|
150
|
+
let cancelled = false;
|
|
151
|
+
setPreflight(null);
|
|
152
|
+
setLoadError(null);
|
|
153
|
+
setTimedOut(false);
|
|
154
|
+
const timer = setTimeout(()=>{
|
|
155
|
+
if (!cancelled) {
|
|
156
|
+
setTimedOut(true);
|
|
157
|
+
}
|
|
158
|
+
}, TIMEOUT_MS);
|
|
159
|
+
(async ()=>{
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${basePath}/api/translation-hub/bulk-translate/preflight`, {
|
|
162
|
+
credentials: 'include'
|
|
163
|
+
});
|
|
164
|
+
if (cancelled) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw new Error(await readResponseError(res));
|
|
169
|
+
}
|
|
170
|
+
const json = await res.json();
|
|
171
|
+
setPreflight(json);
|
|
172
|
+
setTimedOut(false);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (!cancelled) {
|
|
175
|
+
setLoadError(e instanceof Error ? e.message : String(e));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
return ()=>{
|
|
180
|
+
cancelled = true;
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
};
|
|
183
|
+
}, [
|
|
184
|
+
basePath,
|
|
185
|
+
retryNonce
|
|
186
|
+
]);
|
|
187
|
+
// Scale the displayed cost proportionally to the locale selection.
|
|
188
|
+
// The preflight returns cost for the FULL locale set; selecting a
|
|
189
|
+
// subset multiplies by (selected / total). Approximation is exact
|
|
190
|
+
// for evenly-sized locales (which is the common case — same source
|
|
191
|
+
// doc translated N times).
|
|
192
|
+
const scaledPreflight = (()=>{
|
|
193
|
+
if (!preflight) return null;
|
|
194
|
+
const totalLocales = preflight.scope.locales.length;
|
|
195
|
+
const selectedLocaleCount = selectedLocales.length;
|
|
196
|
+
const localeRatio = totalLocales === 0 ? 1 : selectedLocaleCount / totalLocales;
|
|
197
|
+
// v1.2.7: also scale by the ratio of selected docs vs total docs.
|
|
198
|
+
// Sum docs across the selected collections + globals; if everything
|
|
199
|
+
// is selected, ratio is 1. The perCollection rows in `preflight`
|
|
200
|
+
// carry both kinds (collections + globals) keyed by slug, so we
|
|
201
|
+
// sum by slug-membership in either selected list.
|
|
202
|
+
const selectedSlugSet = new Set([
|
|
203
|
+
...selectedCollections,
|
|
204
|
+
...selectedGlobals
|
|
205
|
+
]);
|
|
206
|
+
const totalDocs = preflight.documents.perCollection.reduce((sum, r)=>sum + (r.total ?? 0), 0);
|
|
207
|
+
const selectedDocs = preflight.documents.perCollection.reduce((sum, r)=>sum + (selectedSlugSet.has(r.slug) ? r.total ?? 0 : 0), 0);
|
|
208
|
+
const docRatio = totalDocs === 0 ? 1 : selectedDocs / totalDocs;
|
|
209
|
+
const ratio = localeRatio * docRatio;
|
|
210
|
+
if (ratio === 1) return preflight;
|
|
211
|
+
const scaledCost = typeof preflight.estimatedCostUsd === 'number' ? preflight.estimatedCostUsd * ratio : preflight.estimatedCostUsd;
|
|
212
|
+
return {
|
|
213
|
+
...preflight,
|
|
214
|
+
estimatedCostUsd: scaledCost,
|
|
215
|
+
documents: {
|
|
216
|
+
...preflight.documents,
|
|
217
|
+
total: Math.round((preflight.documents.total ?? 0) * ratio)
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
})();
|
|
221
|
+
const costState = deriveCostState(scaledPreflight, timedOut);
|
|
222
|
+
const capBlocked = isCapBlocked(scaledPreflight);
|
|
223
|
+
const requireTotp = preflight?.requireTotp === true;
|
|
224
|
+
const totpEnrolled = preflight?.totpEnrolled === true;
|
|
225
|
+
const totpMissing = requireTotp && !totpEnrolled;
|
|
226
|
+
const noLocalesSelected = preflight !== null && selectedLocales.length === 0;
|
|
227
|
+
const noScopeSelected = preflight !== null && selectedCollections.length === 0 && selectedGlobals.length === 0;
|
|
228
|
+
const canSubmit = !submitting && preflight !== null && costState.kind === 'ready' && !capBlocked && !totpMissing && !noLocalesSelected && !noScopeSelected && (!requireTotp || totp.length === 6);
|
|
229
|
+
async function onSubmit() {
|
|
230
|
+
if (!canSubmit) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
setSubmitting(true);
|
|
234
|
+
setSubmitError(null);
|
|
235
|
+
try {
|
|
236
|
+
// Narrow the run by locale when the operator has unchecked at
|
|
237
|
+
// least one chip. Omitting `scope.locales` keeps the existing
|
|
238
|
+
// "translate to every configured target locale" behavior for
|
|
239
|
+
// operators who haven't touched the picker.
|
|
240
|
+
const localesNarrowed = preflight !== null && selectedLocales.length > 0 && selectedLocales.length < preflight.scope.locales.length;
|
|
241
|
+
// v1.2.7: same narrowing pattern for collections + globals.
|
|
242
|
+
// enqueue.ts:459-463 reads `scope.collections` / `scope.globals`
|
|
243
|
+
// — when non-empty arrays, the batch is restricted to that set;
|
|
244
|
+
// when omitted/empty, it falls back to the plugin's full config.
|
|
245
|
+
const collectionsNarrowed = preflight !== null && selectedCollections.length < preflight.scope.collections.length;
|
|
246
|
+
const globalsNarrowed = preflight !== null && selectedGlobals.length < preflight.scope.globals.length;
|
|
247
|
+
const scopeNarrowing = {};
|
|
248
|
+
if (localesNarrowed) scopeNarrowing.locales = selectedLocales;
|
|
249
|
+
if (collectionsNarrowed) scopeNarrowing.collections = selectedCollections;
|
|
250
|
+
if (globalsNarrowed) scopeNarrowing.globals = selectedGlobals;
|
|
251
|
+
const res = await fetch(`${basePath}/api/translation-hub/bulk-translate`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
credentials: 'include',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json'
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
mode: force ? 'force' : 'changed',
|
|
259
|
+
totp: requireTotp ? totp : undefined,
|
|
260
|
+
scope: Object.keys(scopeNarrowing).length > 0 ? scopeNarrowing : undefined
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
// Server returns { error: string } for cap/totp/already-running.
|
|
265
|
+
const body = await res.json().catch(()=>null);
|
|
266
|
+
throw new Error(body?.error ?? `HTTP ${res.status}`);
|
|
267
|
+
}
|
|
268
|
+
setPostEnqueueVisible(true);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
setSubmitError(e instanceof Error ? e.message : String(e));
|
|
271
|
+
} finally{
|
|
272
|
+
setSubmitting(false);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (postEnqueueVisible) {
|
|
276
|
+
return /*#__PURE__*/ _jsx(BulkTranslatePostEnqueueTransition, {
|
|
277
|
+
basePath: basePath,
|
|
278
|
+
onResolved: onClose
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
282
|
+
onClick: (e)=>{
|
|
283
|
+
// NEW-6 (v1.2.6): backdrop click dismisses. The previous block
|
|
284
|
+
// (`if (e.target === e.currentTarget) {}`) was unreachable —
|
|
285
|
+
// React's synthetic event for an overlay click reports the
|
|
286
|
+
// child dialog as currentTarget when the click propagates from
|
|
287
|
+
// inside, so the equality never held. Stop-propagation on the
|
|
288
|
+
// dialog wrapper guarantees dismiss only fires on backdrop hits.
|
|
289
|
+
onClose();
|
|
290
|
+
},
|
|
291
|
+
style: OVERLAY_STYLE,
|
|
292
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
293
|
+
"aria-labelledby": "bulk-translate-modal-title",
|
|
294
|
+
"aria-modal": "true",
|
|
295
|
+
"data-testid": "bulk-translate-preflight-modal",
|
|
296
|
+
onClick: (e)=>e.stopPropagation(),
|
|
297
|
+
ref: dialogRef,
|
|
298
|
+
role: "dialog",
|
|
299
|
+
style: {
|
|
300
|
+
...MODAL_STYLE,
|
|
301
|
+
position: 'relative'
|
|
302
|
+
},
|
|
303
|
+
children: [
|
|
304
|
+
/*#__PURE__*/ _jsx("button", {
|
|
305
|
+
"aria-label": "Close",
|
|
306
|
+
"data-testid": "bulk-translate-close",
|
|
307
|
+
onClick: onClose,
|
|
308
|
+
style: {
|
|
309
|
+
position: 'absolute',
|
|
310
|
+
top: '0.5rem',
|
|
311
|
+
right: '0.5rem',
|
|
312
|
+
width: '2rem',
|
|
313
|
+
height: '2rem',
|
|
314
|
+
display: 'flex',
|
|
315
|
+
alignItems: 'center',
|
|
316
|
+
justifyContent: 'center',
|
|
317
|
+
background: 'transparent',
|
|
318
|
+
border: 'none',
|
|
319
|
+
borderRadius: '4px',
|
|
320
|
+
color: 'var(--theme-elevation-700)',
|
|
321
|
+
cursor: 'pointer',
|
|
322
|
+
fontSize: '1.25rem',
|
|
323
|
+
lineHeight: 1
|
|
324
|
+
},
|
|
325
|
+
type: "button",
|
|
326
|
+
children: "×"
|
|
327
|
+
}),
|
|
328
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
329
|
+
id: "bulk-translate-modal-title",
|
|
330
|
+
style: {
|
|
331
|
+
margin: '0 0 1rem',
|
|
332
|
+
fontSize: '1.125rem'
|
|
333
|
+
},
|
|
334
|
+
children: "Bulk Translate"
|
|
335
|
+
}),
|
|
336
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
337
|
+
label: "Scope",
|
|
338
|
+
children: preflight ? /*#__PURE__*/ _jsxs("p", {
|
|
339
|
+
style: {
|
|
340
|
+
margin: 0,
|
|
341
|
+
fontSize: '0.875rem'
|
|
342
|
+
},
|
|
343
|
+
children: [
|
|
344
|
+
selectedCollections.length,
|
|
345
|
+
"/",
|
|
346
|
+
preflight.scope.collections.length,
|
|
347
|
+
" collections ·",
|
|
348
|
+
' ',
|
|
349
|
+
selectedGlobals.length,
|
|
350
|
+
"/",
|
|
351
|
+
preflight.scope.globals.length,
|
|
352
|
+
" global",
|
|
353
|
+
preflight.scope.globals.length === 1 ? '' : 's',
|
|
354
|
+
" · ",
|
|
355
|
+
selectedLocales.length,
|
|
356
|
+
"/",
|
|
357
|
+
preflight.scope.locales.length,
|
|
358
|
+
" target locales · ~",
|
|
359
|
+
scaledPreflight?.documents.total ?? preflight.documents.total,
|
|
360
|
+
" documents"
|
|
361
|
+
]
|
|
362
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
363
|
+
style: SKELETON_STYLE
|
|
364
|
+
})
|
|
365
|
+
}),
|
|
366
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
367
|
+
label: "Collections",
|
|
368
|
+
children: preflight ? preflight.scope.collections.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
369
|
+
style: {
|
|
370
|
+
margin: 0,
|
|
371
|
+
fontSize: '0.8125rem',
|
|
372
|
+
fontStyle: 'italic',
|
|
373
|
+
color: 'var(--theme-elevation-700)'
|
|
374
|
+
},
|
|
375
|
+
children: "No collections registered for translation."
|
|
376
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
377
|
+
"data-testid": "bulk-translate-collection-chips",
|
|
378
|
+
style: {
|
|
379
|
+
display: 'flex',
|
|
380
|
+
flexWrap: 'wrap',
|
|
381
|
+
gap: '0.35rem'
|
|
382
|
+
},
|
|
383
|
+
children: preflight.scope.collections.map((slug)=>{
|
|
384
|
+
const active = selectedCollections.includes(slug);
|
|
385
|
+
return /*#__PURE__*/ _jsx("button", {
|
|
386
|
+
"aria-pressed": active,
|
|
387
|
+
"data-testid": `bulk-translate-collection-${slug}`,
|
|
388
|
+
onClick: ()=>toggleCollection(slug),
|
|
389
|
+
style: {
|
|
390
|
+
padding: '0.2rem 0.55rem',
|
|
391
|
+
borderRadius: '999px',
|
|
392
|
+
fontSize: '0.75rem',
|
|
393
|
+
fontWeight: active ? 600 : 500,
|
|
394
|
+
border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
|
|
395
|
+
background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
|
|
396
|
+
color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
|
|
397
|
+
cursor: 'pointer'
|
|
398
|
+
},
|
|
399
|
+
type: "button",
|
|
400
|
+
children: slug
|
|
401
|
+
}, slug);
|
|
402
|
+
})
|
|
403
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
404
|
+
style: SKELETON_STYLE
|
|
405
|
+
})
|
|
406
|
+
}),
|
|
407
|
+
/*#__PURE__*/ _jsxs(Section, {
|
|
408
|
+
label: "Globals",
|
|
409
|
+
children: [
|
|
410
|
+
preflight ? preflight.scope.globals.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
411
|
+
style: {
|
|
412
|
+
margin: 0,
|
|
413
|
+
fontSize: '0.8125rem',
|
|
414
|
+
fontStyle: 'italic',
|
|
415
|
+
color: 'var(--theme-elevation-700)'
|
|
416
|
+
},
|
|
417
|
+
children: "No globals registered for translation."
|
|
418
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
419
|
+
"data-testid": "bulk-translate-global-chips",
|
|
420
|
+
style: {
|
|
421
|
+
display: 'flex',
|
|
422
|
+
flexWrap: 'wrap',
|
|
423
|
+
gap: '0.35rem'
|
|
424
|
+
},
|
|
425
|
+
children: preflight.scope.globals.map((slug)=>{
|
|
426
|
+
const active = selectedGlobals.includes(slug);
|
|
427
|
+
return /*#__PURE__*/ _jsx("button", {
|
|
428
|
+
"aria-pressed": active,
|
|
429
|
+
"data-testid": `bulk-translate-global-${slug}`,
|
|
430
|
+
onClick: ()=>toggleGlobal(slug),
|
|
431
|
+
style: {
|
|
432
|
+
padding: '0.2rem 0.55rem',
|
|
433
|
+
borderRadius: '999px',
|
|
434
|
+
fontSize: '0.75rem',
|
|
435
|
+
fontWeight: active ? 600 : 500,
|
|
436
|
+
border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
|
|
437
|
+
background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
|
|
438
|
+
color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
|
|
439
|
+
cursor: 'pointer'
|
|
440
|
+
},
|
|
441
|
+
type: "button",
|
|
442
|
+
children: slug
|
|
443
|
+
}, slug);
|
|
444
|
+
})
|
|
445
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
446
|
+
style: SKELETON_STYLE
|
|
447
|
+
}),
|
|
448
|
+
noScopeSelected ? /*#__PURE__*/ _jsx("p", {
|
|
449
|
+
role: "alert",
|
|
450
|
+
style: {
|
|
451
|
+
margin: '0.35rem 0 0',
|
|
452
|
+
fontSize: '0.75rem',
|
|
453
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
454
|
+
},
|
|
455
|
+
children: "Select at least one collection or global."
|
|
456
|
+
}) : null
|
|
457
|
+
]
|
|
458
|
+
}),
|
|
459
|
+
/*#__PURE__*/ _jsxs(Section, {
|
|
460
|
+
label: "Target locales",
|
|
461
|
+
children: [
|
|
462
|
+
preflight ? /*#__PURE__*/ _jsx("div", {
|
|
463
|
+
"data-testid": "bulk-translate-locale-chips",
|
|
464
|
+
style: {
|
|
465
|
+
display: 'flex',
|
|
466
|
+
flexWrap: 'wrap',
|
|
467
|
+
gap: '0.35rem'
|
|
468
|
+
},
|
|
469
|
+
children: preflight.scope.locales.map((code)=>{
|
|
470
|
+
const active = selectedLocales.includes(code);
|
|
471
|
+
return /*#__PURE__*/ _jsx("button", {
|
|
472
|
+
"aria-pressed": active,
|
|
473
|
+
"data-testid": `bulk-translate-locale-${code}`,
|
|
474
|
+
onClick: ()=>toggleLocale(code),
|
|
475
|
+
style: {
|
|
476
|
+
padding: '0.2rem 0.55rem',
|
|
477
|
+
borderRadius: '999px',
|
|
478
|
+
fontSize: '0.75rem',
|
|
479
|
+
fontFamily: 'monospace',
|
|
480
|
+
fontWeight: active ? 600 : 500,
|
|
481
|
+
border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
|
|
482
|
+
background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
|
|
483
|
+
// NEW-20 (v1.2.6): deselected chip text was
|
|
484
|
+
// `elevation-500` (mid-grey ≈ 3.62:1 on the
|
|
485
|
+
// `elevation-50` background, failing WCAG AA
|
|
486
|
+
// 4.5:1). Bumped to `elevation-900` (near-black
|
|
487
|
+
// ≈ 13:1) so the disabled state stays readable
|
|
488
|
+
// without losing the active-vs-inactive visual
|
|
489
|
+
// contrast.
|
|
490
|
+
color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
|
|
491
|
+
cursor: 'pointer'
|
|
492
|
+
},
|
|
493
|
+
type: "button",
|
|
494
|
+
children: code
|
|
495
|
+
}, code);
|
|
496
|
+
})
|
|
497
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
498
|
+
style: SKELETON_STYLE
|
|
499
|
+
}),
|
|
500
|
+
noLocalesSelected ? /*#__PURE__*/ _jsx("p", {
|
|
501
|
+
role: "alert",
|
|
502
|
+
style: {
|
|
503
|
+
margin: '0.35rem 0 0',
|
|
504
|
+
fontSize: '0.75rem',
|
|
505
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
506
|
+
},
|
|
507
|
+
children: "Select at least one locale."
|
|
508
|
+
}) : null
|
|
509
|
+
]
|
|
510
|
+
}),
|
|
511
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
512
|
+
label: "Breakdown",
|
|
513
|
+
children: preflight ? noLocalesSelected ? // NEW-7 (v1.2.6): when zero locales are selected the
|
|
514
|
+
// Scope says "~0 documents" but the Breakdown table
|
|
515
|
+
// used to keep showing full counts (e.g. posts 41/41)
|
|
516
|
+
// — two parts of the same modal disagreeing. Suppress
|
|
517
|
+
// the table here; the Scope summary already conveys
|
|
518
|
+
// the "select at least one locale" state.
|
|
519
|
+
/*#__PURE__*/ _jsx("p", {
|
|
520
|
+
"data-testid": "bulk-translate-breakdown-empty",
|
|
521
|
+
style: {
|
|
522
|
+
margin: 0,
|
|
523
|
+
fontSize: '0.8125rem',
|
|
524
|
+
fontStyle: 'italic',
|
|
525
|
+
color: 'var(--theme-elevation-700)'
|
|
526
|
+
},
|
|
527
|
+
children: "Select at least one locale to see per-collection counts."
|
|
528
|
+
}) : /*#__PURE__*/ _jsx("table", {
|
|
529
|
+
style: {
|
|
530
|
+
width: '100%',
|
|
531
|
+
borderCollapse: 'collapse',
|
|
532
|
+
fontSize: '0.8125rem'
|
|
533
|
+
},
|
|
534
|
+
children: /*#__PURE__*/ _jsx("tbody", {
|
|
535
|
+
children: preflight.documents.perCollection.filter((row)=>{
|
|
536
|
+
// v1.2.7: only show rows that match the active scope
|
|
537
|
+
// selection. The same `slug` may name either a
|
|
538
|
+
// collection or a global — match against both lists.
|
|
539
|
+
return selectedCollections.includes(row.slug) || selectedGlobals.includes(row.slug);
|
|
540
|
+
}).map((row)=>/*#__PURE__*/ _jsxs("tr", {
|
|
541
|
+
children: [
|
|
542
|
+
/*#__PURE__*/ _jsx("td", {
|
|
543
|
+
style: {
|
|
544
|
+
padding: '0.25rem 0',
|
|
545
|
+
color: 'var(--theme-elevation-800)'
|
|
546
|
+
},
|
|
547
|
+
children: row.label
|
|
548
|
+
}),
|
|
549
|
+
/*#__PURE__*/ _jsxs("td", {
|
|
550
|
+
style: {
|
|
551
|
+
padding: '0.25rem 0',
|
|
552
|
+
textAlign: 'right',
|
|
553
|
+
color: 'var(--theme-elevation-700)'
|
|
554
|
+
},
|
|
555
|
+
children: [
|
|
556
|
+
row.changed,
|
|
557
|
+
" / ",
|
|
558
|
+
row.total
|
|
559
|
+
]
|
|
560
|
+
})
|
|
561
|
+
]
|
|
562
|
+
}, row.slug))
|
|
563
|
+
})
|
|
564
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
565
|
+
style: SKELETON_STYLE
|
|
566
|
+
})
|
|
567
|
+
}),
|
|
568
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
569
|
+
label: "Estimated cost",
|
|
570
|
+
children: /*#__PURE__*/ _jsx(CostBlock, {
|
|
571
|
+
costState: costState,
|
|
572
|
+
onRetry: ()=>{
|
|
573
|
+
setTimedOut(false);
|
|
574
|
+
setRetryNonce((n)=>n + 1);
|
|
575
|
+
}
|
|
576
|
+
})
|
|
577
|
+
}),
|
|
578
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
579
|
+
label: "Duration",
|
|
580
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
581
|
+
style: {
|
|
582
|
+
margin: 0,
|
|
583
|
+
fontSize: '0.8125rem',
|
|
584
|
+
color: 'var(--theme-elevation-700)'
|
|
585
|
+
},
|
|
586
|
+
children: "Typically 30-50 min. Jobs run in the background — safe to close."
|
|
587
|
+
})
|
|
588
|
+
}),
|
|
589
|
+
capBlocked && preflight ? /*#__PURE__*/ _jsxs("div", {
|
|
590
|
+
"data-testid": "bulk-translate-cap-blocked",
|
|
591
|
+
role: "alert",
|
|
592
|
+
style: {
|
|
593
|
+
marginTop: '0.75rem',
|
|
594
|
+
padding: '0.6rem 0.75rem',
|
|
595
|
+
background: 'var(--theme-error-100, #fee2e2)',
|
|
596
|
+
border: '1px solid var(--theme-error-500, #b91c1c)',
|
|
597
|
+
borderRadius: '4px',
|
|
598
|
+
fontSize: '0.8125rem',
|
|
599
|
+
color: 'var(--theme-error-500, #b91c1c)'
|
|
600
|
+
},
|
|
601
|
+
children: [
|
|
602
|
+
"Today’s translation budget is used up — ",
|
|
603
|
+
formatCost(preflight.dailySpend.spentUsd),
|
|
604
|
+
' ',
|
|
605
|
+
"spent of the ",
|
|
606
|
+
formatCost(preflight.dailySpend.capUsd),
|
|
607
|
+
" daily limit. The budget resets at midnight UTC, or ask engineering to raise the limit."
|
|
608
|
+
]
|
|
609
|
+
}) : null,
|
|
610
|
+
/*#__PURE__*/ _jsx("p", {
|
|
611
|
+
"data-testid": "bulk-translate-mode-copy",
|
|
612
|
+
style: {
|
|
613
|
+
marginTop: '0.75rem',
|
|
614
|
+
fontSize: '0.75rem',
|
|
615
|
+
color: 'var(--theme-elevation-700)'
|
|
616
|
+
},
|
|
617
|
+
children: force ? 'Force mode on — every document will be re-translated, even ones already up to date.' : "Documents already up-to-date will be skipped — flip 'Force re-translate' to include them."
|
|
618
|
+
}),
|
|
619
|
+
/*#__PURE__*/ _jsxs("label", {
|
|
620
|
+
style: {
|
|
621
|
+
display: 'flex',
|
|
622
|
+
alignItems: 'center',
|
|
623
|
+
gap: '0.5rem',
|
|
624
|
+
marginTop: '0.75rem',
|
|
625
|
+
fontSize: '0.8125rem'
|
|
626
|
+
},
|
|
627
|
+
children: [
|
|
628
|
+
/*#__PURE__*/ _jsx("input", {
|
|
629
|
+
checked: force,
|
|
630
|
+
"data-testid": "bulk-translate-force",
|
|
631
|
+
onChange: (e)=>setForce(e.target.checked),
|
|
632
|
+
type: "checkbox"
|
|
633
|
+
}),
|
|
634
|
+
/*#__PURE__*/ _jsx("span", {
|
|
635
|
+
children: "Force re-translate (includes docs already up to date)"
|
|
636
|
+
})
|
|
637
|
+
]
|
|
638
|
+
}),
|
|
639
|
+
requireTotp ? /*#__PURE__*/ _jsxs("div", {
|
|
640
|
+
style: {
|
|
641
|
+
marginTop: '0.75rem'
|
|
642
|
+
},
|
|
643
|
+
children: [
|
|
644
|
+
/*#__PURE__*/ _jsx("label", {
|
|
645
|
+
htmlFor: "bulk-translate-totp",
|
|
646
|
+
style: {
|
|
647
|
+
...SECTION_LABEL,
|
|
648
|
+
display: 'block'
|
|
649
|
+
},
|
|
650
|
+
children: "Authentication code"
|
|
651
|
+
}),
|
|
652
|
+
totpMissing ? /*#__PURE__*/ _jsx("a", {
|
|
653
|
+
"data-testid": "bulk-translate-totp-missing",
|
|
654
|
+
href: `${basePath}/admin/account`,
|
|
655
|
+
style: {
|
|
656
|
+
fontSize: '0.8125rem',
|
|
657
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
658
|
+
},
|
|
659
|
+
children: "Two-factor auth not set up — click to enrol"
|
|
660
|
+
}) : /*#__PURE__*/ _jsx("input", {
|
|
661
|
+
autoComplete: "one-time-code",
|
|
662
|
+
"data-testid": "bulk-translate-totp-input",
|
|
663
|
+
id: "bulk-translate-totp",
|
|
664
|
+
inputMode: "numeric",
|
|
665
|
+
maxLength: 6,
|
|
666
|
+
onChange: (e)=>setTotp(e.target.value.replace(/\D/g, '').slice(0, 6)),
|
|
667
|
+
pattern: "[0-9]{6}",
|
|
668
|
+
placeholder: "000000",
|
|
669
|
+
style: INPUT_STYLE,
|
|
670
|
+
type: "text",
|
|
671
|
+
value: totp
|
|
672
|
+
})
|
|
673
|
+
]
|
|
674
|
+
}) : null,
|
|
675
|
+
loadError ? /*#__PURE__*/ _jsxs("p", {
|
|
676
|
+
style: ERROR_TEXT,
|
|
677
|
+
children: [
|
|
678
|
+
"Couldn’t open the translation settings — ",
|
|
679
|
+
loadError
|
|
680
|
+
]
|
|
681
|
+
}) : null,
|
|
682
|
+
submitError ? /*#__PURE__*/ _jsx("p", {
|
|
683
|
+
style: ERROR_TEXT,
|
|
684
|
+
children: submitError
|
|
685
|
+
}) : null,
|
|
686
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
687
|
+
style: {
|
|
688
|
+
display: 'flex',
|
|
689
|
+
justifyContent: 'flex-end',
|
|
690
|
+
gap: '0.5rem',
|
|
691
|
+
marginTop: '1.25rem'
|
|
692
|
+
},
|
|
693
|
+
children: [
|
|
694
|
+
/*#__PURE__*/ _jsx("button", {
|
|
695
|
+
onClick: onClose,
|
|
696
|
+
style: {
|
|
697
|
+
padding: '0.4rem 0.85rem',
|
|
698
|
+
background: 'transparent',
|
|
699
|
+
border: 'none',
|
|
700
|
+
borderRadius: '4px',
|
|
701
|
+
color: 'var(--theme-elevation-700)',
|
|
702
|
+
cursor: 'pointer',
|
|
703
|
+
fontSize: '0.8125rem'
|
|
704
|
+
},
|
|
705
|
+
type: "button",
|
|
706
|
+
children: "Cancel"
|
|
707
|
+
}),
|
|
708
|
+
/*#__PURE__*/ _jsx("button", {
|
|
709
|
+
"data-testid": "bulk-translate-start",
|
|
710
|
+
disabled: !canSubmit,
|
|
711
|
+
onClick: onSubmit,
|
|
712
|
+
style: {
|
|
713
|
+
padding: '0.4rem 0.85rem',
|
|
714
|
+
background: 'transparent',
|
|
715
|
+
border: '1px solid var(--theme-success-500, #16a34a)',
|
|
716
|
+
borderRadius: '4px',
|
|
717
|
+
color: 'var(--theme-success-500, #16a34a)',
|
|
718
|
+
cursor: canSubmit ? 'pointer' : 'not-allowed',
|
|
719
|
+
opacity: canSubmit ? 1 : 0.5,
|
|
720
|
+
fontSize: '0.8125rem',
|
|
721
|
+
fontWeight: 600
|
|
722
|
+
},
|
|
723
|
+
type: "button",
|
|
724
|
+
children: "Start Translation"
|
|
725
|
+
})
|
|
726
|
+
]
|
|
727
|
+
})
|
|
728
|
+
]
|
|
729
|
+
})
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
const Section = ({ label, children })=>/*#__PURE__*/ _jsxs("div", {
|
|
733
|
+
style: {
|
|
734
|
+
marginTop: '0.75rem'
|
|
735
|
+
},
|
|
736
|
+
children: [
|
|
737
|
+
/*#__PURE__*/ _jsx("p", {
|
|
738
|
+
style: SECTION_LABEL,
|
|
739
|
+
children: label
|
|
740
|
+
}),
|
|
741
|
+
children
|
|
742
|
+
]
|
|
743
|
+
});
|
|
744
|
+
/**
|
|
745
|
+
* NEW-26 (v1.2.6): accessible tooltip for the "~$0.15" cost estimate.
|
|
746
|
+
*
|
|
747
|
+
* The previous `<span title="...">` only revealed the explanation on
|
|
748
|
+
* mouse hover — keyboard focus, screen-reader announcement, and mobile
|
|
749
|
+
* tap were all locked out. Plugin has no Radix / Popover dependency
|
|
750
|
+
* (intentional — keeps bundle small for an admin-only surface), so
|
|
751
|
+
* this is a minimal focus-visible-revealed `<span>` with `aria-describedby`
|
|
752
|
+
* linking the trigger to a `role="tooltip"` payload kept visually-hidden
|
|
753
|
+
* until the trigger receives keyboard focus or hover.
|
|
754
|
+
*/ const TOOLTIP_ID = 'bulk-translate-cost-tooltip';
|
|
755
|
+
const CostEstimateInfo = ()=>{
|
|
756
|
+
const [open, setOpen] = useState(false);
|
|
757
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
758
|
+
style: {
|
|
759
|
+
position: 'relative',
|
|
760
|
+
display: 'inline-block'
|
|
761
|
+
},
|
|
762
|
+
children: [
|
|
763
|
+
/*#__PURE__*/ _jsx("button", {
|
|
764
|
+
"aria-describedby": open ? TOOLTIP_ID : undefined,
|
|
765
|
+
"aria-label": "About the cost estimate",
|
|
766
|
+
"data-testid": "bulk-translate-cost-info",
|
|
767
|
+
onBlur: ()=>setOpen(false),
|
|
768
|
+
onClick: ()=>setOpen((v)=>!v),
|
|
769
|
+
onFocus: ()=>setOpen(true),
|
|
770
|
+
onMouseEnter: ()=>setOpen(true),
|
|
771
|
+
onMouseLeave: ()=>setOpen(false),
|
|
772
|
+
style: {
|
|
773
|
+
background: 'transparent',
|
|
774
|
+
border: '1px solid var(--theme-elevation-300)',
|
|
775
|
+
borderRadius: '999px',
|
|
776
|
+
width: '1.1rem',
|
|
777
|
+
height: '1.1rem',
|
|
778
|
+
padding: 0,
|
|
779
|
+
fontSize: '0.7rem',
|
|
780
|
+
fontWeight: 600,
|
|
781
|
+
color: 'var(--theme-elevation-700)',
|
|
782
|
+
cursor: 'help',
|
|
783
|
+
lineHeight: 1
|
|
784
|
+
},
|
|
785
|
+
type: "button",
|
|
786
|
+
children: "?"
|
|
787
|
+
}),
|
|
788
|
+
/*#__PURE__*/ _jsx("span", {
|
|
789
|
+
id: TOOLTIP_ID,
|
|
790
|
+
role: "tooltip",
|
|
791
|
+
style: {
|
|
792
|
+
position: 'absolute',
|
|
793
|
+
bottom: 'calc(100% + 6px)',
|
|
794
|
+
right: 0,
|
|
795
|
+
minWidth: '14rem',
|
|
796
|
+
padding: '0.4rem 0.6rem',
|
|
797
|
+
background: 'var(--theme-elevation-1000)',
|
|
798
|
+
color: 'var(--theme-elevation-50)',
|
|
799
|
+
fontSize: '0.75rem',
|
|
800
|
+
lineHeight: 1.4,
|
|
801
|
+
borderRadius: '4px',
|
|
802
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.18)',
|
|
803
|
+
pointerEvents: 'none',
|
|
804
|
+
zIndex: 10,
|
|
805
|
+
// visibility: hidden (not display: none) so the tooltip
|
|
806
|
+
// text stays in the a11y tree and aria-describedby resolves.
|
|
807
|
+
visibility: open ? 'visible' : 'hidden',
|
|
808
|
+
opacity: open ? 1 : 0,
|
|
809
|
+
transition: 'opacity 120ms ease'
|
|
810
|
+
},
|
|
811
|
+
children: "Based on the unchanged-content diff. Actual cost may be lower if more documents are skipped."
|
|
812
|
+
})
|
|
813
|
+
]
|
|
814
|
+
});
|
|
815
|
+
};
|
|
816
|
+
const CostBlock = ({ costState, onRetry })=>{
|
|
817
|
+
if (costState.kind === 'loading') {
|
|
818
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
819
|
+
"aria-busy": "true",
|
|
820
|
+
"data-testid": "bulk-translate-cost-loading",
|
|
821
|
+
role: "status",
|
|
822
|
+
style: SKELETON_STYLE
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (costState.kind === 'ready') {
|
|
826
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
827
|
+
"data-testid": "bulk-translate-cost-ready",
|
|
828
|
+
style: {
|
|
829
|
+
...COST_TEXT_STYLE,
|
|
830
|
+
display: 'inline-flex',
|
|
831
|
+
alignItems: 'center',
|
|
832
|
+
gap: '0.4rem'
|
|
833
|
+
},
|
|
834
|
+
children: [
|
|
835
|
+
"~",
|
|
836
|
+
formatCost(costState.value),
|
|
837
|
+
/*#__PURE__*/ _jsx(CostEstimateInfo, {})
|
|
838
|
+
]
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (costState.kind === 'timeout') {
|
|
842
|
+
return /*#__PURE__*/ _jsxs("p", {
|
|
843
|
+
"data-testid": "bulk-translate-cost-timeout",
|
|
844
|
+
style: {
|
|
845
|
+
fontSize: '0.8125rem',
|
|
846
|
+
color: 'var(--theme-warning-500, #d97706)',
|
|
847
|
+
margin: 0
|
|
848
|
+
},
|
|
849
|
+
children: [
|
|
850
|
+
"Cost estimate isn’t ready yet —",
|
|
851
|
+
' ',
|
|
852
|
+
/*#__PURE__*/ _jsx("button", {
|
|
853
|
+
onClick: onRetry,
|
|
854
|
+
style: {
|
|
855
|
+
background: 'transparent',
|
|
856
|
+
border: 'none',
|
|
857
|
+
color: 'inherit',
|
|
858
|
+
textDecoration: 'underline',
|
|
859
|
+
cursor: 'pointer',
|
|
860
|
+
padding: 0,
|
|
861
|
+
fontSize: 'inherit'
|
|
862
|
+
},
|
|
863
|
+
type: "button",
|
|
864
|
+
children: "try again"
|
|
865
|
+
}),
|
|
866
|
+
"."
|
|
867
|
+
]
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return /*#__PURE__*/ _jsx("p", {
|
|
871
|
+
"data-testid": "bulk-translate-cost-unavailable",
|
|
872
|
+
style: {
|
|
873
|
+
fontSize: '0.8125rem',
|
|
874
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
875
|
+
margin: 0
|
|
876
|
+
},
|
|
877
|
+
children: "This translation can’t be costed right now and has been blocked as a safety measure. Contact engineering — the AI model’s pricing may need updating."
|
|
878
|
+
});
|
|
879
|
+
};
|