@strapi-ai/translator 0.0.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/admin/SettingsPage-C5nm32bD.mjs +466 -0
  4. package/dist/admin/SettingsPage-CD90jfwT.js +466 -0
  5. package/dist/admin/en-CU6ocWlA.mjs +97 -0
  6. package/dist/admin/en-I9cvzxfF.js +97 -0
  7. package/dist/admin/index-CS0LCA35.mjs +1694 -0
  8. package/dist/admin/index-DQ3-ym--.js +1693 -0
  9. package/dist/admin/index.js +4 -0
  10. package/dist/admin/index.mjs +4 -0
  11. package/dist/admin/src/components/DiffView.d.ts +31 -0
  12. package/dist/admin/src/components/Initializer.d.ts +3 -0
  13. package/dist/admin/src/components/PluginIcon.d.ts +4 -0
  14. package/dist/admin/src/components/ProgressBar.d.ts +7 -0
  15. package/dist/admin/src/components/TranslationModal.d.ts +23 -0
  16. package/dist/admin/src/components/TranslationPanel.d.ts +4 -0
  17. package/dist/admin/src/components/TranslationWorkflow.d.ts +2 -0
  18. package/dist/admin/src/components/settings/ProviderConnectionFields.d.ts +12 -0
  19. package/dist/admin/src/components/settings/TranslationSettingsFields.d.ts +7 -0
  20. package/dist/admin/src/hooks/useProviderModels.d.ts +13 -0
  21. package/dist/admin/src/hooks/useTranslationConfig.d.ts +8 -0
  22. package/dist/admin/src/index.d.ts +12 -0
  23. package/dist/admin/src/pages/SettingsPage.d.ts +2 -0
  24. package/dist/admin/src/pluginId.d.ts +1 -0
  25. package/dist/admin/src/utils/clampNumber.d.ts +1 -0
  26. package/dist/admin/src/utils/configPayload.d.ts +23 -0
  27. package/dist/admin/src/utils/contentManagerLocale.d.ts +1 -0
  28. package/dist/admin/src/utils/getTranslation.d.ts +1 -0
  29. package/dist/admin/src/utils/providerDefaults.d.ts +10 -0
  30. package/dist/admin/src/utils/sse.d.ts +2 -0
  31. package/dist/admin/src/utils/translatableFields.d.ts +3 -0
  32. package/dist/admin/src/utils/translationPatch.d.ts +13 -0
  33. package/dist/server/index.js +1217 -0
  34. package/dist/server/index.mjs +1217 -0
  35. package/dist/server/src/application/assemble-translation-patch.d.ts +3 -0
  36. package/dist/server/src/application/extract-translatable-units.d.ts +9 -0
  37. package/dist/server/src/application/translate-entry.d.ts +17 -0
  38. package/dist/server/src/application/translation-provider.d.ts +33 -0
  39. package/dist/server/src/bootstrap.d.ts +5 -0
  40. package/dist/server/src/config/index.d.ts +5 -0
  41. package/dist/server/src/controllers/index.d.ts +27 -0
  42. package/dist/server/src/controllers/translation.controller.d.ts +28 -0
  43. package/dist/server/src/domain/field-path.d.ts +6 -0
  44. package/dist/server/src/domain/translation-config.d.ts +27 -0
  45. package/dist/server/src/domain/translation-event.d.ts +59 -0
  46. package/dist/server/src/domain/translation-patch.d.ts +11 -0
  47. package/dist/server/src/domain/translation-unit.d.ts +28 -0
  48. package/dist/server/src/domain/value-codecs/index.d.ts +3 -0
  49. package/dist/server/src/domain/value-codecs/string-codec.d.ts +2 -0
  50. package/dist/server/src/domain/value-codecs/structured-value-codec.d.ts +3 -0
  51. package/dist/server/src/domain/value-codecs/types.d.ts +11 -0
  52. package/dist/server/src/index.d.ts +64 -0
  53. package/dist/server/src/infrastructure/llm/chat-completion-client.d.ts +18 -0
  54. package/dist/server/src/infrastructure/llm/llm-translation-provider.d.ts +15 -0
  55. package/dist/server/src/infrastructure/llm/model-list-client.d.ts +19 -0
  56. package/dist/server/src/infrastructure/llm/openai-chat-completion-client.d.ts +6 -0
  57. package/dist/server/src/infrastructure/strapi/reconcile-schema.d.ts +7 -0
  58. package/dist/server/src/infrastructure/strapi/secret-cipher.d.ts +8 -0
  59. package/dist/server/src/infrastructure/strapi/translation-config-repository.d.ts +16 -0
  60. package/dist/server/src/infrastructure/strapi/translation-session-service.d.ts +37 -0
  61. package/dist/server/src/routes/index.d.ts +19 -0
  62. package/dist/server/src/services/index.d.ts +7 -0
  63. package/package.json +87 -0
@@ -0,0 +1,1693 @@
1
+ "use strict";
2
+ const react = require("react");
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const admin = require("@strapi/strapi/admin");
5
+ const reactIntl = require("react-intl");
6
+ const icons = require("@strapi/icons");
7
+ const designSystem = require("@strapi/design-system");
8
+ const styledComponents = require("styled-components");
9
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
10
+ const v = glob[path];
11
+ if (v) {
12
+ return typeof v === "function" ? v() : Promise.resolve(v);
13
+ }
14
+ return new Promise((_, reject) => {
15
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
16
+ reject.bind(
17
+ null,
18
+ new Error(
19
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
20
+ )
21
+ )
22
+ );
23
+ });
24
+ };
25
+ const PLUGIN_ID = "ai-translator";
26
+ function getTranslation(id) {
27
+ return [PLUGIN_ID, id].join(".");
28
+ }
29
+ function Initializer({ setPlugin }) {
30
+ const setPluginRef = react.useRef(setPlugin);
31
+ react.useEffect(() => {
32
+ setPluginRef.current(PLUGIN_ID);
33
+ }, []);
34
+ return null;
35
+ }
36
+ const I18N_LOCALE_PARAM = "plugins[i18n][locale]";
37
+ const FALLBACK_LOCALE = "en";
38
+ const getContentManagerLocale = () => {
39
+ if (typeof window === "undefined" || !window.location) {
40
+ return FALLBACK_LOCALE;
41
+ }
42
+ const query = window.location.href.split("?")[1] ?? "";
43
+ return new URLSearchParams(query).get(I18N_LOCALE_PARAM) || FALLBACK_LOCALE;
44
+ };
45
+ async function* parseSSEStream(response) {
46
+ const reader = response.body.getReader();
47
+ const decoder = new TextDecoder();
48
+ let buffer = "";
49
+ let dataLines = [];
50
+ const parseEvent = () => {
51
+ if (dataLines.length === 0) {
52
+ return null;
53
+ }
54
+ const rawData = dataLines.join("\n");
55
+ dataLines = [];
56
+ try {
57
+ return JSON.parse(rawData);
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+ const processLine = (line) => {
63
+ const normalizedLine = line.endsWith("\r") ? line.slice(0, -1) : line;
64
+ if (normalizedLine.startsWith("data:")) {
65
+ dataLines.push(normalizedLine.slice(5).trimStart());
66
+ return null;
67
+ }
68
+ if (normalizedLine === "") {
69
+ return parseEvent();
70
+ }
71
+ return null;
72
+ };
73
+ try {
74
+ while (true) {
75
+ const { done, value } = await reader.read();
76
+ if (done) {
77
+ if (buffer) {
78
+ const event2 = processLine(buffer);
79
+ if (event2) {
80
+ yield event2;
81
+ }
82
+ buffer = "";
83
+ }
84
+ const event = parseEvent();
85
+ if (event) {
86
+ yield event;
87
+ }
88
+ break;
89
+ }
90
+ buffer += decoder.decode(value, { stream: true });
91
+ const lines = buffer.split("\n");
92
+ buffer = lines.pop() || "";
93
+ for (const line of lines) {
94
+ const event = processLine(line);
95
+ if (event) {
96
+ yield event;
97
+ }
98
+ }
99
+ }
100
+ } finally {
101
+ reader.releaseLock();
102
+ }
103
+ }
104
+ const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
105
+ const cloneValue = (value) => {
106
+ if (typeof structuredClone === "function") {
107
+ return structuredClone(value);
108
+ }
109
+ return JSON.parse(JSON.stringify(value));
110
+ };
111
+ const pathToLabel = (path) => {
112
+ if (!path || path.length === 0) {
113
+ return "";
114
+ }
115
+ return path.reduce((label, segment) => {
116
+ if (typeof segment === "number") {
117
+ return `${label}[${segment}]`;
118
+ }
119
+ return label ? `${label}.${segment}` : segment;
120
+ }, "");
121
+ };
122
+ const getValueByPath = (source, path) => path.reduce((cursor, segment) => {
123
+ if (cursor === void 0 || cursor === null) {
124
+ return void 0;
125
+ }
126
+ return cursor[segment];
127
+ }, source);
128
+ const setValueByPath = (source, path, value) => {
129
+ const clone = cloneValue(source);
130
+ if (path.some((segment) => typeof segment === "string" && FORBIDDEN_SEGMENTS.has(segment))) {
131
+ return clone;
132
+ }
133
+ let cursor = clone;
134
+ for (let index2 = 0; index2 < path.length - 1; index2 += 1) {
135
+ const segment = path[index2];
136
+ const nextSegment = path[index2 + 1];
137
+ if (cursor[segment] === void 0 || cursor[segment] === null) {
138
+ cursor[segment] = typeof nextSegment === "number" ? [] : {};
139
+ }
140
+ cursor = cursor[segment];
141
+ }
142
+ cursor[path[path.length - 1]] = value;
143
+ return clone;
144
+ };
145
+ const applyTranslationPatch = (source, patch) => patch.operations.reduce(
146
+ (entry, operation) => setValueByPath(entry, operation.path, operation.value),
147
+ cloneValue(source)
148
+ );
149
+ const applyTranslationPatchToFillData = (fillData, sourceData, patch) => applyTranslationPatch(fillData ?? sourceData, patch);
150
+ const isEmptyValue = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
151
+ const isPlainObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
152
+ const MAX_BACKFILL_DEPTH = 32;
153
+ const backfillEmptyLeaves = (target, source, depth = 0) => {
154
+ if (depth > MAX_BACKFILL_DEPTH) {
155
+ return;
156
+ }
157
+ for (const [key, sourceValue] of Object.entries(source)) {
158
+ if (FORBIDDEN_SEGMENTS.has(key)) {
159
+ continue;
160
+ }
161
+ const targetValue = target[key];
162
+ if (isEmptyValue(targetValue)) {
163
+ target[key] = sourceValue;
164
+ } else if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
165
+ backfillEmptyLeaves(targetValue, sourceValue, depth + 1);
166
+ }
167
+ }
168
+ };
169
+ const mergeTranslationResult = ({
170
+ formValues,
171
+ fillData,
172
+ patch
173
+ }) => {
174
+ const base = cloneValue(formValues ?? {});
175
+ if (fillData) {
176
+ backfillEmptyLeaves(base, fillData);
177
+ }
178
+ return applyTranslationPatch(base, patch);
179
+ };
180
+ const hasTextContent = (value) => {
181
+ if (typeof value === "string") {
182
+ return value.trim().length > 0;
183
+ }
184
+ if (Array.isArray(value)) {
185
+ return value.some(hasTextContent);
186
+ }
187
+ if (value && typeof value === "object") {
188
+ return Object.values(value).some(hasTextContent);
189
+ }
190
+ return false;
191
+ };
192
+ const TRANSLATABLE_TYPES = /* @__PURE__ */ new Set(["string", "text", "richtext", "blocks", "json"]);
193
+ const EXCLUDED_FIELDS = /* @__PURE__ */ new Set([
194
+ "id",
195
+ "documentId",
196
+ "locale",
197
+ "createdAt",
198
+ "updatedAt",
199
+ "publishedAt",
200
+ "createdBy",
201
+ "updatedBy",
202
+ "slug",
203
+ "uid"
204
+ ]);
205
+ const isLocalized = (attribute) => attribute?.pluginOptions?.i18n?.localized !== false;
206
+ const componentHasText = (uid, components, depth = 0) => {
207
+ if (!uid || depth > 8) return false;
208
+ return Object.values(components[uid]?.attributes || {}).some(
209
+ (attribute) => TRANSLATABLE_TYPES.has(attribute?.type) || attribute?.type === "component" && componentHasText(attribute.component, components, depth + 1)
210
+ );
211
+ };
212
+ const listTranslatableFields = (contentType, components) => Object.entries(contentType?.attributes || {}).filter(([name, attribute]) => {
213
+ if (EXCLUDED_FIELDS.has(name) || !isLocalized(attribute)) return false;
214
+ if (TRANSLATABLE_TYPES.has(attribute?.type)) return true;
215
+ if (attribute?.type === "component") return componentHasText(attribute.component, components);
216
+ return attribute?.type === "dynamiczone";
217
+ }).map(([name]) => name);
218
+ const STATUS_BADGE = {
219
+ pending: {
220
+ background: "neutral150",
221
+ color: "neutral600",
222
+ id: "diff.field_status.pending",
223
+ defaultMessage: "Waiting"
224
+ },
225
+ translating: {
226
+ background: "primary100",
227
+ color: "primary700",
228
+ id: "diff.field_status.translating",
229
+ defaultMessage: "Translating…"
230
+ },
231
+ done: {
232
+ background: "success100",
233
+ color: "success700",
234
+ id: "diff.field_status.translated",
235
+ defaultMessage: "Translated"
236
+ },
237
+ failed: {
238
+ background: "danger100",
239
+ color: "danger700",
240
+ id: "diff.field_status.failed",
241
+ defaultMessage: "Failed"
242
+ },
243
+ unchanged: {
244
+ background: "neutral150",
245
+ color: "neutral600",
246
+ id: "diff.field_status.unchanged",
247
+ defaultMessage: "Unchanged"
248
+ },
249
+ skipped: {
250
+ background: "neutral150",
251
+ color: "neutral600",
252
+ id: "diff.field_status.skipped",
253
+ defaultMessage: "Skipped"
254
+ }
255
+ };
256
+ const paneTextStyle = {
257
+ whiteSpace: "pre-wrap",
258
+ wordBreak: "break-word",
259
+ fontSize: "13px",
260
+ lineHeight: 1.6
261
+ };
262
+ const INVARIANT_PATTERNS = [
263
+ /```[\s\S]*?```/g,
264
+ /`[^`\n]+`/g,
265
+ /https?:\/\/[^\s)"'<>\]]+/g,
266
+ /<\/?[a-zA-Z][^>]*>/g
267
+ ];
268
+ const countInvariants = (text) => {
269
+ const counts = /* @__PURE__ */ new Map();
270
+ for (const pattern of INVARIANT_PATTERNS) {
271
+ for (const token of text.match(pattern) ?? []) {
272
+ counts.set(token, (counts.get(token) ?? 0) + 1);
273
+ }
274
+ }
275
+ return counts;
276
+ };
277
+ const missingInvariants = (original, translated) => {
278
+ const source = countInvariants(original);
279
+ const target = countInvariants(translated);
280
+ const missing = [];
281
+ for (const [token, count] of source) {
282
+ if ((target.get(token) ?? 0) < count) {
283
+ missing.push(token);
284
+ }
285
+ }
286
+ return missing;
287
+ };
288
+ const untranslatedSegments = (original, translated) => {
289
+ const segments = original.split(/\n{2,}/).map((segment) => segment.trim()).filter(
290
+ (segment) => segment.length > 30 && /\p{L}/u.test(segment) && !segment.startsWith("```") && !segment.startsWith("<") && !/^https?:\/\//.test(segment)
291
+ );
292
+ return segments.filter((segment) => translated.includes(segment));
293
+ };
294
+ const estimateRows = (text) => {
295
+ const lines = text.split("\n").reduce((total, line) => {
296
+ const wide = line.match(/[ᄀ-₩]/g)?.length ?? 0;
297
+ return total + Math.max(1, Math.ceil((line.length + wide) / 80));
298
+ }, 0);
299
+ return Math.max(3, lines + 1);
300
+ };
301
+ const VIEW_MODES = [
302
+ {
303
+ mode: "split",
304
+ id: "diff.view.split",
305
+ defaultMessage: "Compare",
306
+ hintId: "diff.view.split.hint",
307
+ hint: "Source text and translation side by side, with inline editing"
308
+ },
309
+ {
310
+ mode: "preview",
311
+ id: "diff.view.preview",
312
+ defaultMessage: "Preview",
313
+ hintId: "diff.view.preview.hint",
314
+ hint: "Translation only, as it will read after applying"
315
+ },
316
+ {
317
+ mode: "diff",
318
+ id: "diff.view.diff",
319
+ defaultMessage: "Changes",
320
+ hintId: "diff.view.diff.hint",
321
+ hint: "What will change compared to the current content of this locale"
322
+ }
323
+ ];
324
+ const diffLines = (oldText, newText) => {
325
+ const a = oldText ? oldText.split("\n") : [];
326
+ const b = newText ? newText.split("\n") : [];
327
+ const m = a.length;
328
+ const n = b.length;
329
+ if (m * n > 25e4) return null;
330
+ const dp = Array.from({ length: m + 1 }, () => new Uint32Array(n + 1));
331
+ for (let i2 = m - 1; i2 >= 0; i2 -= 1) {
332
+ for (let j2 = n - 1; j2 >= 0; j2 -= 1) {
333
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
334
+ }
335
+ }
336
+ const out = [];
337
+ let i = 0;
338
+ let j = 0;
339
+ while (i < m && j < n) {
340
+ if (a[i] === b[j]) {
341
+ out.push({ type: "same", text: a[i] });
342
+ i += 1;
343
+ j += 1;
344
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
345
+ out.push({ type: "del", text: a[i] });
346
+ i += 1;
347
+ } else {
348
+ out.push({ type: "add", text: b[j] });
349
+ j += 1;
350
+ }
351
+ }
352
+ while (i < m) out.push({ type: "del", text: a[(i += 1) - 1] });
353
+ while (j < n) out.push({ type: "add", text: b[(j += 1) - 1] });
354
+ return out;
355
+ };
356
+ const DiffPane = ({ label, oldText, newText }) => {
357
+ const theme = styledComponents.useTheme();
358
+ const lines = react.useMemo(() => diffLines(oldText, newText), [oldText, newText]);
359
+ if (!lines) {
360
+ return /* @__PURE__ */ jsxRuntime.jsx(Pane, { label, text: newText });
361
+ }
362
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, padding: 4, style: { minWidth: 0 }, children: [
363
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: label }),
364
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: paneTextStyle, children: lines.map((line, index2) => /* @__PURE__ */ jsxRuntime.jsx(
365
+ "div",
366
+ {
367
+ style: line.type === "add" ? { background: theme.colors.success100, color: theme.colors.success700 } : line.type === "del" ? {
368
+ background: theme.colors.danger100,
369
+ color: theme.colors.danger700,
370
+ textDecoration: "line-through"
371
+ } : void 0,
372
+ children: line.text || " "
373
+ },
374
+ index2
375
+ )) })
376
+ ] });
377
+ };
378
+ const Pane = ({ label, text, muted }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, padding: 4, style: { minWidth: 0 }, children: [
379
+ label ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: label }) : null,
380
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "div", textColor: muted ? "neutral600" : "neutral800", style: paneTextStyle, children: text })
381
+ ] });
382
+ const FieldCard = ({
383
+ entry,
384
+ isStreaming,
385
+ viewMode,
386
+ sourceLabel,
387
+ targetLabel,
388
+ currentLabel,
389
+ onToggleField,
390
+ onEditField
391
+ }) => {
392
+ const { formatMessage } = reactIntl.useIntl();
393
+ const theme = styledComponents.useTheme();
394
+ const [folded, setFolded] = react.useState(false);
395
+ const divider = `1px solid ${theme.colors.neutral200}`;
396
+ const reviewing = entry.changed && !isStreaming;
397
+ const skipped = reviewing && !entry.included;
398
+ const autoCollapsed = isStreaming ? !entry.translated : !entry.changed || skipped;
399
+ const collapsed = autoCollapsed || folded;
400
+ const editing = reviewing && viewMode === "split" && entry.included && entry.editable && Boolean(onEditField);
401
+ const hasCurrent = Boolean(entry.current && entry.current !== entry.translated);
402
+ const missing = reviewing && !skipped ? missingInvariants(entry.original, entry.translated) : [];
403
+ const untranslated = reviewing && !skipped ? untranslatedSegments(entry.original, entry.translated) : [];
404
+ const badge = entry.status ? STATUS_BADGE[entry.status] : skipped ? STATUS_BADGE.skipped : STATUS_BADGE[entry.changed ? "done" : "unchanged"];
405
+ return /* @__PURE__ */ jsxRuntime.jsxs(
406
+ designSystem.Box,
407
+ {
408
+ background: "neutral0",
409
+ hasRadius: true,
410
+ "data-diff-card": entry.key,
411
+ style: { border: divider, overflow: "hidden", width: "100%" },
412
+ children: [
413
+ /* @__PURE__ */ jsxRuntime.jsxs(
414
+ designSystem.Flex,
415
+ {
416
+ justifyContent: "space-between",
417
+ alignItems: "center",
418
+ gap: 2,
419
+ paddingTop: 2,
420
+ paddingBottom: 2,
421
+ paddingLeft: 4,
422
+ paddingRight: 4,
423
+ background: "neutral100",
424
+ style: { borderBottom: collapsed ? void 0 : divider },
425
+ children: [
426
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "baseline", style: { minWidth: 0 }, children: [
427
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", ellipsis: true, children: entry.key }),
428
+ !collapsed ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: formatMessage(
429
+ {
430
+ id: getTranslation("diff.chars"),
431
+ defaultMessage: "{originalChars} → {translatedChars} chars"
432
+ },
433
+ {
434
+ originalChars: entry.original.length.toLocaleString(),
435
+ translatedChars: entry.translated.length.toLocaleString()
436
+ }
437
+ ) }) : null
438
+ ] }),
439
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", children: [
440
+ reviewing && hasCurrent ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: formatMessage({
441
+ id: getTranslation("diff.overwrite"),
442
+ defaultMessage: "Overwrites existing"
443
+ }) }) : null,
444
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: badge.background, textColor: badge.color, children: formatMessage({ id: getTranslation(badge.id), defaultMessage: badge.defaultMessage }) }),
445
+ reviewing && onToggleField ? /* @__PURE__ */ jsxRuntime.jsx(
446
+ designSystem.Checkbox,
447
+ {
448
+ checked: entry.included,
449
+ onCheckedChange: (checked) => onToggleField(entry.key, checked === true),
450
+ children: formatMessage({
451
+ id: getTranslation("diff.use_translation"),
452
+ defaultMessage: "Use translation"
453
+ })
454
+ }
455
+ ) : null,
456
+ !autoCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
457
+ designSystem.IconButton,
458
+ {
459
+ variant: "ghost",
460
+ label: formatMessage(
461
+ folded ? { id: getTranslation("diff.expand"), defaultMessage: "Expand" } : { id: getTranslation("diff.collapse"), defaultMessage: "Collapse" }
462
+ ),
463
+ onClick: () => setFolded((previous) => !previous),
464
+ children: folded ? /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronDown, {}) : /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronUp, {})
465
+ }
466
+ ) : null
467
+ ] })
468
+ ]
469
+ }
470
+ ),
471
+ collapsed ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
472
+ missing.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
473
+ designSystem.Box,
474
+ {
475
+ background: "danger100",
476
+ paddingTop: 2,
477
+ paddingBottom: 2,
478
+ paddingLeft: 4,
479
+ paddingRight: 4,
480
+ style: { borderBottom: divider },
481
+ children: [
482
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: formatMessage(
483
+ {
484
+ id: getTranslation("diff.invariants.warning"),
485
+ defaultMessage: "{count} URL/code/tag elements were altered or dropped by the translation:"
486
+ },
487
+ { count: missing.length }
488
+ ) }),
489
+ missing.slice(0, 5).map((token) => /* @__PURE__ */ jsxRuntime.jsx(
490
+ designSystem.Typography,
491
+ {
492
+ tag: "div",
493
+ variant: "pi",
494
+ textColor: "danger700",
495
+ style: { ...paneTextStyle, fontFamily: "monospace" },
496
+ children: token.length > 160 ? `${token.slice(0, 160)}…` : token
497
+ },
498
+ token
499
+ ))
500
+ ]
501
+ }
502
+ ) : null,
503
+ untranslated.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
504
+ designSystem.Box,
505
+ {
506
+ background: "warning100",
507
+ paddingTop: 2,
508
+ paddingBottom: 2,
509
+ paddingLeft: 4,
510
+ paddingRight: 4,
511
+ style: { borderBottom: divider },
512
+ children: [
513
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "warning700", fontWeight: "bold", children: formatMessage(
514
+ {
515
+ id: getTranslation("diff.untranslated.warning"),
516
+ defaultMessage: "{count} paragraph(s) appear untranslated:"
517
+ },
518
+ { count: untranslated.length }
519
+ ) }),
520
+ untranslated.slice(0, 3).map((segment) => /* @__PURE__ */ jsxRuntime.jsx(
521
+ designSystem.Typography,
522
+ {
523
+ tag: "div",
524
+ variant: "pi",
525
+ textColor: "warning700",
526
+ style: paneTextStyle,
527
+ children: segment.length > 160 ? `${segment.slice(0, 160)}…` : segment
528
+ },
529
+ segment.slice(0, 40)
530
+ ))
531
+ ]
532
+ }
533
+ ) : null,
534
+ isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(Pane, { label: targetLabel, text: entry.translated }) : viewMode === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(Pane, { label: targetLabel, text: entry.translated }) : viewMode === "diff" ? /* @__PURE__ */ jsxRuntime.jsx(
535
+ DiffPane,
536
+ {
537
+ label: hasCurrent ? `${currentLabel} → ${targetLabel}` : targetLabel,
538
+ oldText: entry.current ?? "",
539
+ newText: entry.translated
540
+ }
541
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr" }, children: [
542
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { borderRight: divider, minWidth: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Pane, { label: sourceLabel, text: entry.original, muted: true }) }),
543
+ editing ? /* @__PURE__ */ jsxRuntime.jsxs(
544
+ designSystem.Flex,
545
+ {
546
+ direction: "column",
547
+ alignItems: "stretch",
548
+ gap: 2,
549
+ padding: 4,
550
+ style: { minWidth: 0 },
551
+ children: [
552
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: targetLabel }),
553
+ /* @__PURE__ */ jsxRuntime.jsx(
554
+ designSystem.Textarea,
555
+ {
556
+ value: entry.translated,
557
+ rows: estimateRows(entry.translated),
558
+ style: { fieldSizing: "content", overflow: "hidden" },
559
+ onChange: (event) => onEditField(entry.key, event.target.value)
560
+ }
561
+ )
562
+ ]
563
+ }
564
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Pane, { label: targetLabel, text: entry.translated })
565
+ ] })
566
+ ] })
567
+ ]
568
+ }
569
+ );
570
+ };
571
+ const DiffView = ({
572
+ entries,
573
+ isStreaming,
574
+ viewMode,
575
+ sourceLocale,
576
+ targetLocale,
577
+ onToggleField,
578
+ onToggleAll,
579
+ onEditField
580
+ }) => {
581
+ const { formatMessage } = reactIntl.useIntl();
582
+ const changed = entries.filter(
583
+ (entry) => isStreaming ? entry.status === "done" : entry.changed
584
+ ).length;
585
+ const skipped = entries.filter((entry) => entry.changed && !entry.included).length;
586
+ const unchanged = entries.length - changed;
587
+ const sourceLabel = `${formatMessage({
588
+ id: getTranslation("diff.pane.source"),
589
+ defaultMessage: "Source"
590
+ })}${sourceLocale ? ` (${sourceLocale})` : ""}`;
591
+ const targetLabel = `${formatMessage({
592
+ id: getTranslation("diff.pane.translation"),
593
+ defaultMessage: "Translation"
594
+ })}${targetLocale ? ` (${targetLocale})` : ""}`;
595
+ const currentLabel = `${formatMessage({
596
+ id: getTranslation("diff.pane.current"),
597
+ defaultMessage: "Current"
598
+ })}${targetLocale ? ` (${targetLocale})` : ""}`;
599
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
600
+ /* @__PURE__ */ jsxRuntime.jsx(
601
+ designSystem.Box,
602
+ {
603
+ background: "neutral0",
604
+ paddingTop: 1,
605
+ paddingBottom: 1,
606
+ style: { position: "sticky", top: 0, zIndex: 1 },
607
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", justifyContent: "space-between", children: [
608
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", children: [
609
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "success600", fontWeight: "bold", children: formatMessage(
610
+ {
611
+ id: getTranslation("diff.summary.translated"),
612
+ defaultMessage: "{count} translated"
613
+ },
614
+ { count: changed }
615
+ ) }),
616
+ skipped > 0 && !isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: formatMessage(
617
+ {
618
+ id: getTranslation("diff.summary.skipped"),
619
+ defaultMessage: "{count} skipped"
620
+ },
621
+ { count: skipped }
622
+ ) }) : null,
623
+ unchanged > 0 && !isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: formatMessage(
624
+ {
625
+ id: getTranslation("diff.summary.unchanged"),
626
+ defaultMessage: "{count} unchanged"
627
+ },
628
+ { count: unchanged }
629
+ ) }) : null
630
+ ] }),
631
+ !isStreaming && onToggleAll && changed > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
632
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => onToggleAll(true), children: formatMessage({ id: getTranslation("diff.use_all"), defaultMessage: "Use all" }) }),
633
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => onToggleAll(false), children: formatMessage({
634
+ id: getTranslation("diff.skip_all"),
635
+ defaultMessage: "Skip all"
636
+ }) })
637
+ ] }) : null
638
+ ] })
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 3, children: entries.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
642
+ FieldCard,
643
+ {
644
+ entry,
645
+ isStreaming,
646
+ viewMode,
647
+ sourceLabel,
648
+ targetLabel,
649
+ currentLabel,
650
+ onToggleField,
651
+ onEditField
652
+ },
653
+ entry.key
654
+ )) })
655
+ ] });
656
+ };
657
+ const ProgressBar = ({ current, total, label }) => {
658
+ const percent = total > 0 ? Math.round(current / total * 100) : 0;
659
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingTop: 1, children: [
660
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral200", hasRadius: true, style: { height: "4px", overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx(
661
+ designSystem.Box,
662
+ {
663
+ background: "primary600",
664
+ style: {
665
+ height: "100%",
666
+ width: `${percent}%`,
667
+ transition: "width 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
668
+ borderRadius: "4px"
669
+ }
670
+ }
671
+ ) }),
672
+ label && /* @__PURE__ */ jsxRuntime.jsxs(
673
+ designSystem.Typography,
674
+ {
675
+ variant: "pi",
676
+ textColor: "neutral500",
677
+ style: { marginTop: "4px", display: "block" },
678
+ children: [
679
+ label,
680
+ " - ",
681
+ percent,
682
+ "%"
683
+ ]
684
+ }
685
+ )
686
+ ] });
687
+ };
688
+ const TranslationModal = ({
689
+ isOpen,
690
+ isStreaming,
691
+ sourceLocale,
692
+ targetLocale,
693
+ diffEntries,
694
+ progress,
695
+ currentField,
696
+ failedFields,
697
+ elapsedTime,
698
+ onAccept,
699
+ onCancel,
700
+ onRetryFailed,
701
+ onToggleField,
702
+ onToggleAll,
703
+ onEditField
704
+ }) => {
705
+ const { formatMessage } = reactIntl.useIntl();
706
+ const contentRef = react.useRef(null);
707
+ const [fullScreen, setFullScreen] = react.useState(false);
708
+ const [follow, setFollow] = react.useState(true);
709
+ const [viewMode, setViewMode] = react.useState("split");
710
+ const applyCount = diffEntries.filter((entry) => entry.changed && entry.included).length;
711
+ const canAccept = !isStreaming && applyCount > 0;
712
+ const streamedChars = diffEntries.reduce((total, entry) => total + entry.translated.length, 0);
713
+ react.useEffect(() => {
714
+ if (isStreaming) setFollow(true);
715
+ }, [isStreaming]);
716
+ react.useEffect(() => {
717
+ if (!isStreaming || !follow || !contentRef.current || !currentField) return;
718
+ const container = contentRef.current;
719
+ const card = container.querySelector(`[data-diff-card="${CSS.escape(currentField)}"]`);
720
+ if (!card) return;
721
+ const cardRect = card.getBoundingClientRect();
722
+ const containerRect = container.getBoundingClientRect();
723
+ const delta = cardRect.bottom - containerRect.bottom + 16;
724
+ if (delta > 0 || cardRect.bottom < containerRect.top) {
725
+ container.scrollTop += delta;
726
+ }
727
+ }, [isStreaming, follow, currentField, streamedChars]);
728
+ const stopFollowing = () => {
729
+ if (isStreaming) setFollow(false);
730
+ };
731
+ const requestClose = () => {
732
+ if (isStreaming && !window.confirm(
733
+ formatMessage({
734
+ id: getTranslation("modal.confirm_cancel"),
735
+ defaultMessage: "Translation is in progress. Cancel and discard?"
736
+ })
737
+ )) {
738
+ return;
739
+ }
740
+ onCancel();
741
+ };
742
+ return /* @__PURE__ */ jsxRuntime.jsx(
743
+ designSystem.Modal.Root,
744
+ {
745
+ open: isOpen,
746
+ onOpenChange: (open) => {
747
+ if (!open) requestClose();
748
+ },
749
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
750
+ designSystem.Modal.Content,
751
+ {
752
+ style: fullScreen ? {
753
+ maxWidth: "100vw",
754
+ width: "100vw",
755
+ height: "100vh",
756
+ maxHeight: "100vh",
757
+ borderRadius: 0
758
+ } : { maxWidth: "min(1280px, 96vw)", width: "96vw" },
759
+ children: [
760
+ /* @__PURE__ */ jsxRuntime.jsx(
761
+ designSystem.Modal.Header,
762
+ {
763
+ closeLabel: formatMessage({ id: getTranslation("modal.close"), defaultMessage: "Close" }),
764
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
765
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: formatMessage({
766
+ id: getTranslation("modal.title"),
767
+ defaultMessage: "Translation Review"
768
+ }) }),
769
+ /* @__PURE__ */ jsxRuntime.jsx(
770
+ designSystem.IconButton,
771
+ {
772
+ label: formatMessage(
773
+ fullScreen ? {
774
+ id: getTranslation("modal.exit_full_screen"),
775
+ defaultMessage: "Exit full screen"
776
+ } : { id: getTranslation("modal.full_screen"), defaultMessage: "Full screen" }
777
+ ),
778
+ variant: "ghost",
779
+ onClick: () => setFullScreen((previous) => !previous),
780
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Expand, {})
781
+ }
782
+ ),
783
+ !isStreaming && diffEntries.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 1, paddingLeft: 2, children: VIEW_MODES.map(({ mode, id, defaultMessage, hintId, hint }) => /* @__PURE__ */ jsxRuntime.jsx(
784
+ designSystem.Tooltip,
785
+ {
786
+ label: formatMessage({ id: getTranslation(hintId), defaultMessage: hint }),
787
+ children: /* @__PURE__ */ jsxRuntime.jsx(
788
+ designSystem.Button,
789
+ {
790
+ variant: viewMode === mode ? "secondary" : "ghost",
791
+ size: "S",
792
+ onClick: () => setViewMode(mode),
793
+ children: formatMessage({ id: getTranslation(id), defaultMessage })
794
+ }
795
+ )
796
+ },
797
+ mode
798
+ )) }) : null
799
+ ] })
800
+ }
801
+ ),
802
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
803
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", gap: 4, alignItems: "center", children: [
804
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
805
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral600", children: sourceLocale }),
806
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral400", children: "→" }),
807
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", textColor: "primary600", children: targetLocale })
808
+ ] }),
809
+ elapsedTime > 0 && !isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: formatMessage(
810
+ {
811
+ id: getTranslation("modal.completed_in"),
812
+ defaultMessage: "Completed in {seconds}s"
813
+ },
814
+ { seconds: elapsedTime }
815
+ ) }) : null,
816
+ isStreaming && !follow ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => setFollow(true), children: formatMessage({
817
+ id: getTranslation("modal.follow"),
818
+ defaultMessage: "Follow latest"
819
+ }) }) : null
820
+ ] }),
821
+ isStreaming && progress ? /* @__PURE__ */ jsxRuntime.jsx(
822
+ ProgressBar,
823
+ {
824
+ current: progress.current,
825
+ total: progress.total,
826
+ label: currentField ? `${currentField} (${progress.current}/${progress.total})` : `${progress.current}/${progress.total}`
827
+ }
828
+ ) : null,
829
+ failedFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: formatMessage(
830
+ {
831
+ id: getTranslation("modal.failed_fields"),
832
+ defaultMessage: "{count} field(s) failed: {fields}"
833
+ },
834
+ { count: failedFields.length, fields: failedFields.join(", ") }
835
+ ) }) : null,
836
+ /* @__PURE__ */ jsxRuntime.jsx(
837
+ designSystem.Box,
838
+ {
839
+ ref: contentRef,
840
+ onWheel: stopFollowing,
841
+ onTouchMove: stopFollowing,
842
+ style: {
843
+ maxHeight: fullScreen ? "calc(100vh - 250px)" : "min(64vh, 680px)",
844
+ overflowY: "auto"
845
+ },
846
+ children: diffEntries.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
847
+ DiffView,
848
+ {
849
+ entries: diffEntries,
850
+ isStreaming,
851
+ viewMode,
852
+ sourceLocale,
853
+ targetLocale,
854
+ onToggleField,
855
+ onToggleAll,
856
+ onEditField
857
+ }
858
+ ) : isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", textColor: "neutral500", children: formatMessage({
859
+ id: getTranslation("modal.waiting"),
860
+ defaultMessage: "Starting translation..."
861
+ }) }) }) : null
862
+ }
863
+ )
864
+ ] }) }),
865
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { justifyContent: "space-between", children: [
866
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: requestClose, children: isStreaming ? formatMessage({
867
+ id: getTranslation("actions.cancel_streaming"),
868
+ defaultMessage: "Cancel"
869
+ }) : formatMessage({
870
+ id: getTranslation("actions.cancel"),
871
+ defaultMessage: "Cancel"
872
+ }) }),
873
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
874
+ !isStreaming && failedFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "danger-light", onClick: onRetryFailed, children: formatMessage(
875
+ {
876
+ id: getTranslation("actions.retry_failed"),
877
+ defaultMessage: "Retry failed ({count})"
878
+ },
879
+ { count: failedFields.length }
880
+ ) }) : null,
881
+ canAccept ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "success", onClick: onAccept, children: formatMessage(
882
+ {
883
+ id: getTranslation("actions.accept"),
884
+ defaultMessage: "Accept Translation ({count})"
885
+ },
886
+ { count: applyCount }
887
+ ) }) : null
888
+ ] })
889
+ ] })
890
+ ]
891
+ }
892
+ )
893
+ }
894
+ );
895
+ };
896
+ const TranslationWorkflow = () => {
897
+ const [state, setState] = react.useState("idle");
898
+ const [progress, setProgress] = react.useState(null);
899
+ const [currentField, setCurrentField] = react.useState("");
900
+ const [streamingPreview, setStreamingPreview] = react.useState({});
901
+ const [translatedData, setTranslatedData] = react.useState(null);
902
+ const [sourceData, setSourceData] = react.useState(null);
903
+ const [failedUnits, setFailedUnits] = react.useState([]);
904
+ const [unitStatus, setUnitStatus] = react.useState({});
905
+ const [reviewPaths, setReviewPaths] = react.useState([]);
906
+ const [fillData, setFillData] = react.useState(null);
907
+ const [elapsedTime, setElapsedTime] = react.useState(0);
908
+ const [sourceLocale, setSourceLocale] = react.useState("");
909
+ const [availableLocales, setAvailableLocales] = react.useState([]);
910
+ const [dialogOpen, setDialogOpen] = react.useState(false);
911
+ const [dialogLocale, setDialogLocale] = react.useState("");
912
+ const [translatableFields, setTranslatableFields] = react.useState([]);
913
+ const [selectedFields, setSelectedFields] = react.useState(/* @__PURE__ */ new Set());
914
+ const [excludedFields, setExcludedFields] = react.useState(/* @__PURE__ */ new Set());
915
+ const [editedValues, setEditedValues] = react.useState({});
916
+ const abortRef = react.useRef(null);
917
+ const patchRef = react.useRef(null);
918
+ const reviewLabelsRef = react.useRef(/* @__PURE__ */ new Set());
919
+ const { formatMessage } = reactIntl.useIntl();
920
+ const { form, contentType, components, collectionType } = admin.unstable_useContentManagerContext();
921
+ const { toggleNotification } = admin.useNotification();
922
+ const { get } = admin.useFetchClient();
923
+ const setValues = admin.useForm("TranslateWithAI", (state2) => state2.setValues);
924
+ const isI18nEnabled = contentType?.pluginOptions?.i18n?.localized || false;
925
+ const { values } = form;
926
+ const currentLocale = getContentManagerLocale();
927
+ const addReviewPath = (path) => {
928
+ const label = pathToLabel(path);
929
+ if (reviewLabelsRef.current.has(label)) return;
930
+ reviewLabelsRef.current.add(label);
931
+ setReviewPaths((prev) => [...prev, path]);
932
+ };
933
+ const uniqueReviewPaths = (paths) => {
934
+ const labels = /* @__PURE__ */ new Set();
935
+ return paths.filter((path) => {
936
+ const label = pathToLabel(path);
937
+ if (labels.has(label)) {
938
+ return false;
939
+ }
940
+ labels.add(label);
941
+ return true;
942
+ });
943
+ };
944
+ const stringifyReviewValue = (value) => {
945
+ if (typeof value === "string") {
946
+ return value;
947
+ }
948
+ if (value === void 0 || value === null) {
949
+ return "";
950
+ }
951
+ return JSON.stringify(value, null, 2);
952
+ };
953
+ react.useEffect(() => {
954
+ return () => {
955
+ abortRef.current?.abort();
956
+ };
957
+ }, []);
958
+ react.useEffect(() => {
959
+ if (!isI18nEnabled) return;
960
+ const fetchLocales = async () => {
961
+ try {
962
+ const localeRes = await get("/i18n/locales");
963
+ const allLocales = (localeRes.data || []).map((l) => ({
964
+ code: l.code,
965
+ name: l.name || l.code,
966
+ isDefault: Boolean(l.isDefault),
967
+ status: void 0
968
+ }));
969
+ const contentTypeUid = contentType?.uid;
970
+ const documentId = getDocumentId();
971
+ if (contentTypeUid && documentId && documentId !== "create") {
972
+ try {
973
+ const docRes = await get(
974
+ `/content-manager/collection-types/${contentTypeUid}/${documentId}?locale=${currentLocale}`
975
+ );
976
+ const metaLocales = docRes.data?.meta?.availableLocales || [];
977
+ for (const ml of metaLocales) {
978
+ const match = allLocales.find((l) => l.code === ml.locale);
979
+ if (match) match.status = ml.status;
980
+ }
981
+ } catch {
982
+ }
983
+ }
984
+ setAvailableLocales(allLocales);
985
+ if (!sourceLocale) {
986
+ const defaultL = allLocales.find((l) => l.isDefault && l.code !== currentLocale);
987
+ const en = allLocales.find((l) => l.code === "en" && l.code !== currentLocale);
988
+ const anyWithContent = allLocales.find((l) => l.code !== currentLocale && l.status);
989
+ const fallback = allLocales.find((l) => l.code !== currentLocale);
990
+ const pick = defaultL || en || anyWithContent || fallback;
991
+ if (pick) setSourceLocale(pick.code);
992
+ }
993
+ } catch {
994
+ }
995
+ };
996
+ fetchLocales();
997
+ }, [isI18nEnabled, currentLocale]);
998
+ if (!isI18nEnabled) return null;
999
+ const getToken = () => {
1000
+ const fromStorage = localStorage.getItem("jwtToken");
1001
+ if (fromStorage) {
1002
+ try {
1003
+ return JSON.parse(fromStorage);
1004
+ } catch {
1005
+ return fromStorage;
1006
+ }
1007
+ }
1008
+ return document.cookie.match(/jwtToken=([^;]+)/)?.[1] ?? "";
1009
+ };
1010
+ const doSSEFetch = async (dataToSend, signal) => {
1011
+ const backendURL = window.strapi?.backendURL || "";
1012
+ const token = getToken();
1013
+ let response = await fetch(`${backendURL}/${PLUGIN_ID}/translate`, {
1014
+ method: "POST",
1015
+ headers: {
1016
+ "Content-Type": "application/json",
1017
+ Authorization: `Bearer ${token}`
1018
+ },
1019
+ body: JSON.stringify(dataToSend),
1020
+ signal
1021
+ });
1022
+ if (response.status === 401) {
1023
+ const refreshRes = await fetch(`${backendURL}/admin/access-token`, {
1024
+ method: "POST",
1025
+ credentials: "include",
1026
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
1027
+ signal
1028
+ });
1029
+ if (!refreshRes.ok) throw new Error("Authentication failed");
1030
+ const { data } = await refreshRes.json();
1031
+ if (!data?.token) throw new Error("Authentication failed");
1032
+ if (localStorage.getItem("jwtToken")) {
1033
+ localStorage.setItem("jwtToken", JSON.stringify(data.token));
1034
+ }
1035
+ response = await fetch(`${backendURL}/${PLUGIN_ID}/translate`, {
1036
+ method: "POST",
1037
+ headers: {
1038
+ "Content-Type": "application/json",
1039
+ Authorization: `Bearer ${data.token}`
1040
+ },
1041
+ body: JSON.stringify(dataToSend),
1042
+ signal
1043
+ });
1044
+ }
1045
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1046
+ return response;
1047
+ };
1048
+ const getDocumentId = () => {
1049
+ if (values?.documentId) return values.documentId;
1050
+ const pathParts = window.location.pathname.split("/").filter(Boolean);
1051
+ const cmIndex = pathParts.indexOf("collection-types");
1052
+ if (cmIndex >= 0 && pathParts[cmIndex + 2]) return pathParts[cmIndex + 2];
1053
+ return new URLSearchParams(window.location.search).get("documentId");
1054
+ };
1055
+ const getContentTypeUid = () => contentType?.uid;
1056
+ const getCollectionType = () => collectionType || (contentType?.kind === "singleType" ? "single-types" : "collection-types");
1057
+ const fetchFillData = async (locale) => {
1058
+ const contentTypeUid = getContentTypeUid();
1059
+ const documentId = getDocumentId();
1060
+ if (!documentId || !contentTypeUid || documentId === "create") {
1061
+ throw new Error("Cannot determine document ID. Please save the entry first.");
1062
+ }
1063
+ const response = await get(
1064
+ `/i18n/content-manager/get-fill-from-locale/${contentTypeUid}?documentId=${documentId}&sourceLocale=${locale}&targetLocale=${currentLocale}&collectionType=${getCollectionType()}`
1065
+ );
1066
+ if (!response.data?.data) {
1067
+ throw new Error(`No content found for locale "${locale}".`);
1068
+ }
1069
+ return response.data.data;
1070
+ };
1071
+ const fetchSourceContent = async (locale) => {
1072
+ const contentTypeUid = getContentTypeUid();
1073
+ const documentId = getDocumentId();
1074
+ if (!contentTypeUid || getCollectionType() !== "single-types" && !documentId) {
1075
+ throw new Error("Cannot determine document ID. Please save the entry first.");
1076
+ }
1077
+ const path = getCollectionType() === "single-types" ? `/content-manager/single-types/${contentTypeUid}?locale=${locale}` : `/content-manager/collection-types/${contentTypeUid}/${documentId}?locale=${locale}`;
1078
+ const response = await get(path);
1079
+ if (!response.data?.data) {
1080
+ throw new Error(`No content found for locale "${locale}".`);
1081
+ }
1082
+ return response.data.data;
1083
+ };
1084
+ const handleTranslateFrom = async (fromLocale, retryPaths) => {
1085
+ const effectiveSourceLocale = fromLocale || sourceLocale;
1086
+ if (!effectiveSourceLocale || effectiveSourceLocale === currentLocale) {
1087
+ toggleNotification({
1088
+ type: "warning",
1089
+ message: formatMessage({
1090
+ id: getTranslation("notification.no-source"),
1091
+ defaultMessage: "Please select a source language different from the current locale."
1092
+ })
1093
+ });
1094
+ return;
1095
+ }
1096
+ const sourceLocaleInfo = availableLocales.find((l) => l.code === effectiveSourceLocale);
1097
+ if (!sourceLocaleInfo?.status) {
1098
+ toggleNotification({
1099
+ type: "warning",
1100
+ message: formatMessage(
1101
+ {
1102
+ id: getTranslation("notification.no-source-content"),
1103
+ defaultMessage: 'No content found for source locale "{locale}".'
1104
+ },
1105
+ { locale: effectiveSourceLocale }
1106
+ )
1107
+ });
1108
+ return;
1109
+ }
1110
+ const abort = new AbortController();
1111
+ abortRef.current = abort;
1112
+ const isRetry = Boolean(retryPaths?.length && sourceData);
1113
+ try {
1114
+ setState("translating");
1115
+ setProgress(null);
1116
+ setCurrentField("");
1117
+ setFailedUnits([]);
1118
+ setElapsedTime(0);
1119
+ if (isRetry) {
1120
+ setStreamingPreview(translatedData ?? {});
1121
+ setTranslatedData(null);
1122
+ setUnitStatus((prev) => {
1123
+ const next = { ...prev };
1124
+ for (const path of retryPaths) delete next[pathToLabel(path)];
1125
+ return next;
1126
+ });
1127
+ } else {
1128
+ setStreamingPreview({});
1129
+ setTranslatedData(null);
1130
+ setUnitStatus({});
1131
+ setReviewPaths([]);
1132
+ setExcludedFields(/* @__PURE__ */ new Set());
1133
+ setEditedValues({});
1134
+ reviewLabelsRef.current = /* @__PURE__ */ new Set();
1135
+ patchRef.current = null;
1136
+ }
1137
+ const startTime = Date.now();
1138
+ let entryFillData = fillData;
1139
+ let sourceFields = sourceData;
1140
+ if (!isRetry || !sourceFields) {
1141
+ const [freshFill, freshSource] = await Promise.all([
1142
+ fetchFillData(effectiveSourceLocale).catch(() => null),
1143
+ fetchSourceContent(effectiveSourceLocale)
1144
+ ]);
1145
+ if (!hasTextContent(freshSource)) {
1146
+ throw new Error(`No content found in source locale "${effectiveSourceLocale}".`);
1147
+ }
1148
+ setSourceData({ ...freshSource });
1149
+ setFillData(freshFill);
1150
+ entryFillData = freshFill;
1151
+ sourceFields = freshSource;
1152
+ }
1153
+ if (!sourceFields) {
1154
+ throw new Error(`No content found in source locale "${effectiveSourceLocale}".`);
1155
+ }
1156
+ const include = retryPaths ?? (selectedFields.size > 0 && selectedFields.size < translatableFields.length ? [...selectedFields].map((field) => [field]) : void 0);
1157
+ const dataToSend = {
1158
+ contentType: contentType ?? {},
1159
+ entry: sourceFields,
1160
+ components: components ?? {},
1161
+ sourceLocale: effectiveSourceLocale,
1162
+ targetLocale: currentLocale,
1163
+ ...include ? { include } : {}
1164
+ };
1165
+ const response = await doSSEFetch(dataToSend, abort.signal);
1166
+ const accumulated = {};
1167
+ let summaryData = null;
1168
+ let completedUnits = 0;
1169
+ let totalUnits = 0;
1170
+ for await (const event of parseSSEStream(response)) {
1171
+ if (abort.signal.aborted) break;
1172
+ switch (event.type) {
1173
+ case "session_started":
1174
+ totalUnits = event.totalUnits || 0;
1175
+ setProgress({ current: 0, total: totalUnits });
1176
+ for (const unitPath of event.unitPaths ?? []) {
1177
+ addReviewPath(unitPath);
1178
+ }
1179
+ break;
1180
+ case "unit_started": {
1181
+ const startedPath = event.unit?.path;
1182
+ setCurrentField(pathToLabel(startedPath));
1183
+ if (startedPath) {
1184
+ setUnitStatus((prev) => ({ ...prev, [pathToLabel(startedPath)]: "translating" }));
1185
+ }
1186
+ break;
1187
+ }
1188
+ case "unit_delta":
1189
+ if (event.path && event.content != null) {
1190
+ addReviewPath(event.path);
1191
+ setStreamingPreview((prev) => ({
1192
+ ...setValueByPath(prev, event.path, event.content)
1193
+ }));
1194
+ }
1195
+ break;
1196
+ case "unit_retry_scheduled":
1197
+ if (event.path) {
1198
+ setCurrentField(
1199
+ formatMessage(
1200
+ {
1201
+ id: getTranslation("workflow.retrying"),
1202
+ defaultMessage: "{field} — retrying (attempt {attempt})"
1203
+ },
1204
+ { field: pathToLabel(event.path), attempt: event.attempt ?? 1 }
1205
+ )
1206
+ );
1207
+ }
1208
+ break;
1209
+ case "unit_fallback_used":
1210
+ if (event.path) {
1211
+ setCurrentField(
1212
+ formatMessage(
1213
+ {
1214
+ id: getTranslation("workflow.fallback"),
1215
+ defaultMessage: "{field} — using fallback model"
1216
+ },
1217
+ { field: pathToLabel(event.path) }
1218
+ )
1219
+ );
1220
+ }
1221
+ break;
1222
+ case "unit_completed":
1223
+ completedUnits += 1;
1224
+ if (event.path && event.translatedText != null) {
1225
+ addReviewPath(event.path);
1226
+ setStreamingPreview(
1227
+ (prev) => setValueByPath(prev, event.path, event.translatedText)
1228
+ );
1229
+ setUnitStatus((prev) => ({ ...prev, [pathToLabel(event.path)]: "done" }));
1230
+ }
1231
+ if (totalUnits > 0) {
1232
+ setProgress({ current: completedUnits, total: totalUnits });
1233
+ }
1234
+ break;
1235
+ case "unit_failed": {
1236
+ completedUnits += 1;
1237
+ const failedPath = event.path;
1238
+ if (failedPath) {
1239
+ setFailedUnits((prev) => [...prev, failedPath]);
1240
+ setUnitStatus((prev) => ({ ...prev, [pathToLabel(failedPath)]: "failed" }));
1241
+ }
1242
+ if (totalUnits > 0) {
1243
+ setProgress({ current: completedUnits, total: totalUnits });
1244
+ }
1245
+ break;
1246
+ }
1247
+ case "session_completed": {
1248
+ summaryData = event;
1249
+ if (event.patch) {
1250
+ const newLabels = new Set(
1251
+ event.patch.operations.map((operation) => pathToLabel(operation.path))
1252
+ );
1253
+ const previousOperations = isRetry && patchRef.current ? patchRef.current.operations.filter(
1254
+ (operation) => !newLabels.has(pathToLabel(operation.path))
1255
+ ) : [];
1256
+ const mergedPatch = {
1257
+ operations: [...previousOperations, ...event.patch.operations]
1258
+ };
1259
+ patchRef.current = mergedPatch;
1260
+ const finalPaths = uniqueReviewPaths(
1261
+ mergedPatch.operations.map((operation) => operation.path)
1262
+ );
1263
+ reviewLabelsRef.current = new Set(finalPaths.map(pathToLabel));
1264
+ setReviewPaths(finalPaths);
1265
+ Object.assign(
1266
+ accumulated,
1267
+ applyTranslationPatchToFillData(entryFillData, sourceFields, mergedPatch)
1268
+ );
1269
+ }
1270
+ break;
1271
+ }
1272
+ case "session_failed":
1273
+ throw new Error(event.error || "Translation failed");
1274
+ }
1275
+ }
1276
+ if (abort.signal.aborted) {
1277
+ return;
1278
+ }
1279
+ setTranslatedData(accumulated);
1280
+ setStreamingPreview({});
1281
+ setElapsedTime(Math.round((Date.now() - startTime) / 1e3));
1282
+ if ((summaryData?.summary?.failedUnitIds?.length ?? 0) > 0) {
1283
+ setState("partial");
1284
+ } else if (Object.keys(accumulated).length > 0) {
1285
+ setState("preview");
1286
+ } else {
1287
+ throw new Error("No translation data received");
1288
+ }
1289
+ } catch (err) {
1290
+ if (!abort.signal.aborted) {
1291
+ setState("error");
1292
+ toggleNotification({
1293
+ type: "danger",
1294
+ message: formatMessage(
1295
+ {
1296
+ id: getTranslation("notification.error"),
1297
+ defaultMessage: "Translation failed: {error}"
1298
+ },
1299
+ { error: err.message }
1300
+ )
1301
+ });
1302
+ }
1303
+ }
1304
+ };
1305
+ const buildEffectivePatch = (patch) => ({
1306
+ operations: patch.operations.filter((operation) => !excludedFields.has(pathToLabel(operation.path))).map((operation) => {
1307
+ const edited = editedValues[pathToLabel(operation.path)];
1308
+ return edited !== void 0 && typeof operation.value === "string" ? { ...operation, value: edited } : operation;
1309
+ })
1310
+ });
1311
+ const handleAccept = () => {
1312
+ const patch = patchRef.current;
1313
+ const merged = patch ? mergeTranslationResult({ formValues: values, fillData, patch: buildEffectivePatch(patch) }) : { ...fillData || {}, ...translatedData || {} };
1314
+ if (Object.keys(merged).length > 0) {
1315
+ setValues(merged);
1316
+ }
1317
+ setState("success");
1318
+ setTranslatedData(null);
1319
+ setSourceData(null);
1320
+ setFillData(null);
1321
+ setStreamingPreview({});
1322
+ setReviewPaths([]);
1323
+ setExcludedFields(/* @__PURE__ */ new Set());
1324
+ setEditedValues({});
1325
+ setFailedUnits([]);
1326
+ setUnitStatus({});
1327
+ patchRef.current = null;
1328
+ reviewLabelsRef.current = /* @__PURE__ */ new Set();
1329
+ toggleNotification({
1330
+ type: "success",
1331
+ message: formatMessage({
1332
+ id: getTranslation("notification.success"),
1333
+ defaultMessage: "Translation applied - remember to save."
1334
+ })
1335
+ });
1336
+ setTimeout(() => setState((s) => s === "success" ? "idle" : s), 5e3);
1337
+ };
1338
+ const handleCancel = () => {
1339
+ if (abortRef.current) abortRef.current.abort();
1340
+ setState("idle");
1341
+ setTranslatedData(null);
1342
+ setSourceData(null);
1343
+ setFillData(null);
1344
+ setStreamingPreview({});
1345
+ setReviewPaths([]);
1346
+ setExcludedFields(/* @__PURE__ */ new Set());
1347
+ setEditedValues({});
1348
+ patchRef.current = null;
1349
+ reviewLabelsRef.current = /* @__PURE__ */ new Set();
1350
+ setProgress(null);
1351
+ setCurrentField("");
1352
+ setFailedUnits([]);
1353
+ setUnitStatus({});
1354
+ };
1355
+ const buildDiffEntries = () => {
1356
+ const entries = [];
1357
+ const dataSource = translatedData || streamingPreview;
1358
+ if (!sourceData) return entries;
1359
+ const paths = reviewPaths.length > 0 ? reviewPaths : Object.keys(dataSource).map((key) => [key]);
1360
+ for (const path of paths) {
1361
+ const label = pathToLabel(path);
1362
+ const translatedValue = getValueByPath(dataSource, path);
1363
+ const originalValue = getValueByPath(sourceData, path);
1364
+ const tStr = stringifyReviewValue(translatedValue);
1365
+ const oStr = stringifyReviewValue(originalValue);
1366
+ if (!oStr && !tStr) continue;
1367
+ const displayed = editedValues[label] ?? tStr;
1368
+ entries.push({
1369
+ key: label,
1370
+ original: oStr || "",
1371
+ translated: displayed || "",
1372
+ current: stringifyReviewValue(getValueByPath(values ?? {}, path)),
1373
+ changed: oStr !== tStr,
1374
+ editable: typeof translatedValue === "string",
1375
+ included: !excludedFields.has(label),
1376
+ status: state === "translating" ? unitStatus[label] ?? "pending" : void 0
1377
+ });
1378
+ }
1379
+ return entries;
1380
+ };
1381
+ const handleToggleAll = (included) => {
1382
+ setExcludedFields(
1383
+ included ? /* @__PURE__ */ new Set() : new Set(
1384
+ buildDiffEntries().filter((entry) => entry.changed).map((entry) => entry.key)
1385
+ )
1386
+ );
1387
+ };
1388
+ const handleToggleField = (key, included) => {
1389
+ setExcludedFields((prev) => {
1390
+ const next = new Set(prev);
1391
+ if (included) {
1392
+ next.delete(key);
1393
+ } else {
1394
+ next.add(key);
1395
+ }
1396
+ return next;
1397
+ });
1398
+ };
1399
+ const handleEditField = (key, value) => {
1400
+ setEditedValues((prev) => ({ ...prev, [key]: value }));
1401
+ };
1402
+ const handleRetryFailed = () => {
1403
+ if (failedUnits.length === 0) return;
1404
+ handleTranslateFrom(sourceLocale, failedUnits);
1405
+ };
1406
+ const isModalOpen = state === "translating" || state === "preview" || state === "partial";
1407
+ const otherLocales = availableLocales.filter((l) => l.code !== currentLocale);
1408
+ const openDialog = () => {
1409
+ const fields = listTranslatableFields(contentType, components ?? {});
1410
+ setTranslatableFields(fields);
1411
+ setSelectedFields(new Set(fields));
1412
+ const defaultL = otherLocales.find((l) => l.isDefault && l.status);
1413
+ const en = otherLocales.find((l) => l.code === "en" && l.status);
1414
+ const anyWithContent = otherLocales.find((l) => l.status);
1415
+ setDialogLocale((defaultL || en || anyWithContent || otherLocales[0])?.code || "");
1416
+ setDialogOpen(true);
1417
+ };
1418
+ const confirmTranslate = () => {
1419
+ if (!dialogLocale || selectedFields.size === 0) return;
1420
+ const locale = otherLocales.find((l) => l.code === dialogLocale);
1421
+ if (!locale?.status) return;
1422
+ setSourceLocale(dialogLocale);
1423
+ setDialogOpen(false);
1424
+ handleTranslateFrom(dialogLocale);
1425
+ };
1426
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1427
+ state === "idle" || state === "error" ? otherLocales.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1428
+ designSystem.Tooltip,
1429
+ {
1430
+ label: formatMessage({
1431
+ id: getTranslation("panel.no_other_locales"),
1432
+ defaultMessage: "Add another locale in Settings → Internationalization to enable AI translation."
1433
+ }),
1434
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1435
+ designSystem.Button,
1436
+ {
1437
+ variant: "secondary",
1438
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Magic, {}),
1439
+ size: "S",
1440
+ onClick: openDialog,
1441
+ disabled: true,
1442
+ children: formatMessage({
1443
+ id: getTranslation("panel.status.idle"),
1444
+ defaultMessage: "Translate content"
1445
+ })
1446
+ }
1447
+ ) })
1448
+ }
1449
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1450
+ designSystem.Button,
1451
+ {
1452
+ variant: state === "error" ? "danger" : "secondary",
1453
+ startIcon: state === "error" ? /* @__PURE__ */ jsxRuntime.jsx(icons.WarningCircle, {}) : /* @__PURE__ */ jsxRuntime.jsx(icons.Magic, {}),
1454
+ size: "S",
1455
+ onClick: openDialog,
1456
+ children: state === "error" ? formatMessage({
1457
+ id: getTranslation("panel.status.error"),
1458
+ defaultMessage: "Retry translation"
1459
+ }) : formatMessage({
1460
+ id: getTranslation("panel.status.idle"),
1461
+ defaultMessage: "Translate content"
1462
+ })
1463
+ }
1464
+ ) : state === "translating" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", size: "S", loading: true, disabled: true, children: formatMessage({
1465
+ id: getTranslation("panel.status.loading"),
1466
+ defaultMessage: "Translating..."
1467
+ }) }) : state === "success" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "success", size: "S", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircle, {}), disabled: true, children: formatMessage({ id: getTranslation("panel.status.success"), defaultMessage: "Applied" }) }) : null,
1468
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Root, { open: dialogOpen, onOpenChange: setDialogOpen, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { children: [
1469
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
1470
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, alignItems: "center", children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Magic, { width: "24px", height: "24px", fill: "primary600" }),
1472
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", children: formatMessage({
1473
+ id: getTranslation("dialog.title"),
1474
+ defaultMessage: "AI translation"
1475
+ }) }),
1476
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textAlign: "center", variant: "omega", children: formatMessage({
1477
+ id: getTranslation("dialog.body"),
1478
+ defaultMessage: "Translatable fields will be translated by AI. Non-translatable fields (media, dates, relations) will be copied from the source locale."
1479
+ }) })
1480
+ ] }),
1481
+ /* @__PURE__ */ jsxRuntime.jsxs(
1482
+ designSystem.Field.Root,
1483
+ {
1484
+ width: "100%",
1485
+ hint: formatMessage({
1486
+ id: getTranslation("dialog.source.hint"),
1487
+ defaultMessage: "Content from this locale will be used as the translation source."
1488
+ }),
1489
+ children: [
1490
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({
1491
+ id: getTranslation("dialog.source.label"),
1492
+ defaultMessage: "Select source locale"
1493
+ }) }),
1494
+ /* @__PURE__ */ jsxRuntime.jsx(
1495
+ designSystem.SingleSelect,
1496
+ {
1497
+ value: dialogLocale,
1498
+ onChange: (value) => setDialogLocale(String(value)),
1499
+ children: otherLocales.map((locale) => {
1500
+ const hasContent = Boolean(locale.status);
1501
+ const isPublished = locale.status === "published";
1502
+ const isDraft = locale.status === "draft";
1503
+ const isModified = locale.status === "modified";
1504
+ const statusLabel = isPublished ? formatMessage({
1505
+ id: getTranslation("locale.status.published"),
1506
+ defaultMessage: "Published"
1507
+ }) : isModified ? formatMessage({
1508
+ id: getTranslation("locale.status.modified"),
1509
+ defaultMessage: "Modified"
1510
+ }) : isDraft ? formatMessage({
1511
+ id: getTranslation("locale.status.draft"),
1512
+ defaultMessage: "Draft"
1513
+ }) : formatMessage({
1514
+ id: getTranslation("locale.status.none"),
1515
+ defaultMessage: "No content"
1516
+ });
1517
+ const name = locale.name.includes(locale.code) ? locale.name : `${locale.name} (${locale.code})`;
1518
+ return /* @__PURE__ */ jsxRuntime.jsx(
1519
+ designSystem.SingleSelectOption,
1520
+ {
1521
+ value: locale.code,
1522
+ disabled: !hasContent,
1523
+ children: `${name} - ${statusLabel}`
1524
+ },
1525
+ locale.code
1526
+ );
1527
+ })
1528
+ }
1529
+ ),
1530
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
1531
+ ]
1532
+ }
1533
+ ),
1534
+ translatableFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
1535
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({
1536
+ id: getTranslation("dialog.fields.label"),
1537
+ defaultMessage: "Fields to translate"
1538
+ }) }),
1539
+ /* @__PURE__ */ jsxRuntime.jsx(
1540
+ designSystem.Box,
1541
+ {
1542
+ hasRadius: true,
1543
+ borderColor: "neutral200",
1544
+ borderStyle: "solid",
1545
+ borderWidth: "1px",
1546
+ padding: 3,
1547
+ style: { maxHeight: "220px", overflowY: "auto" },
1548
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
1549
+ /* @__PURE__ */ jsxRuntime.jsx(
1550
+ designSystem.Checkbox,
1551
+ {
1552
+ checked: selectedFields.size === translatableFields.length ? true : selectedFields.size === 0 ? false : "indeterminate",
1553
+ onCheckedChange: (checked) => setSelectedFields(
1554
+ checked === true ? new Set(translatableFields) : /* @__PURE__ */ new Set()
1555
+ ),
1556
+ children: formatMessage(
1557
+ {
1558
+ id: getTranslation("dialog.fields.all"),
1559
+ defaultMessage: "All fields ({count})"
1560
+ },
1561
+ { count: translatableFields.length }
1562
+ )
1563
+ }
1564
+ ),
1565
+ translatableFields.map((field) => /* @__PURE__ */ jsxRuntime.jsx(
1566
+ designSystem.Checkbox,
1567
+ {
1568
+ checked: selectedFields.has(field),
1569
+ onCheckedChange: (checked) => setSelectedFields((prev) => {
1570
+ const next = new Set(prev);
1571
+ if (checked === true) {
1572
+ next.add(field);
1573
+ } else {
1574
+ next.delete(field);
1575
+ }
1576
+ return next;
1577
+ }),
1578
+ children: hasTextContent(values?.[field]) ? formatMessage(
1579
+ {
1580
+ id: getTranslation("dialog.fields.has_content"),
1581
+ defaultMessage: "{field} — already has content here"
1582
+ },
1583
+ { field }
1584
+ ) : field
1585
+ },
1586
+ field
1587
+ ))
1588
+ ] })
1589
+ }
1590
+ )
1591
+ ] }) : null
1592
+ ] }) }),
1593
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
1594
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: formatMessage({ id: getTranslation("actions.cancel"), defaultMessage: "Cancel" }) }) }),
1595
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Action, { children: /* @__PURE__ */ jsxRuntime.jsx(
1596
+ designSystem.Button,
1597
+ {
1598
+ variant: "default",
1599
+ onClick: confirmTranslate,
1600
+ disabled: !dialogLocale || !otherLocales.find((l) => l.code === dialogLocale)?.status || selectedFields.size === 0,
1601
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Magic, {}),
1602
+ children: formatMessage({
1603
+ id: getTranslation("dialog.confirm"),
1604
+ defaultMessage: "Translate"
1605
+ })
1606
+ }
1607
+ ) })
1608
+ ] })
1609
+ ] }) }),
1610
+ /* @__PURE__ */ jsxRuntime.jsx(
1611
+ TranslationModal,
1612
+ {
1613
+ isOpen: isModalOpen,
1614
+ isStreaming: state === "translating",
1615
+ sourceLocale: sourceLocale || "source",
1616
+ targetLocale: currentLocale,
1617
+ diffEntries: buildDiffEntries(),
1618
+ progress,
1619
+ currentField,
1620
+ failedFields: failedUnits.map(pathToLabel),
1621
+ elapsedTime,
1622
+ onAccept: handleAccept,
1623
+ onCancel: handleCancel,
1624
+ onRetryFailed: handleRetryFailed,
1625
+ onToggleField: handleToggleField,
1626
+ onToggleAll: handleToggleAll,
1627
+ onEditField: handleEditField
1628
+ }
1629
+ )
1630
+ ] });
1631
+ };
1632
+ const TranslationPanel = () => {
1633
+ const { formatMessage } = reactIntl.useIntl();
1634
+ const { contentType } = admin.unstable_useContentManagerContext();
1635
+ const isI18nEnabled = Boolean(contentType?.pluginOptions?.i18n?.localized);
1636
+ if (!isI18nEnabled) {
1637
+ return null;
1638
+ }
1639
+ return {
1640
+ title: formatMessage({ id: getTranslation("plugin.name"), defaultMessage: "AI Translator" }),
1641
+ content: /* @__PURE__ */ jsxRuntime.jsx(TranslationWorkflow, {})
1642
+ };
1643
+ };
1644
+ const index = {
1645
+ register(app) {
1646
+ app.createSettingSection(
1647
+ {
1648
+ id: PLUGIN_ID,
1649
+ intlLabel: { id: getTranslation("plugin.name"), defaultMessage: "AI Translator" }
1650
+ },
1651
+ [
1652
+ {
1653
+ id: PLUGIN_ID,
1654
+ to: `/settings/${PLUGIN_ID}`,
1655
+ intlLabel: { id: getTranslation("settings.label"), defaultMessage: "Configuration" },
1656
+ permissions: [{ action: `plugin::${PLUGIN_ID}.settings.manage`, subject: null }],
1657
+ Component: async () => {
1658
+ const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-CD90jfwT.js"));
1659
+ return { default: SettingsPage };
1660
+ }
1661
+ }
1662
+ ]
1663
+ );
1664
+ app.registerPlugin({
1665
+ id: PLUGIN_ID,
1666
+ initializer: Initializer,
1667
+ isReady: false,
1668
+ name: PLUGIN_ID
1669
+ });
1670
+ },
1671
+ bootstrap(app) {
1672
+ const contentManager = app.getPlugin("content-manager");
1673
+ const contentManagerApis = contentManager?.apis;
1674
+ if (typeof contentManagerApis?.addEditViewSidePanel === "function") {
1675
+ contentManagerApis.addEditViewSidePanel([TranslationPanel]);
1676
+ }
1677
+ },
1678
+ async registerTrads({ locales }) {
1679
+ return Promise.all(
1680
+ locales.map(async (locale) => {
1681
+ try {
1682
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-I9cvzxfF.js")) }), `./translations/${locale}.json`, 3);
1683
+ return { data, locale };
1684
+ } catch {
1685
+ return { data: {}, locale };
1686
+ }
1687
+ })
1688
+ );
1689
+ }
1690
+ };
1691
+ exports.PLUGIN_ID = PLUGIN_ID;
1692
+ exports.getTranslation = getTranslation;
1693
+ exports.index = index;