@braze/sanity-plugin-ai-translation 0.2.39 → 0.2.52

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/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
- import { useClient, getPublishedId, pathToString, defineType, defineField, definePlugin } from "sanity";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, WarningOutlineIcon, InfoOutlineIcon, ArrowLeftIcon, EditIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
4
- import { createContext, useReducer, useEffect, useContext, useState, useCallback, useMemo } from "react";
1
+ import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
4
+ import { createContext, useReducer, useEffect, useContext, useMemo, useState, useCallback } from "react";
5
5
  import defaults from "lodash.defaults";
6
- import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Select, TextInput, TextArea, Heading, Inline, Badge, Grid, Radio, Label, Code } from "@sanity/ui";
7
- import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
6
+ import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
8
7
  import { toPlainText } from "@portabletext/react";
9
8
  import { uuid } from "@sanity/uuid";
10
9
  import cloneDeep from "lodash.clonedeep";
11
10
  import get from "lodash.get";
12
11
  import set from "lodash.set";
12
+ import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
13
13
  const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
14
14
  info: (message, context) => {
15
15
  isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
@@ -62,19 +62,65 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
62
62
  error: error instanceof Error ? error.message : "Network error"
63
63
  };
64
64
  }
65
+ }, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
66
+ const entry = {
67
+ state: {
68
+ documentId,
69
+ releaseId,
70
+ overallStatus: "translating",
71
+ translationJobs: /* @__PURE__ */ new Map(),
72
+ startedAt: /* @__PURE__ */ new Date()
73
+ },
74
+ subscribers: /* @__PURE__ */ new Set()
75
+ };
76
+ activeTranslations.set(documentId, entry);
77
+ }, updateActiveTranslationStatus = (documentId, status) => {
78
+ const entry = activeTranslations.get(documentId);
79
+ entry && (entry.state.overallStatus = status, notifySubscribers(entry));
80
+ }, updateActiveTranslationJob = (documentId, job) => {
81
+ const entry = activeTranslations.get(documentId);
82
+ if (entry) {
83
+ const jobKey = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
84
+ Object.entries(job).filter(([_, value]) => value !== void 0)
85
+ ), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
86
+ entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
87
+ }
88
+ }, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
89
+ const entry = activeTranslations.get(documentId);
90
+ return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
91
+ entry.subscribers.delete(callback);
92
+ }) : () => {
93
+ };
94
+ }, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
95
+ setTimeout(() => {
96
+ const entry = activeTranslations.get(documentId);
97
+ entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
98
+ }, delayMs);
99
+ }, notifySubscribers = (entry) => {
100
+ for (const subscriber of entry.subscribers)
101
+ try {
102
+ subscriber(entry.state);
103
+ } catch (error) {
104
+ console.error("Error in active translation subscriber:", error);
105
+ }
106
+ }, forceCleanupActiveTranslation = (documentId) => {
107
+ activeTranslations.delete(documentId);
65
108
  }, TranslationContext = createContext({
66
109
  document: null,
67
110
  config: null,
68
111
  overallStatus: "idle",
69
112
  selectedTargetLanguages: [],
70
113
  translationJobs: /* @__PURE__ */ new Map(),
71
- shouldOverwriteFreshTranslations: !1,
72
114
  shouldTranslateReferences: !1,
73
115
  maxReferenceDepth: 1,
74
116
  references: [],
75
117
  translationMetadata: null,
76
118
  translationMode: "sanity-only",
77
119
  openaiValidated: !1,
120
+ releaseConfig: {
121
+ mode: "new",
122
+ newReleaseTitle: ""
123
+ },
78
124
  dispatch: () => {
79
125
  }
80
126
  }), translationReducer = (context, action) => {
@@ -85,7 +131,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
85
131
  };
86
132
  switch (devLog.debug("Translation action dispatched", {
87
133
  ...logContext,
88
- payload: action.payload
134
+ payload: "payload" in action ? action.payload : void 0
89
135
  }), action.type) {
90
136
  case "SET_OVERALL_STATUS":
91
137
  return { ...context, overallStatus: action.payload };
@@ -116,11 +162,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
116
162
  ...context,
117
163
  maxReferenceDepth: action.payload
118
164
  };
119
- case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
120
- return {
121
- ...context,
122
- shouldOverwriteFreshTranslations: action.payload
123
- };
124
165
  case "SET_REFERENCES":
125
166
  return {
126
167
  ...context,
@@ -133,31 +174,82 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
133
174
  translationJobs: newTranslationJobs
134
175
  };
135
176
  }
177
+ case "SET_RELEASE_CONFIG":
178
+ return {
179
+ ...context,
180
+ releaseConfig: {
181
+ ...context.releaseConfig,
182
+ ...action.payload
183
+ }
184
+ };
185
+ case "RESET_TRANSLATION_STATE":
186
+ return devLog.info("Resetting translation state for new translation", {
187
+ ...logContext,
188
+ previousStatus: context.overallStatus,
189
+ previousJobCount: context.translationJobs.size
190
+ }), {
191
+ ...context,
192
+ overallStatus: "idle",
193
+ translationJobs: /* @__PURE__ */ new Map(),
194
+ // Clear any release conflicts from previous run
195
+ releaseConfig: {
196
+ ...context.releaseConfig,
197
+ conflicts: void 0,
198
+ replaceExisting: void 0
199
+ }
200
+ };
136
201
  default:
137
202
  return context;
138
203
  }
139
- }, TranslationContextProvider = ({ document, config, children }) => {
204
+ }, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
140
205
  devLog.debug("TranslationContextProvider initialized", {
141
206
  documentId: document._id,
142
207
  documentType: document._type,
143
208
  supportedLanguages: config.supportedLanguages?.map((l) => l.id),
144
209
  component: "TranslationContextProvider"
145
210
  });
146
- const initialContext = {
211
+ const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
147
212
  document,
148
213
  config,
149
- overallStatus: "idle",
214
+ // If there's an active translation, restore its state
215
+ overallStatus: activeTranslation?.overallStatus ?? "idle",
150
216
  translationMetadata: null,
151
217
  selectedTargetLanguages: [],
152
- shouldOverwriteFreshTranslations: !1,
153
218
  shouldTranslateReferences: !1,
154
219
  maxReferenceDepth: 1,
155
220
  references: [],
156
- translationJobs: /* @__PURE__ */ new Map(),
221
+ // Restore jobs from active translation if available
222
+ translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
157
223
  translationMode: config?.openai?.defaultMode || "sanity-only",
158
- openaiValidated: !1
159
- }, [context, dispatch] = useReducer(translationReducer, initialContext);
224
+ openaiValidated: !1,
225
+ releaseConfig: {
226
+ mode: "new",
227
+ newReleaseTitle: defaultReleaseTitle
228
+ }
229
+ };
230
+ activeTranslation && devLog.info("Reconnecting to active translation", {
231
+ documentId: document._id,
232
+ status: activeTranslation.overallStatus,
233
+ jobCount: activeTranslation.translationJobs.size,
234
+ component: "TranslationContextProvider"
235
+ });
236
+ const [context, dispatch] = useReducer(translationReducer, initialContext);
160
237
  return useEffect(() => {
238
+ const handleActiveTranslationUpdate = (state) => {
239
+ dispatch({
240
+ type: "SET_OVERALL_STATUS",
241
+ payload: state.overallStatus
242
+ });
243
+ for (const job of state.translationJobs.values())
244
+ dispatch({
245
+ type: "SET_TRANSLATION_JOB_STATUS",
246
+ payload: job
247
+ });
248
+ }, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
249
+ return () => {
250
+ unsubscribe();
251
+ };
252
+ }, [document._id]), useEffect(() => {
161
253
  (async () => {
162
254
  if (config?.openai?.enabled && config?.openai?.apiKey)
163
255
  try {
@@ -196,1044 +288,238 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
196
288
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
197
289
  );
198
290
  return context;
199
- }, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
200
- const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
201
- params: {
202
- documentId: undraftId(documentId),
203
- translationMetadataType: TRANSLATION_METADATA_TYPE
204
- },
205
- initialValue: null
206
- });
207
- return useEffect(() => {
208
- if (loading) {
209
- dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
210
- return;
211
- }
212
- if (error) {
213
- dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
214
- return;
215
- }
216
- if (data) {
217
- dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
218
- return;
219
- }
220
- dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
221
- }, [data, loading, error, dispatch]), {
222
- data,
223
- loading,
224
- error
225
- };
226
- };
227
- async function validateSuggestion(client, suggestion) {
228
- return suggestion.type === "glossaryTerm" ? validateGlossaryTerm(client, suggestion) : suggestion.type === "protectedPhrase" ? validateProtectedPhrase(client, suggestion) : suggestion.type === "globalPrompt" || suggestion.type === "localePrompt" ? validatePrompt(suggestion) : { valid: !0, status: "valid" };
229
- }
230
- async function validateGlossaryTerm(client, suggestion) {
231
- const { sourceValue, targetValue, sourceLocale, targetLocale } = suggestion;
232
- if (!sourceValue || !targetValue || !sourceLocale || !targetLocale)
233
- return {
234
- valid: !1,
235
- status: "invalid",
236
- reason: "Missing required fields for glossary term"
237
- };
238
- if (sourceValue.trim() === targetValue.trim())
239
- return {
240
- valid: !1,
241
- status: "invalid",
242
- reason: `Source and target are identical ("${sourceValue}") - this should be a protected phrase instead`
243
- };
244
- const protectedPhrases = await client.fetch('*[_id == "translation-settings"][0].protectedPhrases');
245
- if (protectedPhrases?.some((phrase) => phrase.toLowerCase() === sourceValue.toLowerCase()))
246
- return {
247
- valid: !1,
248
- status: "invalid",
249
- reason: `Cannot add "${sourceValue}" to glossary because it is already a protected phrase. Protected phrases are never translated. If you want to create a translation mapping for this term, please remove it from protected phrases first by editing Translation Settings.`
250
- };
251
- if (targetValue && protectedPhrases?.some((phrase) => phrase.toLowerCase() === targetValue.toLowerCase()))
252
- return {
253
- valid: !1,
254
- status: "invalid",
255
- reason: `Cannot use "${targetValue}" as a translation target because it is a protected phrase. Protected phrases should not appear as translation targets.`
256
- };
257
- const existingTerm = await client.fetch(
258
- `*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0][0] {
259
- translations
260
- }`,
261
- { sourceLocale, sourceValue }
262
- );
263
- if (existingTerm) {
264
- const existingTarget = existingTerm.translations.find((t) => t._key === targetLocale);
265
- if (existingTarget?.value === targetValue)
266
- return {
267
- valid: !0,
268
- status: "warning",
269
- reason: `This translation already exists in the glossary: "${sourceValue}" \u2192 "${targetValue}"`
270
- };
271
- if (existingTarget && existingTarget.value !== targetValue)
272
- return {
273
- valid: !0,
274
- status: "warning",
275
- reason: `"${sourceValue}" already has a different translation in the glossary: "${existingTarget.value}". Applying this will update it to "${targetValue}".`
276
- };
277
- }
278
- return { valid: !0, status: "valid" };
279
- }
280
- async function validateProtectedPhrase(client, suggestion) {
281
- const phrase = suggestion.description.trim();
282
- if (!phrase)
283
- return {
284
- valid: !1,
285
- status: "invalid",
286
- reason: "Protected phrase cannot be empty"
287
- };
288
- if ((await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.some((p) => p.toLowerCase() === phrase.toLowerCase()))
289
- return {
290
- valid: !0,
291
- status: "warning",
292
- reason: `"${phrase}" is already in protected phrases`
293
- };
294
- const glossaryTermWithPhrase = await client.fetch(
295
- `*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0][0] {
296
- _id,
297
- translations
298
- }`,
299
- { phrase }
300
- );
301
- if (glossaryTermWithPhrase) {
302
- const matchingTranslations = glossaryTermWithPhrase.translations.filter((t) => t.value.toLowerCase() === phrase.toLowerCase()).map((t) => t._key).join(", ");
303
- return {
304
- valid: !1,
305
- status: "invalid",
306
- reason: `Cannot add "${phrase}" as a protected phrase because it exists in the glossary as a translation (${matchingTranslations}). If you want to prevent this term from being translated, please remove it from the glossary first by editing Translation Settings.`
307
- };
308
- }
309
- return { valid: !0, status: "valid" };
310
- }
311
- function validatePrompt(suggestion) {
312
- const update = suggestion.description.trim();
313
- return update ? update.toLowerCase().includes("has been") || update.toLowerCase().includes("was changed") || update.toLowerCase().includes("corrected to") ? {
314
- valid: !0,
315
- status: "warning",
316
- reason: "This update may be describing past changes rather than adding new rules"
317
- } : { valid: !0, status: "valid" } : {
318
- valid: !1,
319
- status: "invalid",
320
- reason: "Prompt update cannot be empty"
321
- };
322
- }
323
- async function analyzeFeedback({
324
- client,
325
- sourceLanguage,
326
- targetLanguage,
327
- userFeedback,
328
- shouldIncludeEdits,
329
- sourceSnapshot,
330
- targetSnapshot,
331
- currentDocument,
332
- onProgress
333
- }) {
334
- onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
335
- const analysisPrompt = buildAnalysisPrompt({
336
- sourceLanguage,
337
- targetLanguage,
338
- userFeedback,
339
- shouldIncludeEdits,
340
- sourceSnapshot,
341
- targetSnapshot,
342
- currentDocument
343
- }), analysis = await analyzeWithAI(client, analysisPrompt), suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
344
- onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
345
- const validatedSuggestions = await Promise.all(
346
- suggestions.map(async (suggestion) => {
347
- const validation = await validateSuggestion(client, suggestion);
348
- return {
349
- ...suggestion,
350
- warning: validation.reason,
351
- validationStatus: validation.status,
352
- // Only auto-approve valid suggestions; invalid ones cannot be approved
353
- approved: validation.status === "valid"
354
- };
355
- })
356
- );
357
- return devLog.info("Feedback analysis complete", {
358
- sourceLanguage,
359
- targetLanguage,
360
- total: validatedSuggestions.length,
361
- valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
362
- warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
363
- invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
364
- }), validatedSuggestions;
365
- }
366
- async function applySuggestions({
291
+ }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
292
+ (translation) => translation._key === targetLanguage
293
+ )?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
367
294
  client,
368
- schemaId,
369
- sourceLanguage,
370
- targetLanguage,
371
- suggestions,
372
- onProgress
373
- }) {
374
- if (!targetLanguage)
375
- throw devLog.error("targetLanguage is undefined in applySuggestions!", {
376
- sourceLanguage,
377
- targetLanguage,
378
- suggestionsCount: suggestions.length
379
- }), new Error("targetLanguage is required but was undefined");
380
- const approvedSuggestions = suggestions.filter((s) => s.approved);
381
- approvedSuggestions.length !== 0 && (onProgress?.({ stage: "applying", message: "Applying updates to translation settings..." }), await applyImprovementsFromSuggestions(client, schemaId, targetLanguage, approvedSuggestions, onProgress), devLog.info("Suggestions applied successfully", {
382
- sourceLanguage,
383
- targetLanguage,
384
- count: approvedSuggestions.length
385
- }));
386
- }
387
- function buildAnalysisPrompt({
388
- sourceLanguage,
389
- targetLanguage,
390
- userFeedback,
391
- shouldIncludeEdits,
392
- sourceSnapshot,
393
- targetSnapshot,
394
- currentDocument
395
- }) {
396
- const parts = [
397
- "You are analyzing feedback about AI translation quality to improve future translations.",
398
- `Source language: ${sourceLanguage}`,
399
- `Target language: ${targetLanguage}`,
400
- "",
401
- "Your task is to analyze the feedback and infer specific improvements to:",
402
- "1. Global translation prompt (general guidance for all translations)",
403
- "2. Locale-specific prompt (guidance specific to this target language)",
404
- "3. Protected phrases (terms that should never be translated)",
405
- "4. Glossary terms (specific term-to-term translation mappings)",
406
- ""
407
- ];
408
- return userFeedback && parts.push("USER FEEDBACK:", userFeedback, ""), shouldIncludeEdits && sourceSnapshot && targetSnapshot && currentDocument && parts.push(
409
- "EDIT ANALYSIS:",
410
- "Compare the original translation with the edited version to understand what the user changed.",
411
- "",
412
- "Original source document:",
413
- sourceSnapshot,
414
- "",
415
- "Original AI translation:",
416
- targetSnapshot,
417
- "",
418
- "Current edited document:",
419
- JSON.stringify(currentDocument, null, 2),
420
- ""
421
- ), parts.push(
422
- "INSTRUCTIONS:",
423
- "Analyze the feedback and/or edits to identify patterns and improvements.",
424
- "",
425
- "For edits, look for:",
426
- "- Terminology changes that suggest glossary additions",
427
- "- Phrases that were protected/unprotected incorrectly",
428
- "- Style, tone, or formatting issues that suggest prompt improvements",
429
- "- Consistent patterns across multiple changes",
430
- "",
431
- "IMPORTANT RULES:",
432
- "1. DO NOT create redundant updates. Choose ONE mechanism for each issue:",
433
- " - Protected phrases = terms that should NEVER be translated (language-agnostic)",
434
- ' - Glossary terms = specific translation mappings (e.g., "chat" \u2192 "cat" in French)',
435
- " - Locale prompts = guidance specific to this source\u2192target language pair",
436
- " - Global prompts = guidance that applies to ALL translations",
437
- "",
438
- "2. Be locale-specific:",
439
- ` - This feedback is specifically about ${sourceLanguage} \u2192 ${targetLanguage} translation`,
440
- " - DO NOT assume rules apply to other language pairs unless explicitly stated",
441
- " - Prefer locale-specific prompts over global prompts unless universally applicable",
442
- "",
443
- "3. Protected phrases vs. Locale prompts:",
444
- " - Use protected phrases ONLY for brand names, product names, proper nouns, technical terms that should NEVER be translated in ANY language",
445
- ' - Use locale prompts for language-pair-specific guidance (e.g., "Keep technical terms in English when translating to German")',
446
- " - NEVER mention specific terms in both protected phrases AND prompts",
447
- "",
448
- "4. Glossary vs. Protected phrases:",
449
- ' - Glossary = "Always translate X as Y" (provides the correct translation)',
450
- ' - Protected = "Never translate X" (keeps it as-is)',
451
- " - CRITICAL: If sourceValue === targetValue, use PROTECTED PHRASES, NOT glossary",
452
- " - Only use glossary when the term actually changes between languages",
453
- " - Use protected when the term should remain unchanged across all languages",
454
- "",
455
- "Output a JSON object with this structure:",
456
- "{",
457
- ' "globalPromptUpdates": "string | null - ONLY if this applies to ALL language pairs",',
458
- ` "localePromptUpdates": "string | null - ONLY if this is specific to ${sourceLanguage}\u2192${targetLanguage}",`,
459
- ' "protectedPhrasesToAdd": ["ONLY brand names, products, proper nouns that should NEVER translate"],',
460
- ' "glossaryTermsToAdd": [{"sourceLocale": "sourceLocale","sourceValue": "term", "targetLocale": "targetLocale", "targetValue": "translation"} - ONLY specific term mappings],',
461
- ' "reasoning": "string - Explain your analysis and which mechanism you chose for each issue"',
462
- "}",
463
- "",
464
- "Be conservative: only suggest changes when there is clear evidence.",
465
- "If in doubt, prefer locale-specific updates over global updates.",
466
- "Avoid redundancy - if you add a protected phrase, do NOT mention it in prompts.",
467
- "",
468
- "CONCRETE EXAMPLES:",
469
- "",
470
- "Example 1 - Brand name protection:",
471
- 'User feedback: "Braze should never be translated"',
472
- "Correct output:",
473
- "{",
474
- ' "protectedPhrasesToAdd": ["Braze"],',
475
- ' "reasoning": "User explicitly stated Braze is a brand name that should not be translated"',
476
- "}",
477
- "WRONG: Do not add this to glossary or prompts.",
478
- "",
479
- "Example 2 - Terminology correction:",
480
- 'Edit analysis: User changed "discussion" to "chat" consistently in French translation',
481
- "Correct output:",
482
- "{",
483
- ` "glossaryTermsToAdd": [{"sourceLocale": "${sourceLanguage}", "sourceValue": "discussion", "targetLocale": "${targetLanguage}", "targetValue": "chat"}],`,
484
- ' "reasoning": "User consistently changed discussion\u2192chat, indicating preferred terminology"',
485
- "}",
486
- "WRONG: Do not add to protected phrases (values differ) or prompts (specific mapping).",
487
- "",
488
- "Example 3 - Style guidance:",
489
- 'User feedback: "Translation is too formal, use casual tone"',
490
- "Correct output:",
491
- "{",
492
- ` "localePromptUpdates": "Use a casual, conversational tone when translating to ${targetLanguage}. Avoid overly formal language.",`,
493
- ' "reasoning": "This is style guidance specific to this language pair"',
494
- "}",
495
- "WRONG: Do not make global (applies only to this target language).",
496
- "",
497
- "Example 4 - Technical term in English:",
498
- `User feedback: "Keep technical terms like 'API' and 'webhook' in English"`,
499
- "Correct output:",
500
- "{",
501
- ` "localePromptUpdates": "When translating to ${targetLanguage}, keep technical terms like 'API', 'webhook', and similar technical jargon in English.",`,
502
- ' "reasoning": "This is language-pair-specific guidance about handling technical terms"',
503
- "}",
504
- 'WRONG: Do not add "API" or "webhook" to protected phrases (already covered by prompt guidance).',
505
- "",
506
- "Example 5 - Multiple issues:",
507
- 'User feedback: "MyApp is our product name. Also, use more concise language."',
508
- "Correct output:",
509
- "{",
510
- ' "protectedPhrasesToAdd": ["MyApp"],',
511
- ` "localePromptUpdates": "Use concise language when translating to ${targetLanguage}. Prefer shorter phrases over lengthy explanations.",`,
512
- ' "reasoning": "MyApp is a product name (protected). Conciseness is a style preference for this language pair (locale prompt)."',
513
- "}",
514
- "",
515
- "Example 6 - Identical source and target:",
516
- 'Edit analysis: User kept "dashboard" as "dashboard" in Spanish',
517
- "Correct output:",
518
- "{",
519
- ' "protectedPhrasesToAdd": ["dashboard"],',
520
- ' "reasoning": "User kept the term unchanged, indicating it should not be translated"',
521
- "}",
522
- "WRONG: Do not add to glossary when source === target."
523
- ), parts.join(`
524
- `);
525
- }
526
- function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage) {
527
- const suggestions = [];
528
- if (analysis.globalPromptUpdates && suggestions.push({
529
- id: crypto.randomUUID(),
530
- type: "globalPrompt",
531
- description: analysis.globalPromptUpdates,
532
- approved: !0
533
- }), analysis.localePromptUpdates && suggestions.push({
534
- id: crypto.randomUUID(),
535
- type: "localePrompt",
536
- description: analysis.localePromptUpdates,
537
- approved: !0
538
- }), analysis.protectedPhrasesToAdd)
539
- for (const phrase of analysis.protectedPhrasesToAdd)
540
- suggestions.push({
541
- id: crypto.randomUUID(),
542
- type: "protectedPhrase",
543
- description: phrase,
544
- approved: !0
545
- });
546
- if (analysis.glossaryTermsToAdd)
547
- for (const term of analysis.glossaryTermsToAdd)
548
- suggestions.push({
549
- id: crypto.randomUUID(),
550
- type: "glossaryTerm",
551
- description: `${term.sourceValue} \u2192 ${term.targetValue}`,
552
- approved: !0,
553
- // Use explicit source/target languages from history instead of AI-provided values
554
- sourceLocale: sourceLanguage,
555
- sourceValue: term.sourceValue,
556
- targetLocale: targetLanguage,
557
- targetValue: term.targetValue
558
- });
559
- return suggestions;
560
- }
561
- async function analyzeWithAI(client, prompt) {
295
+ sourceDocument,
296
+ sourceLanguage
297
+ }) => {
562
298
  try {
563
- const response = await client.agent.action.prompt({
564
- instruction: prompt,
565
- format: "json"
299
+ return await client.fetch(
300
+ "*[_type == $tmType && references($sourceDocumentId)][0]",
301
+ {
302
+ tmType: TRANSLATION_METADATA_TYPE,
303
+ sourceDocumentId: undraftId(sourceDocument._id)
304
+ }
305
+ ) || await client.create({
306
+ _type: TRANSLATION_METADATA_TYPE,
307
+ translations: [
308
+ {
309
+ _key: sourceLanguage,
310
+ _type: "internationalizedArrayReferenceValue",
311
+ value: {
312
+ _type: "reference",
313
+ _ref: undraftId(sourceDocument._id),
314
+ _weak: !0,
315
+ _strengthenOnPublish: {
316
+ _type: sourceDocument._type,
317
+ _weak: !1
318
+ }
319
+ }
320
+ }
321
+ ],
322
+ translationHistory: []
566
323
  });
567
- devLog.debug("AI response received", { responseType: typeof response });
568
- const responseText = typeof response == "string" ? response : response && typeof response == "object" ? JSON.stringify(response) : (() => {
569
- throw new Error(`Unexpected response type: ${typeof response}`);
570
- })();
571
- return parseAIResponse(responseText);
572
324
  } catch (error) {
573
- throw devLog.error("Error during AI analysis", { error }), new Error(`Failed to analyze feedback: ${error instanceof Error ? error.message : "Unknown error"}`);
325
+ throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
574
326
  }
575
- }
576
- function parseAIResponse(responseText) {
577
- try {
578
- let parsed;
579
- try {
580
- parsed = JSON.parse(responseText);
581
- } catch {
582
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
583
- if (!jsonMatch) throw new Error("No JSON found in AI response");
584
- parsed = JSON.parse(jsonMatch[0]);
327
+ }, checkDocumentConflicts = async ({
328
+ client,
329
+ releaseId,
330
+ documentId,
331
+ documentTitle,
332
+ documentType,
333
+ targetLanguages,
334
+ languageFieldName
335
+ }) => {
336
+ const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
337
+ if (!document)
338
+ return conflicts;
339
+ const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
340
+ client,
341
+ sourceDocument: document,
342
+ sourceLanguage
343
+ });
344
+ for (const targetLanguage of targetLanguages) {
345
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
346
+ if (existingTranslationId) {
347
+ const versionedId = `versions.${releaseId}.${existingTranslationId}`;
348
+ await client.getDocument(versionedId) && (conflicts.push({
349
+ sourceDocumentId: documentId,
350
+ sourceDocumentTitle: documentTitle || document.title || document.name,
351
+ sourceDocumentType: documentType || document._type,
352
+ targetLanguage,
353
+ existingVersionId: versionedId
354
+ }), devLog.debug("Found existing version in release", {
355
+ releaseId,
356
+ sourceDocumentId: documentId,
357
+ targetLanguage,
358
+ versionedId
359
+ }));
585
360
  }
586
- if (!parsed || typeof parsed != "object")
587
- throw new Error("Response is not a valid object");
588
- const analysis = parsed;
589
- if (typeof analysis.reasoning != "string")
590
- throw new Error("Invalid response structure: missing reasoning");
591
- return {
592
- globalPromptUpdates: analysis.globalPromptUpdates || void 0,
593
- localePromptUpdates: analysis.localePromptUpdates || void 0,
594
- protectedPhrasesToAdd: Array.isArray(analysis.protectedPhrasesToAdd) ? analysis.protectedPhrasesToAdd : [],
595
- glossaryTermsToAdd: Array.isArray(analysis.glossaryTermsToAdd) ? analysis.glossaryTermsToAdd : [],
596
- reasoning: analysis.reasoning
597
- };
361
+ }
362
+ return conflicts;
363
+ }, checkReleaseConflicts = async ({
364
+ client,
365
+ releaseId,
366
+ sourceDocument,
367
+ targetLanguages,
368
+ languageFieldName,
369
+ references = []
370
+ }) => {
371
+ const allConflicts = [];
372
+ try {
373
+ const documentsToCheck = [
374
+ {
375
+ _id: sourceDocument._id,
376
+ _type: sourceDocument._type,
377
+ title: sourceDocument.title || sourceDocument.name
378
+ },
379
+ ...references.map((ref) => ({
380
+ _id: ref._id,
381
+ _type: ref._type,
382
+ title: ref.title
383
+ }))
384
+ ], conflictChecks = await Promise.all(
385
+ documentsToCheck.map(
386
+ (doc) => checkDocumentConflicts({
387
+ client,
388
+ releaseId,
389
+ documentId: doc._id,
390
+ documentTitle: doc.title,
391
+ documentType: doc._type,
392
+ targetLanguages,
393
+ languageFieldName
394
+ })
395
+ )
396
+ );
397
+ for (const conflicts of conflictChecks)
398
+ allConflicts.push(...conflicts);
399
+ devLog.debug("Release conflict check complete", {
400
+ releaseId,
401
+ documentsChecked: documentsToCheck.length,
402
+ conflictsFound: allConflicts.length
403
+ });
598
404
  } catch (error) {
599
- throw devLog.error("Error parsing AI response", { error, responseText }), new Error("Failed to parse AI response as JSON");
405
+ devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
406
+ releaseId,
407
+ sourceDocumentId: sourceDocument._id,
408
+ targetLanguages,
409
+ referencesCount: references.length
410
+ });
600
411
  }
601
- }
602
- async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
603
- const settingsId = "translation-settings";
604
- await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
605
- _id: settingsId,
606
- _type: "translationSettings"
607
- });
608
- const updates = [], globalPrompts = suggestions.filter((s) => s.type === "globalPrompt" && s.description.trim()), localePrompts = suggestions.filter((s) => s.type === "localePrompt" && s.description.trim()), protectedPhrases = suggestions.filter((s) => s.type === "protectedPhrase" && s.description.trim()), glossaryTerms = suggestions.filter(
609
- (s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
610
- );
611
- if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
612
- stage: "applying",
613
- message: globalPrompts.length > 0 && localePrompts.length > 0 ? "Updating global and locale translation prompts..." : globalPrompts.length > 0 ? "Updating global translation prompt..." : `Updating ${targetLanguage} translation prompt...`
614
- }), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
615
- onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
616
- const phrases = protectedPhrases.map((s) => s.description);
617
- updates.push(addProtectedPhrases(client, settingsId, phrases));
412
+ return allConflicts;
413
+ };
414
+ function isNetworkError(error) {
415
+ if (!error) return !1;
416
+ if (error instanceof TypeError) {
417
+ const message = error.message.toLowerCase();
418
+ return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
618
419
  }
619
- if (glossaryTerms.length > 0) {
620
- onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
621
- const terms = glossaryTerms.map((s) => ({
622
- // biome-ignore-start lint/style/noNonNullAssertion: safely handled
623
- sourceLocale: s.sourceLocale,
624
- sourceValue: s.sourceValue,
625
- targetLocale: s.targetLocale,
626
- targetValue: s.targetValue
627
- // biome-ignore-end lint/style/noNonNullAssertion: safely handled
628
- }));
629
- updates.push(addGlossaryTerms(client, terms));
420
+ if (error && typeof error == "object" && "code" in error) {
421
+ const code = error.code;
422
+ return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
630
423
  }
631
- updates.length === 0 && devLog.warn("No valid updates to apply - all suggestions may have been empty"), await Promise.all(updates), onProgress?.({ stage: "applying", message: "Recording changes..." }), await recordChangeHistory(
632
- client,
633
- settingsId,
634
- targetLanguage,
635
- suggestions.filter((s) => s.approved)
636
- );
637
- }
638
- async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
639
- if (!targetLanguage)
640
- throw devLog.error("targetLanguage is undefined or empty!", {
641
- targetLanguage,
642
- globalPrompts: globalPrompts.length,
643
- localePrompts: localePrompts.length
644
- }), new Error("targetLanguage is required but was undefined");
645
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], currentSettings = await client.fetch("*[_id == $settingsId][0]{ globalPrompt, localePrompts }", { settingsId });
646
- if (globalPrompts.length > 0) {
647
- const currentGlobalPrompt = currentSettings?.globalPrompt || "", updates = globalPrompts.map((s) => s.description.trim()).filter(Boolean), newGlobalPrompt = [
648
- currentGlobalPrompt,
649
- currentGlobalPrompt ? `
650
- ` : "",
651
- `## User Feedback (${timestamp})`,
652
- ...updates.map((update) => `- ${update}`)
653
- ].filter(Boolean).join(`
654
- `);
655
- await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
424
+ if (error && typeof error == "object" && "response" in error) {
425
+ const response = error.response;
426
+ if (response?.status)
427
+ return response.status >= 500 || response.status === 429;
656
428
  }
657
- if (localePrompts.length > 0) {
658
- const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
659
- if (existingLocalePrompt) {
660
- const currentValue = existingLocalePrompt.value || "", newValue = [
661
- currentValue,
662
- currentValue ? `
663
- ` : "",
664
- `## User Feedback (${timestamp})`,
665
- ...updates.map((update) => `- ${update}`)
666
- ].filter(Boolean).join(`
667
- `), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
668
- await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
669
- } else {
670
- const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
671
- `);
672
- await client.patch(settingsId).setIfMissing({ localePrompts: [] }).append("localePrompts", [
673
- {
674
- _type: "internationalizedArrayTextValue",
675
- _key: targetLanguage,
676
- value: newValue
677
- }
678
- ]).commit();
679
- }
429
+ if (error instanceof Error) {
430
+ const message = error.message;
431
+ if (/\b(50[0-9]|429)\b/.test(message))
432
+ return !0;
680
433
  }
681
- devLog.info("Prompts updated successfully", {
682
- targetLanguage,
683
- globalUpdated: globalPrompts.length > 0,
684
- localeUpdated: localePrompts.length > 0
685
- });
686
- }
687
- async function addProtectedPhrases(client, settingsId, phrases) {
688
- const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
689
- let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
690
- const conflictingPhrases = [];
691
- for (const phrase of newPhrases)
692
- await client.fetch(
693
- 'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
694
- { phrase }
695
- ) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
696
- newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length !== 0 && (await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit(), devLog.info("Protected phrases added", { count: newPhrases.length }));
434
+ return !1;
697
435
  }
698
- async function addGlossaryTerms(client, terms) {
699
- const protectedPhrasesLower = (await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.map((p) => p.toLowerCase()) || [], validTerms = terms.filter(({ sourceValue, targetValue }) => sourceValue === targetValue ? (devLog.warn("Skipping glossary term where source === target (should be a protected phrase)", {
700
- sourceValue,
701
- targetValue
702
- }), !1) : protectedPhrasesLower.includes(sourceValue.toLowerCase()) ? (devLog.warn("Skipping glossary term because source value is a protected phrase", {
703
- sourceValue,
704
- targetValue
705
- }), !1) : protectedPhrasesLower.includes(targetValue.toLowerCase()) ? (devLog.warn("Skipping glossary term because target value is a protected phrase", {
706
- sourceValue,
707
- targetValue
708
- }), !1) : !0);
709
- if (validTerms.length === 0) {
710
- devLog.info("No valid glossary terms to add");
711
- return;
712
- }
713
- for (const { sourceLocale, sourceValue, targetLocale, targetValue } of validTerms) {
714
- const existingTerm = await client.fetch(
715
- `*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0
716
- ][0] {
717
- _id,
718
- translations
719
- }`,
720
- { sourceLocale, sourceValue }
721
- );
722
- if (existingTerm) {
723
- const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
724
- existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
725
- {
726
- _type: "internationalizedArrayStringValue",
727
- _key: targetLocale,
728
- value: targetValue
729
- }
730
- ]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
731
- } else
732
- await client.create({
733
- _type: "translationGlossaryTerm",
734
- translations: [
735
- {
736
- _type: "internationalizedArrayStringValue",
737
- _key: sourceLocale,
738
- value: sourceValue
739
- },
740
- {
741
- _type: "internationalizedArrayStringValue",
742
- _key: targetLocale,
743
- value: targetValue
744
- }
745
- ]
746
- });
436
+ function isSanityMutationError(error) {
437
+ if (!error) return !1;
438
+ if (error instanceof Error) {
439
+ const message = error.message.toLowerCase();
440
+ return [
441
+ "validation",
442
+ "schema",
443
+ "invalid document",
444
+ "invalid field",
445
+ "type mismatch",
446
+ "reference",
447
+ "patch",
448
+ "mutation",
449
+ "document structure",
450
+ "required field",
451
+ "field type",
452
+ "sanity"
453
+ ].some((pattern) => message.includes(pattern));
747
454
  }
748
- devLog.info("Glossary terms processed", {
749
- processed: validTerms.length,
750
- skipped: terms.length - validTerms.length
751
- });
752
- }
753
- async function recordChangeHistory(client, settingsId, targetLanguage, suggestions) {
754
- const timestamp = (/* @__PURE__ */ new Date()).toISOString(), historyEntries = suggestions.map((suggestion) => ({
755
- _type: "object",
756
- _key: crypto.randomUUID(),
757
- timestamp,
758
- changeType: suggestion.type,
759
- description: suggestion.description,
760
- locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
761
- }));
762
- historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
763
- }
764
- const ICON_SIZE = 64, progressIconMap = {
765
- in_progress: /* @__PURE__ */ jsx(Spinner, {}),
766
- completed: /* @__PURE__ */ jsx(CheckmarkCircleIcon, { fontSize: ICON_SIZE }),
767
- error: /* @__PURE__ */ jsx(ErrorOutlineIcon, { fontSize: ICON_SIZE })
768
- }, ProgressPanel = ({
769
- title,
770
- description,
771
- tone,
772
- status = "in_progress",
773
- icon,
774
- ...props
775
- }) => /* @__PURE__ */ jsx(Card, { tone, padding: 4, ...props, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, align: "center", justify: "center", children: [
776
- icon || progressIconMap[status],
777
- /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
778
- title && /* @__PURE__ */ jsx(Text, { size: 2, weight: "bold", children: title }),
779
- description && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: description })
780
- ] })
781
- ] }) }), IMPROVEMENT_TYPE_LABELS = {
782
- globalPrompt: "Global Prompt",
783
- localePrompt: "Locale Prompt",
784
- protectedPhrase: "Protected Phrase",
785
- glossaryTerm: "Glossary Term"
786
- }, createNewSuggestion = (type = "protectedPhrase", sourceLanguage, targetLanguage) => {
787
- const baseSuggestion = {
788
- id: crypto.randomUUID(),
789
- type,
790
- description: "",
791
- approved: !0
792
- };
793
- return type === "glossaryTerm" ? {
794
- ...baseSuggestion,
795
- sourceValue: "",
796
- targetValue: "",
797
- sourceLocale: sourceLanguage,
798
- targetLocale: targetLanguage
799
- } : baseSuggestion;
800
- }, SuggestionReviewList = ({
801
- suggestions,
802
- onSuggestionsChange,
803
- sourceLanguage,
804
- targetLanguage
805
- }) => {
806
- const [editingId, setEditingId] = useState(null), handleToggleApproval = (id) => {
807
- const updated = suggestions.map((s) => s.id === id && s.validationStatus !== "invalid" ? { ...s, approved: !s.approved } : s);
808
- onSuggestionsChange(updated);
809
- }, handleTypeChange = (id, newType) => {
810
- const updated = suggestions.map((s) => {
811
- if (s.id !== id)
812
- return s;
813
- if (newType === "glossaryTerm" && s.type !== "glossaryTerm") {
814
- const match = s.description.match(/^(.+?)\s*→\s*(.+)$/);
815
- return {
816
- ...s,
817
- type: newType,
818
- sourceValue: match ? match[1].trim() : "",
819
- targetValue: match ? match[2].trim() : "",
820
- sourceLocale: sourceLanguage,
821
- targetLocale: targetLanguage
822
- };
823
- }
824
- return newType !== "glossaryTerm" && s.type === "glossaryTerm" ? {
825
- ...s,
826
- type: newType,
827
- description: s.sourceValue && s.targetValue ? `${s.sourceValue} \u2192 ${s.targetValue}` : s.description,
828
- sourceValue: void 0,
829
- targetValue: void 0
830
- } : { ...s, type: newType };
831
- });
832
- onSuggestionsChange(updated);
833
- }, handleDescriptionChange = (id, newDescription) => {
834
- const updated = suggestions.map((s) => s.id === id ? { ...s, description: newDescription } : s);
835
- onSuggestionsChange(updated);
836
- }, handleGlossarySourceChange = (id, newSource) => {
837
- const updated = suggestions.map(
838
- (s) => s.id === id ? {
839
- ...s,
840
- sourceValue: newSource,
841
- description: `${newSource} \u2192 ${s.targetValue || ""}`
842
- // Preserve sourceLocale and targetLocale
843
- } : s
844
- );
845
- onSuggestionsChange(updated);
846
- }, handleGlossaryTargetChange = (id, newTarget) => {
847
- const updated = suggestions.map(
848
- (s) => s.id === id ? {
849
- ...s,
850
- targetValue: newTarget,
851
- description: `${s.sourceValue || ""} \u2192 ${newTarget}`
852
- // Preserve sourceLocale and targetLocale
853
- } : s
854
- );
855
- onSuggestionsChange(updated);
856
- }, handleRemove = (id) => {
857
- const updated = suggestions.filter((s) => s.id !== id);
858
- onSuggestionsChange(updated);
859
- }, handleAddSuggestion = (type) => {
860
- const newSuggestion = createNewSuggestion(type, sourceLanguage, targetLanguage);
861
- onSuggestionsChange([...suggestions, newSuggestion]), setEditingId(newSuggestion.id);
862
- };
863
- return suggestions.length === 0 ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
864
- /* @__PURE__ */ jsx(
865
- ProgressPanel,
866
- {
867
- status: "error",
868
- tone: "transparent",
869
- title: "No suggestions generated",
870
- description: 'Try providing more specific feedback or enabling "Include Edits".',
871
- icon: /* @__PURE__ */ jsx(EmptyIcon, { fontSize: 24 })
872
- }
873
- ),
874
- /* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
875
- Button,
876
- {
877
- mode: "ghost",
878
- tone: "primary",
879
- icon: AddIcon,
880
- text: "Add Suggestion",
881
- fontSize: 1,
882
- onClick: () => handleAddSuggestion("protectedPhrase")
883
- }
884
- ) })
885
- ] }) : /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
886
- suggestions.map((suggestion) => {
887
- const isEditing = editingId === suggestion.id, isGlossaryTerm = suggestion.type === "glossaryTerm", isPrompt = suggestion.type === "globalPrompt" || suggestion.type === "localePrompt", hasWarning = suggestion.warning && suggestion.validationStatus !== "valid", isInvalid = suggestion.validationStatus === "invalid";
888
- return /* @__PURE__ */ jsx(
889
- Card,
890
- {
891
- padding: 4,
892
- border: !0,
893
- tone: isInvalid ? "critical" : hasWarning ? "caution" : suggestion.approved ? "default" : "transparent",
894
- style: { opacity: suggestion.approved ? 1 : 0.5 },
895
- children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
896
- /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: isInvalid ? /* @__PURE__ */ jsx(
897
- Tooltip,
898
- {
899
- content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This suggestion cannot be applied because it conflicts with existing settings. See the error message below for details." }) }),
900
- placement: "top",
901
- portal: !0,
902
- children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Checkbox, { checked: !1, disabled: !0, readOnly: !0 }) })
903
- }
904
- ) : /* @__PURE__ */ jsx(Checkbox, { checked: suggestion.approved, onChange: () => handleToggleApproval(suggestion.id) }) }),
905
- /* @__PURE__ */ jsxs(Stack, { space: 3, flex: 1, children: [
906
- /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", wrap: "wrap", children: [
907
- /* @__PURE__ */ jsx(Box, { flex: 1, style: { minWidth: "200px" }, children: /* @__PURE__ */ jsx(
908
- Select,
909
- {
910
- fontSize: 1,
911
- value: suggestion.type,
912
- onChange: (event) => handleTypeChange(suggestion.id, event.currentTarget.value),
913
- "aria-label": "Suggestion type",
914
- children: Object.entries(IMPROVEMENT_TYPE_LABELS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
915
- }
916
- ) }),
917
- /* @__PURE__ */ jsx(
918
- Button,
919
- {
920
- mode: "bleed",
921
- tone: "critical",
922
- icon: TrashIcon,
923
- fontSize: 1,
924
- padding: 2,
925
- onClick: () => handleRemove(suggestion.id),
926
- "aria-label": "Remove suggestion",
927
- title: "Remove suggestion"
928
- }
929
- )
930
- ] }),
931
- isGlossaryTerm ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
932
- (suggestion.sourceLocale || suggestion.targetLocale) && /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
933
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.sourceLocale ? `Source (${suggestion.sourceLocale})` : "Source" }) }),
934
- /* @__PURE__ */ jsx(Box, { style: { width: "20px" } }),
935
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.targetLocale ? `Target (${suggestion.targetLocale})` : "Target" }) })
936
- ] }),
937
- /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
938
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
939
- TextInput,
940
- {
941
- fontSize: 1,
942
- placeholder: "Source term",
943
- value: suggestion.sourceValue || "",
944
- onChange: (event) => handleGlossarySourceChange(suggestion.id, event.currentTarget.value),
945
- "aria-label": `Source term in ${suggestion.sourceLocale || "source language"}`
946
- }
947
- ) }),
948
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, "aria-hidden": "true", children: "\u2192" }),
949
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
950
- TextInput,
951
- {
952
- fontSize: 1,
953
- placeholder: "Target term",
954
- value: suggestion.targetValue || "",
955
- onChange: (event) => handleGlossaryTargetChange(suggestion.id, event.currentTarget.value),
956
- "aria-label": `Target term in ${suggestion.targetLocale || "target language"}`
957
- }
958
- ) })
959
- ] })
960
- ] }) : isPrompt ? /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(
961
- TextArea,
962
- {
963
- fontSize: 1,
964
- rows: isEditing ? 6 : 3,
965
- value: suggestion.description,
966
- onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
967
- onFocus: () => setEditingId(suggestion.id),
968
- onBlur: () => setEditingId(null),
969
- placeholder: "Enter prompt guidance...",
970
- "aria-label": `${IMPROVEMENT_TYPE_LABELS[suggestion.type]} content`
971
- }
972
- ) }) : /* @__PURE__ */ jsx(
973
- TextInput,
974
- {
975
- fontSize: 1,
976
- value: suggestion.description,
977
- onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
978
- placeholder: "Enter protected phrase...",
979
- "aria-label": "Protected phrase"
980
- }
981
- ),
982
- hasWarning && /* @__PURE__ */ jsx(Card, { padding: 3, tone: isInvalid ? "critical" : "caution", border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 2, children: [
983
- /* @__PURE__ */ jsx(Text, { size: 1, children: isInvalid ? /* @__PURE__ */ jsx(WarningOutlineIcon, {}) : /* @__PURE__ */ jsx(InfoOutlineIcon, {}) }),
984
- /* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
985
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: isInvalid ? "Invalid Suggestion" : "Warning" }),
986
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: suggestion.warning })
987
- ] })
988
- ] }) })
989
- ] })
990
- ] }) })
991
- },
992
- suggestion.id
993
- );
994
- }),
995
- /* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
996
- Button,
997
- {
998
- mode: "ghost",
999
- tone: "neutral",
1000
- icon: AddIcon,
1001
- text: "Add Suggestion",
1002
- fontSize: 1,
1003
- onClick: () => handleAddSuggestion("protectedPhrase")
1004
- }
1005
- ) })
1006
- ] });
1007
- }, GiveFeedbackDialog = ({ setIsDialogOpen }) => {
1008
- const client = useClient({ apiVersion: "vX" }), { document, config, translationMetadata } = useTranslationContext(), { loading } = useTranslationMetadata({
1009
- documentId: document._id
1010
- }), [feedback, setFeedback] = useState(""), [shouldIncludeEdits, setShouldIncludeEdits] = useState(!1), [processingState, setProcessingState] = useState("idle"), [suggestions, setSuggestions] = useState([]), [changeSummary, setChangeSummary] = useState(""), [error, setError] = useState(""), [sourceLanguage, setSourceLanguage] = useState(""), [targetLanguage, setTargetLanguage] = useState(""), handleShouldIncludeEditsChange = () => {
1011
- setShouldIncludeEdits(!shouldIncludeEdits);
1012
- }, handleCancel = () => {
1013
- setIsDialogOpen(!1);
1014
- }, handleAnalyze = async () => {
1015
- if (!feedback && !shouldIncludeEdits) {
1016
- setError('Please provide feedback or enable "Include Edits"');
1017
- return;
1018
- }
1019
- if (!config) {
1020
- setError("Missing plugin configuration");
1021
- return;
1022
- }
1023
- setError(""), setProcessingState("analyzing"), setChangeSummary("");
1024
- try {
1025
- const translationHistory2 = translationMetadata?.translationHistory?.find(
1026
- (history) => history.language === document[config.languageFieldName]
1027
- );
1028
- if (!translationHistory2)
1029
- throw new Error(
1030
- "No translation history found for this document. Please ensure this document was translated using the AI Translation plugin."
1031
- );
1032
- const { sourceSnapshot, targetSnapshot, sourceLanguage: srcLang } = translationHistory2;
1033
- if (!sourceSnapshot || !targetSnapshot)
1034
- throw new Error("Translation snapshots are missing from history.");
1035
- const srcLanguage = srcLang || "en-us", tgtLanguage = document[config.languageFieldName];
1036
- setSourceLanguage(srcLanguage), setTargetLanguage(tgtLanguage);
1037
- const generatedSuggestions = await analyzeFeedback({
1038
- client,
1039
- schemaId: config.schemaId,
1040
- sourceLanguage: srcLanguage,
1041
- targetLanguage: tgtLanguage,
1042
- userFeedback: feedback || void 0,
1043
- shouldIncludeEdits,
1044
- sourceSnapshot,
1045
- targetSnapshot,
1046
- currentDocument: shouldIncludeEdits ? document : void 0,
1047
- onProgress: ({ stage, message }) => {
1048
- setProcessingState(stage), setChangeSummary(message);
1049
- }
1050
- });
1051
- setSuggestions(generatedSuggestions), setProcessingState("reviewing");
1052
- } catch (err) {
1053
- console.error("Error analyzing feedback:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
1054
- }
1055
- }, handleApplySuggestions = async () => {
1056
- if (!config) {
1057
- setError("Missing plugin configuration");
1058
- return;
1059
- }
1060
- if (suggestions.filter((s) => s.approved).length === 0) {
1061
- setError("Please approve at least one suggestion to apply");
1062
- return;
1063
- }
1064
- setError(""), setProcessingState("applying");
1065
- try {
1066
- const translationHistory2 = translationMetadata?.translationHistory?.find(
1067
- (history) => history.language === document[config.languageFieldName]
1068
- );
1069
- if (!translationHistory2)
1070
- throw new Error("Translation history not found");
1071
- await applySuggestions({
1072
- client,
1073
- schemaId: config.schemaId,
1074
- sourceLanguage: translationHistory2.sourceLanguage || "en-us",
1075
- targetLanguage: translationHistory2.language,
1076
- suggestions,
1077
- onProgress: ({ stage, message }) => {
1078
- setProcessingState(stage), setChangeSummary(message);
1079
- }
1080
- }), setProcessingState("completed");
1081
- } catch (err) {
1082
- console.error("Error applying suggestions:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
1083
- }
1084
- };
1085
- if (loading)
1086
- return /* @__PURE__ */ jsx(ProgressPanel, { title: "Loading translation metadata" });
1087
- if (processingState === "analyzing")
1088
- return /* @__PURE__ */ jsx(ProgressPanel, { title: "Analyzing feedback with AI...", description: changeSummary });
1089
- if (processingState === "reviewing")
1090
- return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1091
- /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1092
- /* @__PURE__ */ jsx(Heading, { children: "Review Suggestions" }),
1093
- /* @__PURE__ */ jsx(Text, { muted: !0, children: "The AI has analyzed your feedback and generated the following suggestions. Review and edit them before applying." })
1094
- ] }),
1095
- /* @__PURE__ */ jsx(
1096
- SuggestionReviewList,
1097
- {
1098
- suggestions,
1099
- onSuggestionsChange: setSuggestions,
1100
- sourceLanguage,
1101
- targetLanguage
1102
- }
1103
- ),
1104
- error && /* @__PURE__ */ jsx(Card, { padding: 3, tone: "critical", border: !0, children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
1105
- /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, paddingTop: 2, children: [
1106
- /* @__PURE__ */ jsx(Flex, { gap: 2, children: /* @__PURE__ */ jsx(
1107
- Button,
1108
- {
1109
- type: "button",
1110
- mode: "bleed",
1111
- tone: "neutral",
1112
- onClick: () => setProcessingState("idle"),
1113
- icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
1114
- text: "Back"
1115
- }
1116
- ) }),
1117
- /* @__PURE__ */ jsxs(
1118
- Button,
1119
- {
1120
- type: "button",
1121
- mode: "ghost",
1122
- tone: "positive",
1123
- size: 1,
1124
- onClick: handleApplySuggestions,
1125
- disabled: suggestions.filter((s) => s.approved).length === 0,
1126
- children: [
1127
- "Apply ",
1128
- suggestions.filter((s) => s.approved).length,
1129
- " Suggestion",
1130
- suggestions.filter((s) => s.approved).length !== 1 ? "s" : ""
1131
- ]
1132
- }
1133
- )
1134
- ] })
1135
- ] });
1136
- if (processingState === "applying")
1137
- return /* @__PURE__ */ jsx(ProgressPanel, { title: "Updating translation settings...", description: changeSummary, icon: /* @__PURE__ */ jsx(EditIcon, {}) });
1138
- if (processingState === "completed") {
1139
- const appliedCount = suggestions.filter((s) => s.approved).length, description = `${appliedCount} suggestion${appliedCount !== 1 ? "s" : ""} applied. Translation prompts and glossary havebeen updated based on your feedback.`;
1140
- return /* @__PURE__ */ jsxs(Box, { padding: 4, children: [
1141
- /* @__PURE__ */ jsx(ProgressPanel, { title: "Translation settings updated", description, status: "completed" }),
1142
- /* @__PURE__ */ jsx(Flex, { justify: "flex-end", paddingTop: 3, children: /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", tone: "primary", size: 1, onClick: handleCancel, children: "Close" }) })
1143
- ] });
455
+ if (error && typeof error == "object") {
456
+ const errorObj = error;
457
+ if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
458
+ const statusCode = errorObj.statusCode;
459
+ if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
460
+ return !0;
461
+ }
1144
462
  }
1145
- return processingState === "error" ? /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1146
- /* @__PURE__ */ jsx(ProgressPanel, { status: "error", title: "Error processing feedback", description: error, tone: "critical" }),
1147
- /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
1148
- /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", onClick: handleCancel, children: "Close" }),
1149
- /* @__PURE__ */ jsx(Button, { type: "reset", mode: "ghost", tone: "primary", onClick: () => setProcessingState("idle"), children: "Reset" })
1150
- ] })
1151
- ] }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1152
- /* @__PURE__ */ jsx(Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1153
- /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
1154
- /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
1155
- /* @__PURE__ */ jsx(Text, { size: 2, muted: !0, children: "Enter your feedback for the translation AI agent." }),
1156
- /* @__PURE__ */ jsx(
1157
- Tooltip,
1158
- {
1159
- portal: !0,
1160
- content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "The AI agent will use your feedback to improve translation prompts, update protected phrases, and modify the glossary. Be specific about what should be changed." }) }),
1161
- children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(InfoOutlineIcon, {}) })
1162
- }
1163
- )
1164
- ] }),
1165
- /* @__PURE__ */ jsx(
1166
- TextArea,
463
+ return !1;
464
+ }
465
+ const DEFAULT_RETRY_OPTIONS = {
466
+ maxAttempts: 2,
467
+ initialDelayMs: 0,
468
+ maxDelayMs: 5e3,
469
+ backoffMultiplier: 2,
470
+ retryableErrors: isNetworkError,
471
+ logRetries: !0
472
+ };
473
+ function calculateDelay(attempt, options) {
474
+ const delay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt);
475
+ return Math.min(delay, options.maxDelayMs);
476
+ }
477
+ function sleep(ms) {
478
+ return new Promise((resolve) => setTimeout(resolve, ms));
479
+ }
480
+ async function retryWithBackoff(operation, options = {}) {
481
+ const opts = {
482
+ ...DEFAULT_RETRY_OPTIONS,
483
+ ...options,
484
+ retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
485
+ };
486
+ let lastError;
487
+ const maxAttempts = opts.maxAttempts + 1;
488
+ for (let attempt = 1; attempt <= maxAttempts; attempt++)
489
+ try {
490
+ const result = await operation();
491
+ return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
492
+ ...opts.context,
493
+ attempt,
494
+ totalAttempts: attempt
495
+ }), result;
496
+ } catch (error) {
497
+ if (lastError = error, !opts.retryableErrors(error))
498
+ throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
499
+ ...opts.context,
500
+ attempt,
501
+ error: error instanceof Error ? error.message : String(error)
502
+ }), error;
503
+ if (attempt >= maxAttempts)
504
+ throw opts.logRetries && devLog.error(
505
+ `Operation failed after ${maxAttempts} attempts`,
506
+ error instanceof Error ? error : new Error(String(error)),
1167
507
  {
1168
- rows: 8,
1169
- fontSize: 2,
1170
- onChange: (event) => setFeedback(event.currentTarget.value),
1171
- padding: 3,
1172
- placeholder: 'Example: "Braze" is a branded term and should not be translated.',
1173
- value: feedback
508
+ ...opts.context,
509
+ totalAttempts: maxAttempts
1174
510
  }
1175
- )
1176
- ] }) }),
1177
- /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsx(
1178
- Button,
1179
- {
1180
- type: "button",
1181
- mode: shouldIncludeEdits ? "ghost" : "bleed",
1182
- size: 2,
1183
- onClick: handleShouldIncludeEditsChange,
1184
- tone: shouldIncludeEdits ? "primary" : "default",
1185
- children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
1186
- /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Checkbox, { checked: shouldIncludeEdits, readOnly: !0, style: { pointerEvents: "none" } }) }),
1187
- /* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
1188
- /* @__PURE__ */ jsx(Text, { size: 2, weight: "medium", children: "Include Edits in Feedback" }),
1189
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, style: { textAlign: "left" }, children: "The AI will analyze edits you made to the translation." })
1190
- ] })
1191
- ] })
1192
- }
1193
- ) }) })
1194
- ] }) }),
1195
- /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, paddingTop: 2, children: [
1196
- /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", size: 1, onClick: handleCancel, children: "Cancel" }),
1197
- /* @__PURE__ */ jsx(
1198
- Button,
1199
- {
1200
- type: "submit",
1201
- mode: "ghost",
1202
- tone: "positive",
1203
- size: 1,
1204
- onClick: handleAnalyze,
1205
- disabled: !feedback.trim() && !shouldIncludeEdits,
1206
- children: "Analyze Feedback"
1207
- }
1208
- )
1209
- ] })
1210
- ] });
1211
- }, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
1212
- const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
1213
- return devLog.debug("ImproveTranslationPromptsAction initialized", {
1214
- documentId: document?._id,
1215
- documentType: document?._type
1216
- }), !document || !config.translatableSchemaTypes?.includes(document._type) ? (devLog.warn("Document not in translatable schema types", {
1217
- documentType: document?._type,
1218
- translatableTypes: config.translatableSchemaTypes
1219
- }), null) : config ? {
1220
- label: "Give Translation Feedback",
1221
- icon: TranslateIcon,
1222
- onHandle: () => {
1223
- devLog.info("Give Translation Feedback dialog opened", {
1224
- documentId: document._id,
1225
- documentType: document._type,
1226
- component: "GiveTranslationFeedbackAction"
1227
- }), setIsDialogOpen(!0);
1228
- },
1229
- dialog: isDialogOpen && {
1230
- type: "dialog",
1231
- onClose: () => setIsDialogOpen(!1),
1232
- header: "Give Translation Feedback",
1233
- content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
511
+ ), error;
512
+ const delay = calculateDelay(attempt - 1, opts);
513
+ opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
514
+ ...opts.context,
515
+ attempt,
516
+ totalAttempts: maxAttempts,
517
+ delayMs: delay,
518
+ error: error instanceof Error ? error.message : String(error)
519
+ }), delay > 0 && await sleep(delay);
1234
520
  }
1235
- } : (devLog.error("Missing config in TranslateAction"), null);
1236
- };
521
+ throw lastError;
522
+ }
1237
523
  class Semaphore {
1238
524
  permits;
1239
525
  queue = [];
@@ -1584,13 +870,27 @@ const fetchTranslationPrompts = async ({
1584
870
  }
1585
871
  }), hasChanges = !0);
1586
872
  }
1587
- hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
873
+ hasChanges && (await retryWithBackoff(
874
+ async () => {
875
+ await patch.commit();
876
+ },
877
+ {
878
+ maxAttempts: 2,
879
+ initialDelayMs: 0,
880
+ retryableErrors: isNetworkError,
881
+ context: {
882
+ operation: "localizeReferences",
883
+ documentId,
884
+ referenceCount: refs.length
885
+ }
886
+ }
887
+ ), devLog.info("Updated references in batch", {
1588
888
  documentId,
1589
889
  count: refs.length,
1590
890
  translatedCount: translationMap.size,
1591
891
  component: "updateReferences"
1592
892
  }));
1593
- }, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
893
+ }, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
1594
894
  for (const sourceDocument of sourceDocuments)
1595
895
  for (const lang of targetLanguages) {
1596
896
  const payload = {
@@ -1603,146 +903,8 @@ const fetchTranslationPrompts = async ({
1603
903
  dispatch({
1604
904
  type: "SET_TRANSLATION_JOB_STATUS",
1605
905
  payload
1606
- });
1607
- }
1608
- }, DEFAULT_OPTIONS = {
1609
- maxRetries: 3,
1610
- initialDelayMs: 1e3,
1611
- maxDelayMs: 3e4,
1612
- backoffMultiplier: 2,
1613
- jitter: !0
1614
- };
1615
- function isRetryableError(error) {
1616
- if (error instanceof Error) {
1617
- const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
1618
- "schema not found",
1619
- "invalid schema",
1620
- "not found",
1621
- "unauthorized",
1622
- "forbidden",
1623
- "invalid api key",
1624
- "missing api key",
1625
- "validation failed",
1626
- "invalid document",
1627
- "source language not found",
1628
- "invalid configuration",
1629
- "type mismatch"
1630
- ];
1631
- for (const pattern of nonRetryablePatterns)
1632
- if (message.includes(pattern) || name.includes(pattern))
1633
- return !1;
1634
- const retryablePatterns = [
1635
- "failed to fetch",
1636
- "network",
1637
- "timeout",
1638
- "econnreset",
1639
- "econnrefused",
1640
- "enotfound",
1641
- "socket",
1642
- "rate limit",
1643
- "too many requests",
1644
- "429",
1645
- "500",
1646
- "502",
1647
- "503",
1648
- "504",
1649
- "internal server error",
1650
- "bad gateway",
1651
- "service unavailable",
1652
- "gateway timeout",
1653
- "temporarily unavailable",
1654
- "try again"
1655
- ];
1656
- for (const pattern of retryablePatterns)
1657
- if (message.includes(pattern) || name.includes(pattern))
1658
- return !0;
1659
- }
1660
- if (typeof error == "object" && error !== null) {
1661
- const statusCode = error.statusCode || error.status;
1662
- if (statusCode) {
1663
- if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
1664
- return !0;
1665
- if (statusCode >= 400 && statusCode < 500)
1666
- return !1;
1667
- }
1668
- }
1669
- return !0;
1670
- }
1671
- function calculateBackoffDelay(attempt, options) {
1672
- const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
1673
- let delay = initialDelayMs * backoffMultiplier ** attempt;
1674
- if (delay = Math.min(delay, maxDelayMs), jitter) {
1675
- const jitterFactor = 0.75 + Math.random() * 0.5;
1676
- delay = Math.round(delay * jitterFactor);
1677
- }
1678
- return delay;
1679
- }
1680
- function sleep(ms) {
1681
- return new Promise((resolve) => setTimeout(resolve, ms));
1682
- }
1683
- async function withRetry(fn, options = {}) {
1684
- const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
1685
- let lastError, attempts = 0;
1686
- for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
1687
- attempts = attempt + 1;
1688
- try {
1689
- return { success: !0, result: await fn(), attempts };
1690
- } catch (error) {
1691
- if (lastError = error, attempt >= opts.maxRetries) {
1692
- devLog.debug("Max retries exhausted", { attempt: attempts, error });
1693
- break;
1694
- }
1695
- if (!shouldRetry(error)) {
1696
- devLog.debug("Error is not retryable", { attempt: attempts, error });
1697
- break;
1698
- }
1699
- const delay = calculateBackoffDelay(attempt, opts);
1700
- devLog.debug("Scheduling retry", {
1701
- attempt: attempts,
1702
- nextAttempt: attempt + 2,
1703
- delayMs: delay,
1704
- error: error instanceof Error ? error.message : String(error)
1705
- }), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
906
+ }), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
1706
907
  }
1707
- }
1708
- return { success: !1, error: lastError, attempts };
1709
- }
1710
- const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1711
- (translation) => translation._key === targetLanguage
1712
- )?.value._ref || null, getTranslationMetadata = async ({
1713
- client,
1714
- sourceDocument,
1715
- sourceLanguage
1716
- }) => {
1717
- try {
1718
- return await client.fetch(
1719
- "*[_type == $tmType && references($sourceDocumentId)][0]",
1720
- {
1721
- tmType: TRANSLATION_METADATA_TYPE,
1722
- sourceDocumentId: undraftId(sourceDocument._id)
1723
- }
1724
- ) || await client.create({
1725
- _type: TRANSLATION_METADATA_TYPE,
1726
- translations: [
1727
- {
1728
- _key: sourceLanguage,
1729
- _type: "internationalizedArrayReferenceValue",
1730
- value: {
1731
- _type: "reference",
1732
- _ref: undraftId(sourceDocument._id),
1733
- _weak: !0,
1734
- _strengthenOnPublish: {
1735
- _type: sourceDocument._type,
1736
- _weak: !1
1737
- }
1738
- }
1739
- }
1740
- ],
1741
- translationHistory: []
1742
- });
1743
- } catch (error) {
1744
- throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
1745
- }
1746
908
  }, updateTranslationMetadata = async ({
1747
909
  client,
1748
910
  targetLanguage,
@@ -1776,11 +938,71 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
1776
938
  targetRev: newDocument._rev,
1777
939
  targetSnapshot: JSON.stringify(newDocument)
1778
940
  }, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
1779
- isNewTranslation ? await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit() : await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
941
+ isNewTranslation ? await retryWithBackoff(
942
+ async () => {
943
+ await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
944
+ },
945
+ {
946
+ maxAttempts: 2,
947
+ initialDelayMs: 0,
948
+ retryableErrors: isNetworkError,
949
+ context: {
950
+ operation: "updateTranslationMetadata.translations",
951
+ sourceDocumentId: sourceDocument._id,
952
+ targetLanguage,
953
+ isNewTranslation: !0
954
+ }
955
+ }
956
+ ) : await retryWithBackoff(
957
+ async () => {
958
+ await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
959
+ },
960
+ {
961
+ maxAttempts: 2,
962
+ initialDelayMs: 0,
963
+ retryableErrors: isNetworkError,
964
+ context: {
965
+ operation: "updateTranslationMetadata.translations",
966
+ sourceDocumentId: sourceDocument._id,
967
+ targetLanguage,
968
+ isNewTranslation: !1
969
+ }
970
+ }
971
+ );
1780
972
  const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
1781
973
  translationMetadata.translationHistory?.some(
1782
974
  (history) => history.language === targetLanguage
1783
- ) ? await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit() : await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
975
+ ) ? await retryWithBackoff(
976
+ async () => {
977
+ await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
978
+ },
979
+ {
980
+ maxAttempts: 2,
981
+ initialDelayMs: 0,
982
+ retryableErrors: isNetworkError,
983
+ context: {
984
+ operation: "updateTranslationMetadata.history",
985
+ sourceDocumentId: sourceDocument._id,
986
+ targetLanguage,
987
+ isNewHistory: !1
988
+ }
989
+ }
990
+ ) : await retryWithBackoff(
991
+ async () => {
992
+ await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
993
+ },
994
+ {
995
+ maxAttempts: 2,
996
+ initialDelayMs: 0,
997
+ retryableErrors: isNetworkError,
998
+ context: {
999
+ operation: "updateTranslationMetadata.history",
1000
+ sourceDocumentId: sourceDocument._id,
1001
+ targetLanguage,
1002
+ isNewHistory: !0
1003
+ }
1004
+ }
1005
+ );
1784
1006
  } catch (error) {
1785
1007
  throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
1786
1008
  sourceDocumentId: sourceDocument._id,
@@ -1789,15 +1011,16 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
1789
1011
  component: "updateTranslationMetadata"
1790
1012
  }), error;
1791
1013
  }
1792
- }, setJobStatus = (dispatch, _id, language, status, payload) => {
1014
+ }, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
1015
+ const job = defaults(payload, {
1016
+ _id,
1017
+ language,
1018
+ status
1019
+ });
1793
1020
  dispatch({
1794
1021
  type: "SET_TRANSLATION_JOB_STATUS",
1795
- payload: defaults(payload, {
1796
- _id,
1797
- language,
1798
- status
1799
- })
1800
- });
1022
+ payload: job
1023
+ }), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
1801
1024
  };
1802
1025
  function shouldExcludeField(fieldPath, excludeFields) {
1803
1026
  for (const pattern of excludeFields) {
@@ -2123,14 +1346,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2123
1346
  }), devLog.debug("OpenAI GPT-5 Full Prompt", {
2124
1347
  messages: request.messages
2125
1348
  });
2126
- const response = await fetch("https://api.openai.com/v1/responses", {
2127
- method: "POST",
2128
- headers: {
2129
- "Content-Type": "application/json",
2130
- Authorization: `Bearer ${apiKey}`
1349
+ const response = await retryWithBackoff(
1350
+ async () => {
1351
+ const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
1352
+ method: "POST",
1353
+ headers: {
1354
+ "Content-Type": "application/json",
1355
+ Authorization: `Bearer ${apiKey}`
1356
+ },
1357
+ body: JSON.stringify(requestBody)
1358
+ });
1359
+ if (!fetchResponse.ok) {
1360
+ const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
1361
+ throw errorObj.response = { status: fetchResponse.status }, errorObj;
1362
+ }
1363
+ return fetchResponse;
2131
1364
  },
2132
- body: JSON.stringify(requestBody)
2133
- });
1365
+ {
1366
+ maxAttempts: 2,
1367
+ initialDelayMs: 0,
1368
+ retryableErrors: isNetworkError,
1369
+ context: { model: requestBody.model, endpoint: "responses" }
1370
+ }
1371
+ );
2134
1372
  if (!response.ok) {
2135
1373
  const error = await response.json();
2136
1374
  throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
@@ -2169,14 +1407,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2169
1407
  }), devLog.debug("OpenAI Chat Completions Full Prompt", {
2170
1408
  messages: request.messages
2171
1409
  });
2172
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
2173
- method: "POST",
2174
- headers: {
2175
- "Content-Type": "application/json",
2176
- Authorization: `Bearer ${apiKey}`
1410
+ const response = await retryWithBackoff(
1411
+ async () => {
1412
+ const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
1413
+ method: "POST",
1414
+ headers: {
1415
+ "Content-Type": "application/json",
1416
+ Authorization: `Bearer ${apiKey}`
1417
+ },
1418
+ body: JSON.stringify(requestBody)
1419
+ });
1420
+ if (!fetchResponse.ok) {
1421
+ const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
1422
+ throw errorObj.response = { status: fetchResponse.status }, errorObj;
1423
+ }
1424
+ return fetchResponse;
2177
1425
  },
2178
- body: JSON.stringify(requestBody)
2179
- });
1426
+ {
1427
+ maxAttempts: 2,
1428
+ initialDelayMs: 0,
1429
+ retryableErrors: isNetworkError,
1430
+ context: { model, endpoint: "chat/completions" }
1431
+ }
1432
+ );
2180
1433
  if (!response.ok) {
2181
1434
  const error = await response.json();
2182
1435
  throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
@@ -2331,7 +1584,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2331
1584
  parent && parent.children.push(node);
2332
1585
  } else
2333
1586
  root.push(node);
2334
- else
1587
+ else if (attributes.type === "string" && !content) {
1588
+ const contentLines = [];
1589
+ for (i++; i < lines.length; ) {
1590
+ const currentLine = lines[i];
1591
+ if (currentLine.trim() === "</field>")
1592
+ break;
1593
+ contentLines.push(currentLine), i++;
1594
+ }
1595
+ if (node.text = contentLines.join(`
1596
+ `).trim(), stack.length > 0) {
1597
+ const parent = stack[stack.length - 1];
1598
+ parent && parent.children.push(node);
1599
+ } else
1600
+ root.push(node);
1601
+ } else
2335
1602
  stack.push(node);
2336
1603
  continue;
2337
1604
  }
@@ -2595,7 +1862,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2595
1862
  languageFieldName,
2596
1863
  translationPrompts,
2597
1864
  targetLanguage,
2598
- openaiConfig
1865
+ openaiConfig,
1866
+ previousError,
1867
+ errorContext
2599
1868
  }) => {
2600
1869
  const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2601
1870
  sourceId: sourceDocument._id,
@@ -2613,7 +1882,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2613
1882
  sourceText,
2614
1883
  sourceLanguage,
2615
1884
  targetLanguage,
2616
- translationPrompts
1885
+ translationPrompts,
1886
+ errorContext
2617
1887
  }), response = await callGPT5(openaiConfig.apiKey, {
2618
1888
  messages: [
2619
1889
  {
@@ -2671,14 +1941,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2671
1941
  sourceText,
2672
1942
  sourceLanguage,
2673
1943
  targetLanguage,
2674
- translationPrompts
1944
+ translationPrompts,
1945
+ errorContext
2675
1946
  }) => {
2676
1947
  const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2677
1948
  return `# TRANSLATION TASK
2678
1949
 
2679
1950
  Translate this ${sourceLanguage} document into ${targetLanguage}.
2680
1951
 
2681
- ## Guidelines
1952
+ ${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
1953
+
1954
+ ${errorContext}
1955
+
1956
+ Please carefully review the error above and ensure your translation fixes the issue.
1957
+
1958
+ ` : ""}## Guidelines
2682
1959
 
2683
1960
  ${globalPrompt ? `### General Guidelines
2684
1961
  ${globalPrompt}
@@ -2852,21 +2129,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2852
2129
  languageFieldName,
2853
2130
  targetLanguage
2854
2131
  });
2855
- return await client.agent.action.translate({
2856
- schemaId,
2857
- toLanguage,
2858
- fromLanguage,
2859
- documentId: sourceDocument._id,
2860
- noWrite: !0,
2861
- // Do not write to the document, just return the translated content
2862
- languageFieldPath: [languageFieldName],
2863
- styleGuide,
2864
- protectedPhrases: translationPrompts.protectedPhrases,
2865
- conditionalPaths: {
2866
- defaultHidden: !1,
2867
- defaultReadOnly: !1
2132
+ return await retryWithBackoff(
2133
+ async () => await client.agent.action.translate({
2134
+ schemaId,
2135
+ toLanguage,
2136
+ fromLanguage,
2137
+ documentId: sourceDocument._id,
2138
+ noWrite: !0,
2139
+ // Do not write to the document, just return the translated content
2140
+ languageFieldPath: [languageFieldName],
2141
+ styleGuide,
2142
+ protectedPhrases: translationPrompts.protectedPhrases,
2143
+ conditionalPaths: {
2144
+ defaultHidden: !1,
2145
+ defaultReadOnly: !1
2146
+ }
2147
+ }),
2148
+ {
2149
+ maxAttempts: 2,
2150
+ initialDelayMs: 0,
2151
+ retryableErrors: isNetworkError,
2152
+ context: {
2153
+ operation: "agent.action.translate",
2154
+ documentId: sourceDocument._id,
2155
+ targetLanguage,
2156
+ schemaId
2157
+ }
2868
2158
  }
2869
- });
2159
+ );
2870
2160
  }, handleTranslation = async ({
2871
2161
  dispatch,
2872
2162
  client,
@@ -2879,75 +2169,99 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2879
2169
  translationMode,
2880
2170
  openaiConfig,
2881
2171
  releaseId,
2882
- excludeFields = []
2172
+ excludeFields = [],
2173
+ replaceExisting = !1,
2174
+ mainSourceDocumentId
2883
2175
  }) => {
2884
- devLog.info("Filtering excluded fields before translation", {
2176
+ const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
2177
+ excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
2178
+ documentId: sourceDocument._id,
2179
+ languageFieldName,
2180
+ originalExcludeFields: excludeFields,
2181
+ safeExcludeFields
2182
+ }), devLog.info("Filtering excluded fields before translation", {
2885
2183
  documentId: sourceDocument._id,
2886
2184
  documentType: sourceDocument._type,
2887
- excludeFields,
2888
- excludeFieldsLength: excludeFields?.length ?? 0,
2185
+ excludeFields: safeExcludeFields,
2186
+ excludeFieldsLength: safeExcludeFields?.length ?? 0,
2889
2187
  sourceHasTitle: "title" in sourceDocument,
2890
2188
  sourceTitleValue: sourceDocument.title
2891
2189
  });
2892
- const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, excludeFields);
2190
+ const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
2893
2191
  devLog.info("After filtering excluded fields", {
2894
2192
  documentId: sourceDocument._id,
2895
2193
  filteredHasTitle: "title" in filteredDocument,
2896
2194
  filteredTitleValue: filteredDocument.title,
2897
2195
  excludedValuesKeys: Object.keys(excludedValues),
2898
2196
  excludedValues
2899
- }), devLog.debug("Applying field transforms before translation", {
2900
- documentId: sourceDocument._id,
2901
- documentType: sourceDocument._type
2902
- });
2903
- const { transformedDocument, originalStructure } = applyTransforms({
2904
- document: filteredDocument,
2905
- registry: globalTransformRegistry,
2906
- schemaType: sourceDocument._type
2907
2197
  });
2908
- let translatedDoc;
2909
- switch (translationMode) {
2910
- case "sanity-only": {
2911
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2912
- sourceDocument: transformedDocument,
2913
- targetLanguage,
2914
- languageFieldName,
2915
- schemaId,
2916
- client,
2917
- translationPrompts
2918
- });
2919
- break;
2920
- }
2921
- case "openai-only": {
2922
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2923
- throw new Error("OpenAI is not configured but openai-only mode was selected");
2924
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2925
- dispatch,
2926
- sourceDocument: transformedDocument,
2927
- targetLanguage,
2928
- languageFieldName,
2929
- translationPrompts,
2930
- openaiConfig
2931
- });
2932
- break;
2198
+ let documentToTranslate, originalStructure = {};
2199
+ if (translationMode === "openai-only") {
2200
+ devLog.debug("Applying field transforms before translation (OpenAI mode)", {
2201
+ documentId: sourceDocument._id,
2202
+ documentType: sourceDocument._type
2203
+ });
2204
+ const transformResult = applyTransforms({
2205
+ document: filteredDocument,
2206
+ registry: globalTransformRegistry,
2207
+ schemaType: sourceDocument._type
2208
+ });
2209
+ documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
2210
+ } else
2211
+ devLog.debug("Skipping field transforms (Sanity Agent mode)", {
2212
+ documentId: sourceDocument._id,
2213
+ documentType: sourceDocument._type
2214
+ }), documentToTranslate = filteredDocument;
2215
+ let translatedDoc, previousError = null;
2216
+ const performTranslation = async (errorContext) => {
2217
+ switch (translationMode) {
2218
+ case "sanity-only":
2219
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
2220
+ sourceDocument: documentToTranslate,
2221
+ targetLanguage,
2222
+ languageFieldName,
2223
+ schemaId,
2224
+ client,
2225
+ translationPrompts
2226
+ });
2227
+ case "openai-only": {
2228
+ if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2229
+ throw new Error("OpenAI is not configured but openai-only mode was selected");
2230
+ return setJobStatus(
2231
+ dispatch,
2232
+ sourceDocument._id,
2233
+ targetLanguage,
2234
+ "translating_with_openai",
2235
+ void 0,
2236
+ mainSourceDocumentId
2237
+ ), await getOpenAITranslation({
2238
+ dispatch,
2239
+ sourceDocument: documentToTranslate,
2240
+ targetLanguage,
2241
+ languageFieldName,
2242
+ translationPrompts,
2243
+ openaiConfig,
2244
+ previousError: errorContext && previousError ? previousError : void 0,
2245
+ errorContext
2246
+ });
2247
+ }
2248
+ default:
2249
+ throw new Error(`Invalid translation mode: ${translationMode}`);
2933
2250
  }
2934
- case "sanity-with-review":
2935
- throw new Error("Sanity with review translation method is not implemented");
2936
- default:
2937
- throw new Error(`Invalid translation mode: ${translationMode}`);
2938
- }
2939
- if (!translatedDoc)
2251
+ };
2252
+ if (translatedDoc = await performTranslation(), !translatedDoc)
2940
2253
  return null;
2941
- devLog.debug("Restoring field transforms after translation", {
2254
+ let transformedResult;
2255
+ Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
2942
2256
  documentId: sourceDocument._id,
2943
2257
  documentType: sourceDocument._type
2944
- });
2945
- const transformedResult = restoreTransforms({
2258
+ }), transformedResult = restoreTransforms({
2946
2259
  translatedDocument: translatedDoc,
2947
2260
  originalStructure,
2948
2261
  registry: globalTransformRegistry,
2949
2262
  schemaType: sourceDocument._type
2950
- }), excludedValuesToRestore = { ...excludedValues };
2263
+ })) : transformedResult = translatedDoc;
2264
+ const excludedValuesToRestore = { ...excludedValues };
2951
2265
  delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
2952
2266
  documentId: sourceDocument._id,
2953
2267
  excludedFieldCount: Object.keys(excludedValues).length,
@@ -2966,11 +2280,76 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2966
2280
  ...finalDoc,
2967
2281
  _id: targetDocumentVersionId
2968
2282
  };
2969
- return await client.action({
2970
- actionType: "sanity.action.document.version.create",
2971
- publishedId: targetDocumentPublishedId,
2972
- document: newDoc
2973
- }), newDoc;
2283
+ replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
2284
+ documentId: sourceDocument._id,
2285
+ targetDocumentVersionId,
2286
+ releaseId
2287
+ }), await client.delete(targetDocumentVersionId));
2288
+ try {
2289
+ await retryWithBackoff(
2290
+ async () => {
2291
+ await client.action({
2292
+ actionType: "sanity.action.document.version.create",
2293
+ publishedId: targetDocumentPublishedId,
2294
+ document: newDoc
2295
+ });
2296
+ },
2297
+ {
2298
+ maxAttempts: 2,
2299
+ initialDelayMs: 0,
2300
+ retryableErrors: isNetworkError,
2301
+ context: {
2302
+ documentId: sourceDocument._id,
2303
+ targetLanguage,
2304
+ operation: "version.create"
2305
+ }
2306
+ }
2307
+ );
2308
+ } catch (error) {
2309
+ if (translationMode === "openai-only" && isSanityMutationError(error)) {
2310
+ devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
2311
+ documentId: sourceDocument._id,
2312
+ targetLanguage,
2313
+ error: error instanceof Error ? error.message : String(error)
2314
+ }), previousError = error instanceof Error ? error : new Error(String(error));
2315
+ const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
2316
+ if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
2317
+ throw new Error("Translation retry failed: no document returned");
2318
+ let retryTransformedResult;
2319
+ Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
2320
+ translatedDocument: translatedDoc,
2321
+ originalStructure,
2322
+ registry: globalTransformRegistry,
2323
+ schemaType: sourceDocument._type
2324
+ }) : retryTransformedResult = translatedDoc;
2325
+ const retryNewDoc = {
2326
+ ...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
2327
+ _id: targetDocumentVersionId
2328
+ };
2329
+ return await retryWithBackoff(
2330
+ async () => {
2331
+ await client.action({
2332
+ actionType: "sanity.action.document.version.create",
2333
+ publishedId: targetDocumentPublishedId,
2334
+ document: retryNewDoc
2335
+ });
2336
+ },
2337
+ {
2338
+ maxAttempts: 2,
2339
+ initialDelayMs: 0,
2340
+ retryableErrors: isNetworkError,
2341
+ context: {
2342
+ documentId: sourceDocument._id,
2343
+ targetLanguage,
2344
+ operation: "version.create",
2345
+ isRetry: !0
2346
+ }
2347
+ }
2348
+ ), retryNewDoc;
2349
+ }
2350
+ throw error;
2351
+ }
2352
+ return newDoc;
2974
2353
  }, translateDocument = async ({
2975
2354
  dispatch,
2976
2355
  client,
@@ -2978,84 +2357,66 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2978
2357
  sourceLanguage,
2979
2358
  targetLanguage,
2980
2359
  languageFieldName,
2981
- shouldOverwriteFreshTranslations,
2982
2360
  schemaId,
2983
2361
  translationPrompts,
2984
2362
  translationMode,
2985
2363
  openaiConfig,
2986
2364
  releaseId,
2987
- excludeFields
2365
+ excludeFields,
2366
+ replaceExisting,
2367
+ mainSourceDocumentId
2988
2368
  }) => {
2989
2369
  const logContext = {
2990
2370
  documentId: sourceDocument._id,
2991
2371
  targetLanguage,
2992
2372
  schemaId,
2993
- shouldOverwriteFreshTranslations,
2994
2373
  translationMode
2995
2374
  };
2996
- devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2375
+ devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
2997
2376
  try {
2998
2377
  const translationMetadata = await getTranslationMetadata({
2999
2378
  client,
3000
2379
  sourceDocument,
3001
2380
  sourceLanguage
3002
- }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
3003
- (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
3004
- );
3005
- if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
3006
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
3007
- targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
3008
- });
3009
- return;
3010
- }
3011
- const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
3012
- () => handleTranslation({
2381
+ }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2382
+ dispatch,
2383
+ client,
2384
+ schemaId,
2385
+ sourceDocument,
2386
+ targetLanguage,
2387
+ existingTranslationId,
2388
+ languageFieldName,
2389
+ translationPrompts,
2390
+ translationMode,
2391
+ openaiConfig,
2392
+ releaseId,
2393
+ excludeFields,
2394
+ replaceExisting,
2395
+ mainSourceDocumentId
2396
+ });
2397
+ if (!newDocument) {
2398
+ devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
3013
2399
  dispatch,
3014
- client,
3015
- schemaId,
3016
- sourceDocument,
2400
+ sourceDocument._id,
3017
2401
  targetLanguage,
3018
- existingTranslationId,
3019
- languageFieldName,
3020
- translationPrompts,
3021
- translationMode,
3022
- openaiConfig,
3023
- releaseId,
3024
- excludeFields
3025
- }),
3026
- {
3027
- maxRetries: 3,
3028
- initialDelayMs: 1e3,
3029
- isRetryable: isRetryableError,
3030
- onRetry: (attempt, error, delayMs) => {
3031
- devLog.warn("Translation failed, scheduling retry", {
3032
- ...logContext,
3033
- attempt,
3034
- delayMs,
3035
- error: error instanceof Error ? error.message : String(error)
3036
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
3037
- retryCount: attempt,
3038
- errorMessage: error instanceof Error ? error.message : String(error)
3039
- });
3040
- }
3041
- }
3042
- );
3043
- if (!translationResult.success || !translationResult.result) {
3044
- const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
3045
- devLog.error("Translation failed after retries", translationResult.error, {
3046
- ...logContext,
3047
- attempts: translationResult.attempts
3048
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
3049
- errorMessage,
3050
- retryCount: translationResult.attempts - 1
3051
- // -1 because attempts includes initial try
3052
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2402
+ "error",
2403
+ {
2404
+ errorMessage: "Translation failed"
2405
+ },
2406
+ mainSourceDocumentId
2407
+ ), devLog.timeEnd("translateDocument"), devLog.groupEnd();
3053
2408
  return;
3054
2409
  }
3055
- const newDocument = translationResult.result;
3056
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
3057
- targetId: newDocument._id
3058
- }), await updateTranslationMetadata({
2410
+ return setJobStatus(
2411
+ dispatch,
2412
+ sourceDocument._id,
2413
+ targetLanguage,
2414
+ "updating_metadata",
2415
+ {
2416
+ targetId: newDocument._id
2417
+ },
2418
+ mainSourceDocumentId
2419
+ ), await updateTranslationMetadata({
3059
2420
  client,
3060
2421
  sourceDocument,
3061
2422
  sourceLanguage,
@@ -3063,17 +2424,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3063
2424
  targetLanguage,
3064
2425
  translationMetadata,
3065
2426
  isNewTranslation: !existingTranslationId
3066
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
3067
- targetId: newDocument._id
3068
- }), devLog.info("Translation completed successfully", {
2427
+ }), setJobStatus(
2428
+ dispatch,
2429
+ sourceDocument._id,
2430
+ targetLanguage,
2431
+ "completed",
2432
+ {
2433
+ targetId: newDocument._id
2434
+ },
2435
+ mainSourceDocumentId
2436
+ ), devLog.info("Translation completed successfully", {
3069
2437
  ...logContext,
3070
2438
  newDocumentId: newDocument._id
3071
2439
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
3072
2440
  } catch (error) {
3073
2441
  const errorMessage = error instanceof Error ? error.message : String(error);
3074
- devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
3075
- errorMessage
3076
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2442
+ devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
2443
+ dispatch,
2444
+ sourceDocument._id,
2445
+ targetLanguage,
2446
+ "error",
2447
+ {
2448
+ errorMessage
2449
+ },
2450
+ mainSourceDocumentId
2451
+ ), devLog.timeEnd("translateDocument"), devLog.groupEnd();
3077
2452
  return;
3078
2453
  }
3079
2454
  }, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
@@ -3082,13 +2457,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3082
2457
  targetLanguages,
3083
2458
  dispatch,
3084
2459
  languageFieldName,
3085
- shouldOverwriteFreshTranslations,
3086
2460
  schemaId,
3087
2461
  references = [],
3088
2462
  maxConcurrentTranslationJobs,
3089
2463
  translationMode,
3090
2464
  openaiConfig,
3091
- excludeFields
2465
+ excludeFields,
2466
+ releaseConfig
3092
2467
  }) => {
3093
2468
  const logContext = {
3094
2469
  documentId: sourceDocument._id,
@@ -3097,28 +2472,48 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3097
2472
  referencesCount: references.length,
3098
2473
  maxConcurrentJobs: maxConcurrentTranslationJobs,
3099
2474
  translationMode,
3100
- shouldOverwriteFreshTranslations
2475
+ releaseMode: releaseConfig.mode
3101
2476
  };
3102
- let releaseId;
2477
+ let releaseId, createdNewRelease = !1;
3103
2478
  try {
3104
2479
  const sourceLanguage = sourceDocument[languageFieldName];
3105
2480
  if (!sourceLanguage)
3106
2481
  throw devLog.error("Source language not found"), new Error("Source language not found");
3107
- maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2482
+ if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
3108
2483
  type: "SET_OVERALL_STATUS",
3109
2484
  payload: "translating"
3110
- });
3111
- const releaseTitle = `[AI Translation] ${sourceDocument.title || sourceDocument._id.split("-").slice(0, 2).join("-")} (${sourceDocument._type}) ${sourceLanguage} \u2192 ${targetLanguages.join(", ")}`, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`, { releaseId: releaseId2 } = await client.releases.create({
3112
- metadata: {
3113
- title: releaseTitle,
3114
- description: releaseDescription,
3115
- releaseType: "undecided"
3116
- }
3117
- });
3118
- devLog.info("Content Release created", {
3119
- ...logContext,
3120
- releaseId: releaseId2
3121
- });
2485
+ }), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
2486
+ releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
2487
+ ...logContext,
2488
+ releaseId
2489
+ });
2490
+ else {
2491
+ const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
2492
+ releaseId = (await retryWithBackoff(
2493
+ async () => await client.releases.create({
2494
+ metadata: {
2495
+ title: releaseTitle,
2496
+ description: releaseDescription,
2497
+ releaseType: "undecided"
2498
+ }
2499
+ }),
2500
+ {
2501
+ maxAttempts: 2,
2502
+ initialDelayMs: 0,
2503
+ retryableErrors: isNetworkError,
2504
+ context: {
2505
+ operation: "releases.create",
2506
+ releaseTitle,
2507
+ ...logContext
2508
+ }
2509
+ }
2510
+ )).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
2511
+ ...logContext,
2512
+ releaseId,
2513
+ releaseTitle
2514
+ });
2515
+ }
2516
+ registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
3122
2517
  const sourceDocuments = [
3123
2518
  {
3124
2519
  _id: sourceDocument._id,
@@ -3133,7 +2528,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3133
2528
  targetLanguages,
3134
2529
  sourceDocument
3135
2530
  });
3136
- setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2531
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
3137
2532
  const translationTasks = sourceDocumentIds.flatMap(
3138
2533
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3139
2534
  try {
@@ -3141,7 +2536,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3141
2536
  const document = await client.withConfig({
3142
2537
  perspective: "drafts"
3143
2538
  }).getDocument(documentId);
3144
- if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2539
+ if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
3145
2540
  const versionedTargetDocumentId = await translateDocument({
3146
2541
  dispatch,
3147
2542
  client,
@@ -3149,13 +2544,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3149
2544
  sourceLanguage,
3150
2545
  targetLanguage,
3151
2546
  languageFieldName,
3152
- shouldOverwriteFreshTranslations,
3153
2547
  schemaId,
3154
2548
  translationPrompts,
3155
2549
  translationMode,
3156
2550
  openaiConfig,
3157
- releaseId: releaseId2,
3158
- excludeFields
2551
+ releaseId,
2552
+ excludeFields,
2553
+ replaceExisting: releaseConfig.replaceExisting,
2554
+ // Pass main source document ID for active translations tracking
2555
+ mainSourceDocumentId: sourceDocument._id
3159
2556
  });
3160
2557
  versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
3161
2558
  }
@@ -3182,7 +2579,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3182
2579
  }), dispatch({
3183
2580
  type: "SET_OVERALL_STATUS",
3184
2581
  payload: "linking_references"
3185
- }), await Promise.all(
2582
+ }), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
3186
2583
  versionedTargetDocumentIds.map(async (documentId) => {
3187
2584
  await localizeReferences({
3188
2585
  client,
@@ -3193,18 +2590,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3193
2590
  })
3194
2591
  );
3195
2592
  const hasAnyFailures = failed > 0;
3196
- hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" })) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
2593
+ hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id)) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
3197
2594
  ...logContext,
3198
2595
  succeeded,
3199
2596
  failed
3200
- }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
2597
+ }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)) : (dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
3201
2598
  } catch (error) {
3202
- throw devLog.error(
2599
+ if (devLog.error(
3203
2600
  "translateDocumentSet failed with unexpected error",
3204
2601
  error instanceof Error ? error : new Error(String(error)),
3205
2602
  logContext
3206
- ), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
2603
+ ), releaseId && createdNewRelease)
2604
+ try {
2605
+ await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
2606
+ ...logContext,
2607
+ releaseId
2608
+ });
2609
+ } catch (cleanupError) {
2610
+ devLog.error(
2611
+ "Failed to cleanup release",
2612
+ cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
2613
+ logContext
2614
+ );
2615
+ }
2616
+ throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
3207
2617
  }
2618
+ }, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
2619
+ const uniqueDocuments = useMemo(() => new Set(conflicts.map((c) => c.sourceDocumentId)).size, [conflicts]), summaryText = useMemo(() => conflicts.length === 1 ? "1 translation" : uniqueDocuments === 1 ? `${conflicts.length} translations` : `${conflicts.length} translations across ${uniqueDocuments} documents`, [conflicts.length, uniqueDocuments]);
2620
+ return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2621
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2622
+ /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
2623
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
2624
+ ] }),
2625
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2626
+ "The selected release already has ",
2627
+ summaryText,
2628
+ " that will be overwritten if you continue:"
2629
+ ] }),
2630
+ /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsx(Box, { style: { maxHeight: "500px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 2, children: conflicts.map((conflict) => /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2631
+ conflict.sourceDocumentTitle || conflict.sourceDocumentId,
2632
+ conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
2633
+ " \u2192 ",
2634
+ conflict.targetLanguage
2635
+ ] }, conflict.existingVersionId)) }) }) }),
2636
+ /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2637
+ /* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
2638
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2639
+ /* @__PURE__ */ jsx("strong", { children: "Cancel" }),
2640
+ " \u2014 Go back and select a different release, or create a new one."
2641
+ ] }),
2642
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2643
+ /* @__PURE__ */ jsx("strong", { children: "Replace" }),
2644
+ " \u2014 Delete the existing translations in this release and create new ones."
2645
+ ] })
2646
+ ] }),
2647
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
2648
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
2649
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
2650
+ ] })
2651
+ ] }) });
3208
2652
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
3209
2653
  const {
3210
2654
  document,
@@ -3213,39 +2657,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3213
2657
  dispatch,
3214
2658
  references,
3215
2659
  selectedTargetLanguages,
3216
- shouldOverwriteFreshTranslations,
3217
- translationMode
3218
- } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), handleTranslate = useCallback(() => {
3219
- translateDocumentSet({
3220
- sourceDocument: document,
3221
- targetLanguages: selectedTargetLanguages,
2660
+ translationMode,
2661
+ releaseConfig
2662
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
2663
+ useEffect(() => {
2664
+ releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2665
+ }, [releaseConfig.mode, conflictContext]);
2666
+ const startTranslation = useCallback(
2667
+ (replaceExisting, overrideReleaseId) => {
2668
+ setConflictContext(null), translateDocumentSet({
2669
+ sourceDocument: document,
2670
+ targetLanguages: selectedTargetLanguages,
2671
+ client,
2672
+ dispatch,
2673
+ languageFieldName: config.languageFieldName,
2674
+ schemaId: config.schemaId,
2675
+ references,
2676
+ maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2677
+ maxReferenceDepth: config.maxReferenceDepth,
2678
+ translationMode,
2679
+ openaiConfig: config.openai,
2680
+ excludeFields: config.excludeFields,
2681
+ releaseConfig: {
2682
+ ...releaseConfig,
2683
+ replaceExisting,
2684
+ // Use the override release ID if provided (from conflict context),
2685
+ // otherwise use the current releaseConfig value
2686
+ existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
2687
+ }
2688
+ });
2689
+ },
2690
+ [
2691
+ document,
2692
+ selectedTargetLanguages,
3222
2693
  client,
3223
2694
  dispatch,
3224
- languageFieldName: config.languageFieldName,
3225
- schemaId: config.schemaId,
2695
+ config.languageFieldName,
2696
+ config.schemaId,
2697
+ config.maxConcurrentTranslationJobs,
2698
+ config.maxReferenceDepth,
2699
+ config.openai,
2700
+ config.excludeFields,
3226
2701
  references,
3227
- shouldOverwriteFreshTranslations,
3228
- maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
3229
- maxReferenceDepth: config.maxReferenceDepth,
3230
2702
  translationMode,
3231
- openaiConfig: config.openai,
3232
- excludeFields: config.excludeFields
3233
- });
3234
- }, [
3235
- document,
3236
- selectedTargetLanguages,
3237
- client,
3238
- dispatch,
3239
- config.languageFieldName,
3240
- config.schemaId,
3241
- config.maxConcurrentTranslationJobs,
3242
- config.maxReferenceDepth,
3243
- config.openai,
3244
- config.excludeFields,
3245
- references,
3246
- shouldOverwriteFreshTranslations,
3247
- translationMode
3248
- ]), handleSelectAll = useCallback(() => {
2703
+ releaseConfig
2704
+ ]
2705
+ ), handleTranslate = useCallback(async () => {
2706
+ if (releaseConfig.mode === "new") {
2707
+ startTranslation(!1);
2708
+ return;
2709
+ }
2710
+ if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
2711
+ const checkedReleaseId = releaseConfig.existingReleaseId;
2712
+ setIsCheckingConflicts(!0);
2713
+ const foundConflicts = await checkReleaseConflicts({
2714
+ client,
2715
+ releaseId: checkedReleaseId,
2716
+ sourceDocument: document,
2717
+ targetLanguages: selectedTargetLanguages,
2718
+ languageFieldName: config.languageFieldName,
2719
+ references
2720
+ });
2721
+ if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
2722
+ setConflictContext({
2723
+ releaseId: checkedReleaseId,
2724
+ conflicts: foundConflicts
2725
+ });
2726
+ return;
2727
+ }
2728
+ }
2729
+ startTranslation(!1);
2730
+ }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
2731
+ setConflictContext(null);
2732
+ }, []), handleReplaceConflict = useCallback(() => {
2733
+ conflictContext && startTranslation(!0, conflictContext.releaseId);
2734
+ }, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
3249
2735
  dispatch({
3250
2736
  type: "SET_SELECTED_TARGET_LANGUAGES",
3251
2737
  payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
@@ -3256,7 +2742,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3256
2742
  payload: []
3257
2743
  });
3258
2744
  }, [dispatch]);
3259
- return /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
2745
+ return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
2746
+ ReleaseConflictWarning,
2747
+ {
2748
+ conflicts: conflictContext.conflicts,
2749
+ onCancel: handleCancelConflict,
2750
+ onReplace: handleReplaceConflict
2751
+ }
2752
+ ) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
3260
2753
  /* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
3261
2754
  /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
3262
2755
  /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
@@ -3270,16 +2763,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3270
2763
  tone: "positive",
3271
2764
  size: 2,
3272
2765
  onClick: handleTranslate,
3273
- disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
3274
- children: "Translate"
2766
+ disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
2767
+ children: isCheckingConflicts ? "Checking..." : "Translate"
3275
2768
  }
3276
2769
  )
3277
2770
  ] })
3278
2771
  ] });
2772
+ }, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
2773
+ const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
2774
+ params: {
2775
+ documentId: undraftId(documentId),
2776
+ translationMetadataType: TRANSLATION_METADATA_TYPE
2777
+ },
2778
+ initialValue: null
2779
+ });
2780
+ return useEffect(() => {
2781
+ if (loading) {
2782
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
2783
+ return;
2784
+ }
2785
+ if (error) {
2786
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
2787
+ return;
2788
+ }
2789
+ if (data) {
2790
+ dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
2791
+ return;
2792
+ }
2793
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
2794
+ }, [data, loading, error, dispatch]), {
2795
+ data,
2796
+ loading,
2797
+ error
2798
+ };
3279
2799
  }, LanguageCheckbox = ({ lang, isLoading }) => {
3280
- const { document, dispatch, selectedTargetLanguages, translationMetadata } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), hasTranslation = translationMetadata?.translations?.some((translation) => translation._key === lang.id), isStale = hasTranslation && translationMetadata?.translationHistory?.some(
3281
- (history) => history.language === lang.id && history.sourceRev !== document._rev
3282
- ), handleChange = () => {
2800
+ const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
3283
2801
  if (isDisabled) return;
3284
2802
  const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
3285
2803
  dispatch({
@@ -3297,16 +2815,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3297
2815
  disabled: isDisabled,
3298
2816
  tone: isChecked ? "primary" : "default",
3299
2817
  muted: !0,
3300
- children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: [
3301
- /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
3302
- /* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
3303
- /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
3304
- /* @__PURE__ */ jsx(Text, { children: lang.title }),
3305
- isDisabled ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" }) : /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: hasTranslation && !isDisabled ? "Already translated" : "Not translated" })
3306
- ] })
3307
- ] }),
3308
- !isDisabled && hasTranslation && /* @__PURE__ */ jsx(Flex, { flex: 1, justify: "flex-end", children: /* @__PURE__ */ jsx(Badge, { tone: isStale ? "caution" : "positive", size: 3, muted: !0, children: isStale ? "Stale" : "Fresh" }) })
3309
- ] })
2818
+ children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
2819
+ /* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
2820
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
2821
+ /* @__PURE__ */ jsx(Text, { children: lang.title }),
2822
+ isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
2823
+ ] })
2824
+ ] }) })
3310
2825
  }
3311
2826
  );
3312
2827
  }, LanguageSelectionPanel = () => {
@@ -3314,6 +2829,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3314
2829
  documentId: document._id
3315
2830
  });
3316
2831
  return /* @__PURE__ */ jsx(Card, { padding: 4, muted: !0, border: !0, children: /* @__PURE__ */ jsx(Grid, { columns: [1, 2], gap: 3, children: config?.supportedLanguages.map((lang) => /* @__PURE__ */ jsx(LanguageCheckbox, { lang, isLoading: loading }, lang.id)) }) });
2832
+ }, ReleaseSelectionPanel = () => {
2833
+ const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
2834
+ useEffect(() => {
2835
+ (async () => {
2836
+ setIsLoading(!0), setError(null);
2837
+ try {
2838
+ const result = await client.fetch(
2839
+ 'releases::all()[state == "active"] | order(_createdAt desc)'
2840
+ );
2841
+ setReleases(result), devLog.debug("Fetched active releases", {
2842
+ count: result.length,
2843
+ component: "ReleaseSelectionPanel"
2844
+ });
2845
+ } catch (err) {
2846
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
2847
+ setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
2848
+ component: "ReleaseSelectionPanel"
2849
+ });
2850
+ } finally {
2851
+ setIsLoading(!1);
2852
+ }
2853
+ })();
2854
+ }, [client]);
2855
+ const handleModeChange = useCallback(
2856
+ (mode) => {
2857
+ dispatch({
2858
+ type: "SET_RELEASE_CONFIG",
2859
+ payload: { mode }
2860
+ });
2861
+ },
2862
+ [dispatch]
2863
+ ), handleTitleChange = useCallback(
2864
+ (event) => {
2865
+ dispatch({
2866
+ type: "SET_RELEASE_CONFIG",
2867
+ payload: { newReleaseTitle: event.target.value }
2868
+ });
2869
+ },
2870
+ [dispatch]
2871
+ ), handleSelectRelease = useCallback(
2872
+ (releaseId) => {
2873
+ dispatch({
2874
+ type: "SET_RELEASE_CONFIG",
2875
+ payload: { existingReleaseId: releaseId }
2876
+ });
2877
+ },
2878
+ [dispatch]
2879
+ ), handleSearchChange = useCallback((event) => {
2880
+ setSearchQuery(event.target.value);
2881
+ }, []), filteredReleases = useMemo(() => {
2882
+ if (!searchQuery.trim())
2883
+ return releases;
2884
+ const query = searchQuery.toLowerCase();
2885
+ return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
2886
+ }, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
2887
+ month: "short",
2888
+ day: "numeric",
2889
+ year: "numeric"
2890
+ });
2891
+ return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2892
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2893
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
2894
+ /* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
2895
+ /* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
2896
+ ] }),
2897
+ /* @__PURE__ */ jsxs(
2898
+ Flex,
2899
+ {
2900
+ align: "center",
2901
+ gap: 3,
2902
+ as: "label",
2903
+ style: { cursor: "pointer" },
2904
+ onClick: () => handleModeChange("existing"),
2905
+ children: [
2906
+ /* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
2907
+ /* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
2908
+ ]
2909
+ }
2910
+ )
2911
+ ] }),
2912
+ releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
2913
+ TextInput,
2914
+ {
2915
+ value: releaseConfig.newReleaseTitle,
2916
+ onChange: handleTitleChange,
2917
+ placeholder: "Release title..."
2918
+ }
2919
+ ),
2920
+ releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2921
+ isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2922
+ /* @__PURE__ */ jsx(Spinner, { muted: !0 }),
2923
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
2924
+ ] }),
2925
+ error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
2926
+ !isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
2927
+ !isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2928
+ /* @__PURE__ */ jsx(
2929
+ TextInput,
2930
+ {
2931
+ icon: SearchIcon,
2932
+ value: searchQuery,
2933
+ onChange: handleSearchChange,
2934
+ placeholder: "Search releases..."
2935
+ }
2936
+ ),
2937
+ /* @__PURE__ */ jsx(Box, { style: { maxHeight: "200px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 1, children: filteredReleases.length === 0 ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No releases match your search." }) : filteredReleases.map((release) => {
2938
+ const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
2939
+ return /* @__PURE__ */ jsx(
2940
+ Card,
2941
+ {
2942
+ padding: 3,
2943
+ radius: 2,
2944
+ tone: isSelected ? "primary" : "default",
2945
+ style: { cursor: "pointer" },
2946
+ onClick: () => handleSelectRelease(releaseName),
2947
+ border: !0,
2948
+ muted: isSelected,
2949
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
2950
+ /* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
2951
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
2952
+ ] })
2953
+ },
2954
+ release._id
2955
+ );
2956
+ }) }) })
2957
+ ] })
2958
+ ] })
2959
+ ] }) });
3317
2960
  }, TranslationModePanel = () => {
3318
2961
  const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
3319
2962
  if (!config?.openai?.enabled || !config?.openai?.apiKey)
@@ -3332,7 +2975,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3332
2975
  return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3333
2976
  /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3334
2977
  /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
3335
- /* @__PURE__ */ jsx(Badge, { tone: "positive", children: "GPT-5 Available" })
2978
+ /* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
3336
2979
  ] }),
3337
2980
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3338
2981
  /* @__PURE__ */ jsx(
@@ -3362,32 +3005,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3362
3005
  /* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
3363
3006
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3364
3007
  /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3365
- /* @__PURE__ */ jsx(Text, { children: "GPT-5 Only" }),
3008
+ /* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
3366
3009
  /* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
3367
3010
  ] }),
3368
3011
  /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
3369
3012
  ] })
3370
3013
  ] })
3371
3014
  }
3372
- ),
3373
- /* @__PURE__ */ jsx(
3374
- Button,
3375
- {
3376
- type: "button",
3377
- mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
3378
- tone: translationMode === "sanity-with-review" ? "primary" : "default",
3379
- onClick: () => handleModeChange("sanity-with-review"),
3380
- children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3381
- /* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
3382
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3383
- /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3384
- /* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
3385
- /* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
3386
- ] }),
3387
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
3388
- ] })
3389
- ] })
3390
- }
3391
3015
  )
3392
3016
  ] }),
3393
3017
  translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
@@ -3550,41 +3174,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3550
3174
  ] }),
3551
3175
  shouldTranslateReferences ? /* @__PURE__ */ jsx(Flex, { padding: 2, justify: "flex-end", children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : `${references.length} translatable references found.` }) }) : null
3552
3176
  ] });
3553
- }, WriteSettingsPanel = () => {
3554
- const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
3555
- dispatch({
3556
- type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
3557
- payload: !shouldOverwriteFreshTranslations
3558
- });
3559
- };
3560
- return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3561
- /* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
3562
- /* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
3563
- Button,
3564
- {
3565
- type: "button",
3566
- mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
3567
- size: 2,
3568
- onClick: handleShouldOverwriteFreshTranslationsChange,
3569
- tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
3570
- children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3571
- /* @__PURE__ */ jsx(
3572
- Checkbox,
3573
- {
3574
- checked: shouldOverwriteFreshTranslations,
3575
- onChange: handleShouldOverwriteFreshTranslationsChange,
3576
- label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
3577
- }
3578
- ),
3579
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3580
- /* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
3581
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
3582
- ] })
3583
- ] })
3584
- }
3585
- ) })
3586
- ] });
3587
- }, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3177
+ }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3588
3178
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3589
3179
  /* @__PURE__ */ jsx(Label, { children: "Languages" }),
3590
3180
  /* @__PURE__ */ jsx(LanguageSelectionPanel, {})
@@ -3593,16 +3183,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3593
3183
  /* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
3594
3184
  /* @__PURE__ */ jsx(TranslationModePanel, {})
3595
3185
  ] }),
3186
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3187
+ /* @__PURE__ */ jsx(Label, { children: "Content Release" }),
3188
+ /* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
3189
+ ] }),
3596
3190
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3597
3191
  /* @__PURE__ */ jsx(Label, { children: "Settings" }),
3598
3192
  /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
3599
3193
  ] }),
3600
3194
  /* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
3601
3195
  ] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
3602
- const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
3603
- () => overallStatus === "completed" ? "Translation completed" : "Translation in progress",
3604
- [overallStatus]
3605
- ), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
3196
+ const { overallStatus, translationJobs, translationMode, dispatch, document } = useTranslationContext(), headline = useMemo(() => overallStatus === "completed" ? "Translation completed" : overallStatus === "error" ? "Translation failed" : "Translation in progress", [overallStatus]), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), isTerminalState = overallStatus === "completed" || overallStatus === "error", handleReset = useCallback(() => {
3197
+ forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
3198
+ }, [dispatch, document._id]), openaiStats = useMemo(() => {
3606
3199
  if (translationMode === "sanity-only") return null;
3607
3200
  const completedJobs = jobs.filter((j) => j.status === "completed"), totalTokens = completedJobs.reduce((sum, job) => sum + (job.tokensUsed || 0), 0), totalCost = completedJobs.reduce((sum, job) => sum + (job.estimatedCost || 0), 0);
3608
3201
  return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
@@ -3636,17 +3229,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3636
3229
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3637
3230
  "Error: ",
3638
3231
  job.errorMessage
3639
- ] }) }) : null,
3640
- job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3641
- "Retrying after: ",
3642
- job.errorMessage
3643
3232
  ] }) }) : null
3644
3233
  ] })
3645
3234
  ] }),
3646
3235
  /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
3647
3236
  job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
3648
- job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
3649
- job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
3237
+ job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
3650
3238
  job.targetId ? /* @__PURE__ */ jsx(
3651
3239
  TranslationOpenerButton,
3652
3240
  {
@@ -3663,18 +3251,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3663
3251
  /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
3664
3252
  /* @__PURE__ */ jsxs(Inline, { space: 4, children: [
3665
3253
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3666
- "\u{1F4CA} Total Tokens: ",
3254
+ "Total Tokens: ",
3667
3255
  /* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
3668
3256
  ] }),
3669
3257
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3670
- "\u{1F4B0} Total Cost: ",
3258
+ "Total Cost: ",
3671
3259
  /* @__PURE__ */ jsxs("strong", { children: [
3672
3260
  "$",
3673
3261
  openaiStats.totalCost.toFixed(4)
3674
3262
  ] })
3675
3263
  ] })
3676
3264
  ] })
3677
- ] }) })
3265
+ ] }) }),
3266
+ isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
3678
3267
  ] });
3679
3268
  }, ProgressBar = ({ progress, max = 100 }) => {
3680
3269
  const percentage = Math.min(progress / max * 100, 100);
@@ -3726,35 +3315,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3726
3315
  content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
3727
3316
  children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
3728
3317
  }
3729
- ), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3730
- Tooltip,
3731
- {
3732
- portal: !0,
3733
- content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
3734
- children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
3735
- "Failed",
3736
- retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
3737
- ] })
3738
- }
3739
- ), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3318
+ ), ErrorBadge = () => /* @__PURE__ */ jsx(
3740
3319
  Tooltip,
3741
3320
  {
3742
3321
  portal: !0,
3743
- content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
3744
- children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
3745
- "Retry ",
3746
- retryCount || 1
3747
- ] })
3322
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
3323
+ children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
3748
3324
  }
3749
3325
  ), getStatusLabel = (status) => ({
3750
3326
  idle: "Idle",
3751
3327
  queued: "Queued...",
3752
3328
  preparing_translation: "Preparing translation...",
3753
3329
  translating: "Translating with Sanity Agent...",
3754
- translating_with_openai: "Translating with GPT-5...",
3755
- reviewing_with_openai: "Reviewing translation with GPT-5...",
3330
+ translating_with_openai: "Translating with OpenAI...",
3756
3331
  updating_metadata: "Saving metadata...",
3757
- retrying: "Retrying...",
3758
3332
  error: "Error",
3759
3333
  skipped: "Skipped",
3760
3334
  completed: "Completed"
@@ -3762,14 +3336,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3762
3336
  "translating",
3763
3337
  "linking_references",
3764
3338
  "completed"
3765
- ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3339
+ ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
3766
3340
  const { overallStatus, document } = useTranslationContext();
3767
- return STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen }) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationStatusPanel, {}) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? /* @__PURE__ */ jsx("div", { children: "Error occurred" }) : (devLog.warn("Unhandled translation dialog state", {
3341
+ return useEffect(() => {
3342
+ onStatusChange?.(overallStatus);
3343
+ }, [overallStatus, onStatusChange]), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen }) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationStatusPanel, {}) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? /* @__PURE__ */ jsx("div", { children: "Error occurred" }) : (devLog.warn("Unhandled translation dialog state", {
3768
3344
  documentId: document._id,
3769
3345
  overallStatus
3770
3346
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3771
- }, getTranslateAction = (config) => ({ draft, published }) => {
3772
- const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
3347
+ }, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
3348
+ const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
3349
+ setCurrentStatus(status);
3350
+ }, []), handleClose = useCallback(() => {
3351
+ if (isTranslating) {
3352
+ if (!window.confirm(
3353
+ "Translation is in progress. Closing this dialog will not stop the translation, but refreshing the page will cancel the translation. You can reopen the Translate dialog to see updated progress."
3354
+ ))
3355
+ return;
3356
+ devLog.warn("User closed dialog during active translation", {
3357
+ documentId: document?._id,
3358
+ component: "TranslateAction"
3359
+ });
3360
+ }
3361
+ setIsDialogOpen(!1);
3362
+ }, [isTranslating, document?._id]);
3773
3363
  return devLog.debug("TranslateAction initialized", {
3774
3364
  documentId: document?._id,
3775
3365
  documentType: document?._type
@@ -3777,7 +3367,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3777
3367
  documentType: document?._type,
3778
3368
  translatableTypes: config.translatableSchemaTypes
3779
3369
  }), null) : config ? {
3780
- label: "Translate",
3370
+ label: getButtonLabel(currentStatus),
3781
3371
  icon: TranslateIcon,
3782
3372
  onHandle: () => {
3783
3373
  devLog.info("Translation dialog opened", {
@@ -3788,9 +3378,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3788
3378
  },
3789
3379
  dialog: isDialogOpen && {
3790
3380
  type: "dialog",
3791
- onClose: () => setIsDialogOpen(!1),
3381
+ onClose: handleClose,
3792
3382
  header: "Translate Document",
3793
- content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3383
+ content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
3794
3384
  }
3795
3385
  } : (devLog.error("Missing config in TranslateAction"), null);
3796
3386
  }, translationGlossaryTerm = defineType({
@@ -3906,7 +3496,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3906
3496
  defineField({
3907
3497
  name: "changeHistory",
3908
3498
  title: "Change History",
3909
- description: "History of changes made via user feedback.",
3499
+ description: "History of changes made to translation settings.",
3910
3500
  type: "array",
3911
3501
  readOnly: !0,
3912
3502
  group: "history",
@@ -4211,12 +3801,283 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
4211
3801
  resultExample: result.length > 0 ? result[0] : null
4212
3802
  }), result;
4213
3803
  }
3804
+ const LOCALE_TO_ISO = {
3805
+ // English variants
3806
+ "en-us": "eng",
3807
+ "en-US": "eng",
3808
+ "en-gb": "eng",
3809
+ "en-GB": "eng",
3810
+ "en-ca": "eng",
3811
+ "en-CA": "eng",
3812
+ "en-au": "eng",
3813
+ "en-AU": "eng",
3814
+ // Spanish variants
3815
+ es: "spa",
3816
+ "es-es": "spa",
3817
+ "es-ES": "spa",
3818
+ "es-mx": "spa",
3819
+ "es-MX": "spa",
3820
+ // French variants
3821
+ fr: "fra",
3822
+ "fr-fr": "fra",
3823
+ "fr-FR": "fra",
3824
+ "fr-ca": "fra",
3825
+ "fr-CA": "fra",
3826
+ // German variants
3827
+ de: "ger",
3828
+ "de-de": "ger",
3829
+ "de-DE": "ger",
3830
+ "de-at": "ger",
3831
+ "de-AT": "ger",
3832
+ // Italian variants
3833
+ it: "ita",
3834
+ "it-it": "ita",
3835
+ "it-IT": "ita",
3836
+ // Portuguese variants
3837
+ pt: "por",
3838
+ "pt-br": "por",
3839
+ "pt-BR": "por",
3840
+ "pt-pt": "por",
3841
+ "pt-PT": "por",
3842
+ // Russian variants
3843
+ ru: "rus",
3844
+ "ru-ru": "rus",
3845
+ "ru-RU": "rus",
3846
+ // Japanese variants
3847
+ ja: "jpn",
3848
+ "ja-jp": "jpn",
3849
+ "ja-JP": "jpn",
3850
+ // Korean variants
3851
+ ko: "kor",
3852
+ "ko-kr": "kor",
3853
+ "ko-KR": "kor",
3854
+ // Chinese variants
3855
+ zh: "chi",
3856
+ "zh-cn": "chi",
3857
+ "zh-CN": "chi",
3858
+ "zh-tw": "chi",
3859
+ "zh-TW": "chi",
3860
+ "zh-hk": "chi",
3861
+ "zh-HK": "chi"
3862
+ }, normalizeToIsoLanguageCode = (code) => {
3863
+ const normalizedCode = code.toLowerCase();
3864
+ return LOCALE_TO_ISO[normalizedCode];
3865
+ }, wistiaCaptionTransform = {
3866
+ matcher: {
3867
+ fieldName: "wistiaCaptions"
3868
+ },
3869
+ toTranslatable: (wistiaCaptions, context) => {
3870
+ if (devLog.info("Wistia captions toTranslatable - input:", {
3871
+ fieldPath: context.fieldPath,
3872
+ hasCaptionContent: !!wistiaCaptions?.captionContent,
3873
+ captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
3874
+ isoLanguage: wistiaCaptions?.isoLanguage
3875
+ }), !wistiaCaptions || !wistiaCaptions.captionContent)
3876
+ return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
3877
+ fieldPath: context.fieldPath,
3878
+ value: wistiaCaptions
3879
+ }), {
3880
+ _type: "translatable.wistia.captions",
3881
+ translationText: "",
3882
+ _originalMetadata: {
3883
+ captionFileName: wistiaCaptions?.captionFileName,
3884
+ isoLanguage: wistiaCaptions?.isoLanguage,
3885
+ captionUploadStatus: wistiaCaptions?.captionUploadStatus,
3886
+ captionUploadMessage: wistiaCaptions?.captionUploadMessage,
3887
+ _originalIndex: 0
3888
+ },
3889
+ _srtCues: []
3890
+ };
3891
+ devLog.info("Applying Wistia caption transform", {
3892
+ fieldPath: context.fieldPath,
3893
+ captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
3894
+ });
3895
+ const srtCues = parseSRTContent(wistiaCaptions.captionContent);
3896
+ devLog.debug("Parsed SRT cues:", {
3897
+ fieldPath: context.fieldPath,
3898
+ cueCount: srtCues.length,
3899
+ firstCue: srtCues.length > 0 ? srtCues[0] : null
3900
+ });
3901
+ const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
3902
+ captionFileName: wistiaCaptions.captionFileName,
3903
+ isoLanguage: wistiaCaptions.isoLanguage,
3904
+ captionUploadStatus: wistiaCaptions.captionUploadStatus,
3905
+ captionUploadMessage: wistiaCaptions.captionUploadMessage,
3906
+ _originalIndex: 0
3907
+ }, srtCuesMetadata = srtCues.map((cue) => ({
3908
+ index: cue.index,
3909
+ startTime: cue.startTime,
3910
+ endTime: cue.endTime
3911
+ }));
3912
+ return devLog.info("Wistia captions serialized:", {
3913
+ fieldPath: context.fieldPath,
3914
+ translationTextLength: translationText.length,
3915
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
3916
+ cueCount: srtCues.length
3917
+ }), {
3918
+ _type: "translatable.wistia.captions",
3919
+ translationText,
3920
+ _originalMetadata: originalMetadata,
3921
+ _srtCues: srtCuesMetadata
3922
+ };
3923
+ },
3924
+ fromTranslatable: (translatedValue, originalValue, context) => {
3925
+ if (devLog.info("Wistia captions fromTranslatable - input:", {
3926
+ fieldPath: context.fieldPath,
3927
+ valueType: typeof translatedValue,
3928
+ isCorrectType: translatedValue._type === "translatable.wistia.captions",
3929
+ hasText: !!translatedValue.translationText,
3930
+ hasMetadata: !!translatedValue._originalMetadata,
3931
+ hasCues: Array.isArray(translatedValue._srtCues)
3932
+ }), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
3933
+ return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
3934
+ fieldPath: context.fieldPath,
3935
+ valueType: typeof translatedValue,
3936
+ translatedValue
3937
+ }), originalValue || {};
3938
+ devLog.info("Processing translated Wistia captions", {
3939
+ fieldPath: context.fieldPath,
3940
+ cueCount: translatedValue._srtCues.length
3941
+ });
3942
+ const { translationText, _originalMetadata, _srtCues } = translatedValue;
3943
+ devLog.info("Wistia captions translationText:", {
3944
+ fieldPath: context.fieldPath,
3945
+ translationTextLength: translationText.length,
3946
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
3947
+ });
3948
+ const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
3949
+ let updatedIsoLanguage = _originalMetadata.isoLanguage;
3950
+ if (documentLanguage) {
3951
+ const isoCode = normalizeToIsoLanguageCode(documentLanguage);
3952
+ isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
3953
+ fieldPath: context.fieldPath,
3954
+ documentLanguage,
3955
+ isoCode
3956
+ })) : devLog.debug("Could not convert document language to ISO code, preserving original", {
3957
+ fieldPath: context.fieldPath,
3958
+ documentLanguage,
3959
+ originalIsoLanguage: _originalMetadata.isoLanguage
3960
+ });
3961
+ } else
3962
+ devLog.debug("Document language field not found, preserving original isoLanguage", {
3963
+ fieldPath: context.fieldPath,
3964
+ originalIsoLanguage: _originalMetadata.isoLanguage
3965
+ });
3966
+ const result = {
3967
+ captionContent: reconstructedCaptionContent,
3968
+ captionFileName: _originalMetadata.captionFileName,
3969
+ isoLanguage: updatedIsoLanguage,
3970
+ captionUploadStatus: _originalMetadata.captionUploadStatus,
3971
+ captionUploadMessage: _originalMetadata.captionUploadMessage
3972
+ };
3973
+ return devLog.info("Wistia captions final result:", {
3974
+ fieldPath: context.fieldPath,
3975
+ hasCaptionContent: !!result.captionContent,
3976
+ captionContentLength: result.captionContent?.length ?? 0,
3977
+ isoLanguage: result.isoLanguage
3978
+ }), result;
3979
+ }
3980
+ };
3981
+ function parseSRTContent(content) {
3982
+ if (devLog.debug("parseSRTContent - input:", {
3983
+ contentLength: content.length,
3984
+ contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
3985
+ }), !content || content.trim() === "")
3986
+ return [];
3987
+ const cues = [], cueBlocks = content.split(/\n\s*\n/);
3988
+ for (const block of cueBlocks) {
3989
+ const lines = block.trim().split(`
3990
+ `);
3991
+ if (lines.length < 3)
3992
+ continue;
3993
+ const cueNumber = Number.parseInt(lines[0].trim(), 10);
3994
+ if (Number.isNaN(cueNumber)) {
3995
+ devLog.warn("Invalid SRT cue number", { line: lines[0] });
3996
+ continue;
3997
+ }
3998
+ const timestampLine = lines[1].trim(), timestampMatch = timestampLine.match(/^(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})$/);
3999
+ if (!timestampMatch) {
4000
+ devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
4001
+ continue;
4002
+ }
4003
+ const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
4004
+ `).trim();
4005
+ text && cues.push({
4006
+ index: cueNumber,
4007
+ startTime,
4008
+ endTime,
4009
+ text
4010
+ });
4011
+ }
4012
+ return devLog.debug("parseSRTContent - output:", {
4013
+ cueCount: cues.length,
4014
+ firstCue: cues.length > 0 ? cues[0] : null
4015
+ }), cues;
4016
+ }
4017
+ function serializeWistiaCaptionText(cues) {
4018
+ if (devLog.debug("serializeWistiaCaptionText - input:", {
4019
+ cuesCount: cues.length,
4020
+ firstCue: cues.length > 0 ? cues[0] : null
4021
+ }), !Array.isArray(cues) || cues.length === 0)
4022
+ return "";
4023
+ const result = cues.map((cue) => (devLog.debug(`Processing cue ${cue.index}:`, {
4024
+ text: cue.text,
4025
+ hasText: typeof cue.text == "string"
4026
+ }), `[${cue.index}] ${cue.text}`)).join(`
4027
+ `);
4028
+ return devLog.debug("serializeWistiaCaptionText - output:", {
4029
+ resultLength: result.length,
4030
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
4031
+ }), result;
4032
+ }
4033
+ function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
4034
+ if (devLog.debug("deserializeWistiaCaptionText - input:", {
4035
+ translationTextLength: translationText.length,
4036
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
4037
+ metadataCount: srtCuesMetadata.length,
4038
+ metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
4039
+ }), !translationText || translationText.trim() === "")
4040
+ return "";
4041
+ const result = [], lines = translationText.split(`
4042
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
4043
+ for (const metadata of srtCuesMetadata)
4044
+ metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
4045
+ for (const line of lines) {
4046
+ const match = line.match(lineRegex);
4047
+ if (!match) {
4048
+ devLog.warn("Failed to parse caption line", { line });
4049
+ continue;
4050
+ }
4051
+ const index = Number.parseInt(match[1], 10), text = match[2];
4052
+ devLog.debug("Found caption:", {
4053
+ index,
4054
+ text
4055
+ });
4056
+ const metadata = metadataByIndex.get(index);
4057
+ metadata ? (devLog.debug(`Found matching metadata for index ${index}`), result.push(`${index}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index });
4058
+ }
4059
+ result.length > 0 && result[result.length - 1] === "" && result.pop();
4060
+ const reconstructed = result.join(`
4061
+ `);
4062
+ return devLog.debug("deserializeWistiaCaptionText - output:", {
4063
+ resultLength: reconstructed.length,
4064
+ resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
4065
+ }), reconstructed;
4066
+ }
4067
+ function getDocumentLanguage(document) {
4068
+ if (!document || typeof document != "object")
4069
+ return;
4070
+ const languageFieldNames = ["language", "lang", "locale"];
4071
+ for (const fieldName of languageFieldNames)
4072
+ if (fieldName in document && typeof document[fieldName] == "string")
4073
+ return document[fieldName];
4074
+ }
4214
4075
  const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
4215
4076
  globalTransformRegistry.register(transform);
4216
4077
  })), {
4217
4078
  name: "sanity-plugin-ai-translation",
4218
4079
  document: {
4219
- actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
4080
+ actions: [getTranslateAction(config)]
4220
4081
  },
4221
4082
  schema: {
4222
4083
  types: [translationSettings, translationGlossaryTerm]
@@ -4230,6 +4091,7 @@ export {
4230
4091
  restoreExcludedFields,
4231
4092
  richTextTransform,
4232
4093
  translationHistorySchema,
4233
- vttCaptionTransform
4094
+ vttCaptionTransform,
4095
+ wistiaCaptionTransform
4234
4096
  };
4235
4097
  //# sourceMappingURL=index.js.map