@braze/sanity-plugin-ai-translation 0.2.3 → 0.2.5

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,63 @@ 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), mergedJob = existingJob ? { ...existingJob, ...job } : job;
84
+ entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
85
+ }
86
+ }, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
87
+ const entry = activeTranslations.get(documentId);
88
+ return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
89
+ entry.subscribers.delete(callback);
90
+ }) : () => {
91
+ };
92
+ }, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
93
+ setTimeout(() => {
94
+ const entry = activeTranslations.get(documentId);
95
+ entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
96
+ }, delayMs);
97
+ }, notifySubscribers = (entry) => {
98
+ for (const subscriber of entry.subscribers)
99
+ try {
100
+ subscriber(entry.state);
101
+ } catch (error) {
102
+ console.error("Error in active translation subscriber:", error);
103
+ }
104
+ }, forceCleanupActiveTranslation = (documentId) => {
105
+ activeTranslations.delete(documentId);
65
106
  }, TranslationContext = createContext({
66
107
  document: null,
67
108
  config: null,
68
109
  overallStatus: "idle",
69
110
  selectedTargetLanguages: [],
70
111
  translationJobs: /* @__PURE__ */ new Map(),
71
- shouldOverwriteFreshTranslations: !1,
72
112
  shouldTranslateReferences: !1,
73
113
  maxReferenceDepth: 1,
74
114
  references: [],
75
115
  translationMetadata: null,
76
116
  translationMode: "sanity-only",
77
117
  openaiValidated: !1,
118
+ releaseConfig: {
119
+ mode: "new",
120
+ newReleaseTitle: ""
121
+ },
78
122
  dispatch: () => {
79
123
  }
80
124
  }), translationReducer = (context, action) => {
@@ -85,7 +129,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
85
129
  };
86
130
  switch (devLog.debug("Translation action dispatched", {
87
131
  ...logContext,
88
- payload: action.payload
132
+ payload: "payload" in action ? action.payload : void 0
89
133
  }), action.type) {
90
134
  case "SET_OVERALL_STATUS":
91
135
  return { ...context, overallStatus: action.payload };
@@ -116,11 +160,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
116
160
  ...context,
117
161
  maxReferenceDepth: action.payload
118
162
  };
119
- case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
120
- return {
121
- ...context,
122
- shouldOverwriteFreshTranslations: action.payload
123
- };
124
163
  case "SET_REFERENCES":
125
164
  return {
126
165
  ...context,
@@ -133,31 +172,82 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
133
172
  translationJobs: newTranslationJobs
134
173
  };
135
174
  }
175
+ case "SET_RELEASE_CONFIG":
176
+ return {
177
+ ...context,
178
+ releaseConfig: {
179
+ ...context.releaseConfig,
180
+ ...action.payload
181
+ }
182
+ };
183
+ case "RESET_TRANSLATION_STATE":
184
+ return devLog.info("Resetting translation state for new translation", {
185
+ ...logContext,
186
+ previousStatus: context.overallStatus,
187
+ previousJobCount: context.translationJobs.size
188
+ }), {
189
+ ...context,
190
+ overallStatus: "idle",
191
+ translationJobs: /* @__PURE__ */ new Map(),
192
+ // Clear any release conflicts from previous run
193
+ releaseConfig: {
194
+ ...context.releaseConfig,
195
+ conflicts: void 0,
196
+ replaceExisting: void 0
197
+ }
198
+ };
136
199
  default:
137
200
  return context;
138
201
  }
139
- }, TranslationContextProvider = ({ document, config, children }) => {
202
+ }, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
140
203
  devLog.debug("TranslationContextProvider initialized", {
141
204
  documentId: document._id,
142
205
  documentType: document._type,
143
206
  supportedLanguages: config.supportedLanguages?.map((l) => l.id),
144
207
  component: "TranslationContextProvider"
145
208
  });
146
- const initialContext = {
209
+ const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
147
210
  document,
148
211
  config,
149
- overallStatus: "idle",
212
+ // If there's an active translation, restore its state
213
+ overallStatus: activeTranslation?.overallStatus ?? "idle",
150
214
  translationMetadata: null,
151
215
  selectedTargetLanguages: [],
152
- shouldOverwriteFreshTranslations: !1,
153
216
  shouldTranslateReferences: !1,
154
217
  maxReferenceDepth: 1,
155
218
  references: [],
156
- translationJobs: /* @__PURE__ */ new Map(),
219
+ // Restore jobs from active translation if available
220
+ translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
157
221
  translationMode: config?.openai?.defaultMode || "sanity-only",
158
- openaiValidated: !1
159
- }, [context, dispatch] = useReducer(translationReducer, initialContext);
222
+ openaiValidated: !1,
223
+ releaseConfig: {
224
+ mode: "new",
225
+ newReleaseTitle: defaultReleaseTitle
226
+ }
227
+ };
228
+ activeTranslation && devLog.info("Reconnecting to active translation", {
229
+ documentId: document._id,
230
+ status: activeTranslation.overallStatus,
231
+ jobCount: activeTranslation.translationJobs.size,
232
+ component: "TranslationContextProvider"
233
+ });
234
+ const [context, dispatch] = useReducer(translationReducer, initialContext);
160
235
  return useEffect(() => {
236
+ const handleActiveTranslationUpdate = (state) => {
237
+ dispatch({
238
+ type: "SET_OVERALL_STATUS",
239
+ payload: state.overallStatus
240
+ });
241
+ for (const job of state.translationJobs.values())
242
+ dispatch({
243
+ type: "SET_TRANSLATION_JOB_STATUS",
244
+ payload: job
245
+ });
246
+ }, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
247
+ return () => {
248
+ unsubscribe();
249
+ };
250
+ }, [document._id]), useEffect(() => {
161
251
  (async () => {
162
252
  if (config?.openai?.enabled && config?.openai?.apiKey)
163
253
  try {
@@ -196,1043 +286,128 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
196
286
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
197
287
  );
198
288
  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({
289
+ }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
290
+ (translation) => translation._key === targetLanguage
291
+ )?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
367
292
  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) {
293
+ sourceDocument,
294
+ sourceLanguage
295
+ }) => {
562
296
  try {
563
- const response = await client.agent.action.prompt({
564
- instruction: prompt,
565
- format: "json"
297
+ return await client.fetch(
298
+ "*[_type == $tmType && references($sourceDocumentId)][0]",
299
+ {
300
+ tmType: TRANSLATION_METADATA_TYPE,
301
+ sourceDocumentId: undraftId(sourceDocument._id)
302
+ }
303
+ ) || await client.create({
304
+ _type: TRANSLATION_METADATA_TYPE,
305
+ translations: [
306
+ {
307
+ _key: sourceLanguage,
308
+ _type: "internationalizedArrayReferenceValue",
309
+ value: {
310
+ _type: "reference",
311
+ _ref: undraftId(sourceDocument._id),
312
+ _weak: !0,
313
+ _strengthenOnPublish: {
314
+ _type: sourceDocument._type,
315
+ _weak: !1
316
+ }
317
+ }
318
+ }
319
+ ],
320
+ translationHistory: []
566
321
  });
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
322
  } catch (error) {
573
- throw devLog.error("Error during AI analysis", { error }), new Error(`Failed to analyze feedback: ${error instanceof Error ? error.message : "Unknown error"}`);
574
- }
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]);
585
- }
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
- };
598
- } catch (error) {
599
- throw devLog.error("Error parsing AI response", { error, responseText }), new Error("Failed to parse AI response as JSON");
600
- }
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));
618
- }
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));
323
+ throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
630
324
  }
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(
325
+ }, checkDocumentConflicts = async ({
326
+ client,
327
+ releaseId,
328
+ documentId,
329
+ documentTitle,
330
+ documentType,
331
+ targetLanguages,
332
+ languageFieldName
333
+ }) => {
334
+ const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
335
+ if (!document)
336
+ return conflicts;
337
+ const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
632
338
  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();
656
- }
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
- }
680
- }
681
- devLog.info("Prompts updated successfully", {
682
- targetLanguage,
683
- globalUpdated: globalPrompts.length > 0,
684
- localeUpdated: localePrompts.length > 0
339
+ sourceDocument: document,
340
+ sourceLanguage
685
341
  });
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 }));
697
- }
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
- });
342
+ for (const targetLanguage of targetLanguages) {
343
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
344
+ if (existingTranslationId) {
345
+ const versionedId = `versions.${releaseId}.${existingTranslationId}`;
346
+ await client.getDocument(versionedId) && (conflicts.push({
347
+ sourceDocumentId: documentId,
348
+ sourceDocumentTitle: documentTitle || document.title || document.name,
349
+ sourceDocumentType: documentType || document._type,
350
+ targetLanguage,
351
+ existingVersionId: versionedId
352
+ }), devLog.debug("Found existing version in release", {
353
+ releaseId,
354
+ sourceDocumentId: documentId,
355
+ targetLanguage,
356
+ versionedId
357
+ }));
358
+ }
747
359
  }
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
360
+ return conflicts;
361
+ }, checkReleaseConflicts = async ({
362
+ client,
363
+ releaseId,
364
+ sourceDocument,
365
+ targetLanguages,
366
+ languageFieldName,
367
+ references = []
805
368
  }) => {
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,
369
+ const allConflicts = [];
370
+ try {
371
+ const documentsToCheck = [
997
372
  {
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
- ] });
1144
- }
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,
1167
- {
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
1174
- }
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
- }
373
+ _id: sourceDocument._id,
374
+ _type: sourceDocument._type,
375
+ title: sourceDocument.title || sourceDocument.name
376
+ },
377
+ ...references.map((ref) => ({
378
+ _id: ref._id,
379
+ _type: ref._type,
380
+ title: ref.title
381
+ }))
382
+ ], conflictChecks = await Promise.all(
383
+ documentsToCheck.map(
384
+ (doc) => checkDocumentConflicts({
385
+ client,
386
+ releaseId,
387
+ documentId: doc._id,
388
+ documentTitle: doc.title,
389
+ documentType: doc._type,
390
+ targetLanguages,
391
+ languageFieldName
392
+ })
1208
393
  )
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 }) })
1234
- }
1235
- } : (devLog.error("Missing config in TranslateAction"), null);
394
+ );
395
+ for (const conflicts of conflictChecks)
396
+ allConflicts.push(...conflicts);
397
+ devLog.debug("Release conflict check complete", {
398
+ releaseId,
399
+ documentsChecked: documentsToCheck.length,
400
+ conflictsFound: allConflicts.length
401
+ });
402
+ } catch (error) {
403
+ devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
404
+ releaseId,
405
+ sourceDocumentId: sourceDocument._id,
406
+ targetLanguages,
407
+ referencesCount: references.length
408
+ });
409
+ }
410
+ return allConflicts;
1236
411
  };
1237
412
  class Semaphore {
1238
413
  permits;
@@ -1409,11 +584,21 @@ const fetchTranslationPrompts = async ({
1409
584
  const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
1410
585
  `*[_id == "translation-settings"][0] {
1411
586
  globalPrompt,
1412
- localePrompts[locale in $targetLanguages],
587
+ localePrompts[_key in $targetLanguages],
1413
588
  protectedPhrases
1414
589
  }`,
1415
590
  { targetLanguages }
1416
- ), translationGlossaryTerms = await client.fetch(
591
+ );
592
+ devLog.debug("Fetched translation settings", {
593
+ hasSettings: !!translationSettings2,
594
+ hasGlobalPrompt: !!translationSettings2?.globalPrompt,
595
+ globalPromptLength: translationSettings2?.globalPrompt?.length,
596
+ localePromptsCount: translationSettings2?.localePrompts?.length,
597
+ localePrompts: translationSettings2?.localePrompts,
598
+ protectedPhrasesCount: translationSettings2?.protectedPhrases?.length,
599
+ targetLanguages
600
+ });
601
+ const translationGlossaryTerms = await client.fetch(
1417
602
  `*[
1418
603
  _type == "translationGlossaryTerm"
1419
604
  && count(translations[_key in $targetLanguages]) > 0
@@ -1433,9 +618,21 @@ const fetchTranslationPrompts = async ({
1433
618
  )), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
1434
619
  }
1435
620
  let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
1436
- return sourceDocument && filteredProtectedPhrases && (filteredProtectedPhrases = filteredProtectedPhrases.filter(
1437
- (phrase) => protectedPhraseAppearsInDocument(phrase, sourceDocumentString)
1438
- )), {
621
+ return sourceDocument && filteredProtectedPhrases && (devLog.debug("Filtering protected phrases", {
622
+ totalPhrases: filteredProtectedPhrases.length,
623
+ phrases: filteredProtectedPhrases,
624
+ sourceDocumentStringLength: sourceDocumentString.length,
625
+ sourceDocumentPreview: sourceDocumentString.substring(0, 500)
626
+ }), filteredProtectedPhrases = filteredProtectedPhrases.filter((phrase) => {
627
+ const appears = protectedPhraseAppearsInDocument(phrase, sourceDocumentString);
628
+ return devLog.debug("Protected phrase match result", {
629
+ phrase,
630
+ appears
631
+ }), appears;
632
+ }), devLog.debug("Filtered protected phrases result", {
633
+ remainingCount: filteredProtectedPhrases.length,
634
+ remainingPhrases: filteredProtectedPhrases
635
+ })), {
1439
636
  globalPrompt: translationSettings2?.globalPrompt,
1440
637
  protectedPhrases: filteredProtectedPhrases,
1441
638
  localeInstructions: Object.fromEntries(
@@ -1568,7 +765,7 @@ const fetchTranslationPrompts = async ({
1568
765
  translatedCount: translationMap.size,
1569
766
  component: "updateReferences"
1570
767
  }));
1571
- }, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
768
+ }, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
1572
769
  for (const sourceDocument of sourceDocuments)
1573
770
  for (const lang of targetLanguages) {
1574
771
  const payload = {
@@ -1581,44 +778,8 @@ const fetchTranslationPrompts = async ({
1581
778
  dispatch({
1582
779
  type: "SET_TRANSLATION_JOB_STATUS",
1583
780
  payload
1584
- });
781
+ }), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
1585
782
  }
1586
- }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1587
- (translation) => translation._key === targetLanguage
1588
- )?.value._ref || null, getTranslationMetadata = async ({
1589
- client,
1590
- sourceDocument,
1591
- sourceLanguage
1592
- }) => {
1593
- try {
1594
- return await client.fetch(
1595
- "*[_type == $tmType && references($sourceDocumentId)][0]",
1596
- {
1597
- tmType: TRANSLATION_METADATA_TYPE,
1598
- sourceDocumentId: undraftId(sourceDocument._id)
1599
- }
1600
- ) || await client.create({
1601
- _type: TRANSLATION_METADATA_TYPE,
1602
- translations: [
1603
- {
1604
- _key: sourceLanguage,
1605
- _type: "internationalizedArrayReferenceValue",
1606
- value: {
1607
- _type: "reference",
1608
- _ref: undraftId(sourceDocument._id),
1609
- _weak: !0,
1610
- _strengthenOnPublish: {
1611
- _type: sourceDocument._type,
1612
- _weak: !1
1613
- }
1614
- }
1615
- }
1616
- ],
1617
- translationHistory: []
1618
- });
1619
- } catch (error) {
1620
- throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
1621
- }
1622
783
  }, updateTranslationMetadata = async ({
1623
784
  client,
1624
785
  targetLanguage,
@@ -1665,15 +826,156 @@ const fetchTranslationPrompts = async ({
1665
826
  component: "updateTranslationMetadata"
1666
827
  }), error;
1667
828
  }
1668
- }, setJobStatus = (dispatch, _id, language, status, payload) => {
829
+ }, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
830
+ const job = defaults(payload, {
831
+ _id,
832
+ language,
833
+ status
834
+ });
1669
835
  dispatch({
1670
836
  type: "SET_TRANSLATION_JOB_STATUS",
1671
- payload: defaults(payload, {
1672
- _id,
1673
- language,
1674
- status
1675
- })
1676
- });
837
+ payload: job
838
+ }), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
839
+ };
840
+ function shouldExcludeField(fieldPath, excludeFields) {
841
+ for (const pattern of excludeFields) {
842
+ if (fieldPath === pattern)
843
+ return !0;
844
+ if (pattern.startsWith("**.")) {
845
+ const fieldName = pattern.substring(3), segments = fieldPath.split(".");
846
+ if (segments[segments.length - 1]?.split("[")[0] === fieldName)
847
+ return !0;
848
+ } else if (!pattern.includes(".") && !pattern.includes("[")) {
849
+ const segments = fieldPath.split(".");
850
+ if (segments[segments.length - 1]?.split("[")[0] === pattern)
851
+ return !0;
852
+ } else if (pattern.includes("[*]")) {
853
+ const regexPattern = pattern.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\\\[\*\\\]/g, "\\[\\d+\\]");
854
+ if (new RegExp(`^${regexPattern}$`).test(fieldPath))
855
+ return !0;
856
+ }
857
+ }
858
+ return !1;
859
+ }
860
+ function filterExcludedFields(document, excludeFields = [], currentPath = "") {
861
+ if (!excludeFields.length)
862
+ return {
863
+ filteredDocument: document,
864
+ excludedValues: {}
865
+ };
866
+ const filteredDocument = {}, excludedValues = {};
867
+ let hasExclusions = !1;
868
+ for (const [key, value] of Object.entries(document)) {
869
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
870
+ if (shouldExcludeField(fieldPath, excludeFields)) {
871
+ excludedValues[fieldPath] = value, hasExclusions = !0, devLog.debug("Excluding field from translation", {
872
+ fieldPath,
873
+ fieldType: typeof value,
874
+ value: typeof value == "string" ? value : "[object]"
875
+ });
876
+ continue;
877
+ }
878
+ if (value && typeof value == "object" && !Array.isArray(value) && value._type !== "reference") {
879
+ const nested = filterExcludedFields(value, excludeFields, fieldPath);
880
+ filteredDocument[key] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (hasExclusions = !0);
881
+ } else if (Array.isArray(value)) {
882
+ const filteredArray = [];
883
+ let arrayHasExclusions = !1;
884
+ for (let i = 0; i < value.length; i++) {
885
+ const arrayItem = value[i], itemPath = `${fieldPath}[${i}]`;
886
+ if (arrayItem && typeof arrayItem == "object" && !Array.isArray(arrayItem) && arrayItem._type !== "reference") {
887
+ const nested = filterExcludedFields(arrayItem, excludeFields, itemPath);
888
+ filteredArray[i] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (arrayHasExclusions = !0);
889
+ } else
890
+ filteredArray[i] = arrayItem;
891
+ }
892
+ filteredDocument[key] = filteredArray, arrayHasExclusions && (hasExclusions = !0);
893
+ } else
894
+ filteredDocument[key] = value;
895
+ }
896
+ return hasExclusions && devLog.info("Filtered excluded fields from document", {
897
+ documentId: document._id || "unknown",
898
+ excludedFieldCount: Object.keys(excludedValues).length,
899
+ excludedFields: Object.keys(excludedValues)
900
+ }), {
901
+ filteredDocument,
902
+ excludedValues
903
+ };
904
+ }
905
+ function restoreExcludedFields(translatedDocument, excludedValues) {
906
+ if (!Object.keys(excludedValues).length)
907
+ return translatedDocument;
908
+ const result = { ...translatedDocument };
909
+ for (const [fieldPath, value] of Object.entries(excludedValues))
910
+ setNestedValue(result, fieldPath, value);
911
+ return devLog.debug("Restored excluded fields to translated document", {
912
+ documentId: result._id || "unknown",
913
+ restoredFieldCount: Object.keys(excludedValues).length,
914
+ restoredFields: Object.keys(excludedValues)
915
+ }), result;
916
+ }
917
+ function setNestedValue(obj, path, value) {
918
+ const segments = path.split(".");
919
+ let current = obj;
920
+ for (let i = 0; i < segments.length - 1; i++) {
921
+ const segment = segments[i], arrayMatch2 = segment.match(/^(.+)\[(\d+)\]$/);
922
+ if (arrayMatch2) {
923
+ const [, fieldName, indexStr] = arrayMatch2, index = Number.parseInt(indexStr, 10);
924
+ for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
925
+ current[fieldName].push({});
926
+ current = current[fieldName][index];
927
+ } else
928
+ (!(segment in current) || typeof current[segment] != "object") && (current[segment] = {}), current = current[segment];
929
+ }
930
+ const finalSegment = segments[segments.length - 1], arrayMatch = finalSegment.match(/^(.+)\[(\d+)\]$/);
931
+ if (arrayMatch) {
932
+ const [, fieldName, indexStr] = arrayMatch, index = Number.parseInt(indexStr, 10);
933
+ for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
934
+ current[fieldName].push(null);
935
+ current[fieldName][index] = value;
936
+ } else
937
+ current[finalSegment] = value;
938
+ }
939
+ const COMMON_ENUM_FIELDS = [
940
+ "status",
941
+ "state",
942
+ "type",
943
+ "category",
944
+ "priority",
945
+ "visibility",
946
+ "role",
947
+ "permission",
948
+ "level",
949
+ "mode",
950
+ "format",
951
+ "variant",
952
+ "size",
953
+ "theme",
954
+ "kind",
955
+ "phase",
956
+ "stage"
957
+ ], PREDEFINED_EXCLUSIONS = {
958
+ /** Common enum and status fields */
959
+ enums: [...COMMON_ENUM_FIELDS],
960
+ /** All metadata and system fields (starting with _) */
961
+ metadata: ["_id", "_type", "_rev", "_createdAt", "_updatedAt"],
962
+ /** Common technical and configuration fields */
963
+ technical: [
964
+ "slug",
965
+ "handle",
966
+ "key",
967
+ "code",
968
+ "id",
969
+ "uuid",
970
+ "token",
971
+ "hash",
972
+ "checksum",
973
+ "version",
974
+ "config",
975
+ "settings"
976
+ ],
977
+ /** SEO and URL-related fields that often shouldn't be translated */
978
+ seo: ["slug", "handle", "permalink", "canonical", "redirect", "robots", "sitemap"]
1677
979
  };
1678
980
  function applyTransforms({
1679
981
  document,
@@ -1837,9 +1139,10 @@ class TransformRegistry {
1837
1139
  }), matches[0];
1838
1140
  }
1839
1141
  }
1840
- const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1142
+ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5").startsWith("gpt-5") ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1841
1143
  const model = request.model || "gpt-5", requestBody = {
1842
- model: "gpt-5.1",
1144
+ model: model === "gpt-5" ? "gpt-5.1" : model,
1145
+ // Map gpt-5 to gpt-5.1, use others as-is
1843
1146
  input: request.messages
1844
1147
  // Uses 'input' instead of 'messages'
1845
1148
  };
@@ -1847,6 +1150,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1847
1150
  effort: request.reasoningEffort,
1848
1151
  summary: "auto"
1849
1152
  // Enable automatic reasoning summaries
1153
+ }), devLog.debug("OpenAI GPT-5 Request", {
1154
+ model: requestBody.model,
1155
+ reasoningEffort: request.reasoningEffort,
1156
+ messages: request.messages.map((m) => ({
1157
+ role: m.role,
1158
+ contentLength: m.content.length,
1159
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1160
+ }))
1161
+ }), devLog.debug("OpenAI GPT-5 Full Prompt", {
1162
+ messages: request.messages
1850
1163
  });
1851
1164
  const response = await fetch("https://api.openai.com/v1/responses", {
1852
1165
  method: "POST",
@@ -1864,7 +1177,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1864
1177
  prompt: result.usage?.input_tokens || 0,
1865
1178
  completion: result.usage?.output_tokens || 0,
1866
1179
  reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
1867
- total: result.usage?.total_tokens || 0
1180
+ total: result.usage?.total_tokens || 0,
1181
+ cached: result.usage?.input_tokens_details?.cached_tokens || 0
1868
1182
  }, cost = calculateCost(tokensUsed, model);
1869
1183
  let content = "";
1870
1184
  if (result.output && Array.isArray(result.output)) {
@@ -1881,7 +1195,19 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1881
1195
  model,
1882
1196
  messages: request.messages,
1883
1197
  temperature: request.temperature ?? 0.3
1884
- }, response = await fetch("https://api.openai.com/v1/chat/completions", {
1198
+ };
1199
+ devLog.debug("OpenAI Chat Completions Request", {
1200
+ model,
1201
+ temperature: request.temperature ?? 0.3,
1202
+ messages: request.messages.map((m) => ({
1203
+ role: m.role,
1204
+ contentLength: m.content.length,
1205
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1206
+ }))
1207
+ }), devLog.debug("OpenAI Chat Completions Full Prompt", {
1208
+ messages: request.messages
1209
+ });
1210
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
1885
1211
  method: "POST",
1886
1212
  headers: {
1887
1213
  "Content-Type": "application/json",
@@ -1897,7 +1223,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1897
1223
  prompt: usage.prompt_tokens || 0,
1898
1224
  completion: usage.completion_tokens || 0,
1899
1225
  reasoning: usage.reasoning_tokens || 0,
1900
- total: usage.total_tokens || 0
1226
+ total: usage.total_tokens || 0,
1227
+ cached: usage.prompt_tokens_details?.cached_tokens || 0
1901
1228
  }, cost = calculateCost(tokensUsed, model);
1902
1229
  return {
1903
1230
  content: result.choices[0].message.content || "",
@@ -1907,27 +1234,47 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1907
1234
  }, calculateCost = (tokens, model) => {
1908
1235
  const pricing = {
1909
1236
  "gpt-5": {
1910
- input: 2e-6,
1237
+ input: 125e-8,
1238
+ output: 1e-5,
1239
+ reasoning: 0
1240
+ },
1241
+ "gpt-5.1": {
1242
+ input: 125e-8,
1911
1243
  output: 1e-5,
1912
- reasoning: 5e-6
1244
+ reasoning: 0
1245
+ },
1246
+ "gpt-5-mini": {
1247
+ input: 25e-8,
1248
+ output: 2e-6,
1249
+ reasoning: 0
1250
+ },
1251
+ "gpt-5-nano": {
1252
+ input: 50000000000000004e-24,
1253
+ output: 40000000000000003e-23,
1254
+ reasoning: 0
1255
+ },
1256
+ "gpt-5-pro": {
1257
+ input: 15e-6,
1258
+ output: 12e-5,
1259
+ reasoning: 0
1913
1260
  },
1914
1261
  "gpt-4o": {
1915
- input: 5e-6,
1916
- output: 14999999999999999e-21,
1262
+ input: 25e-7,
1263
+ output: 1e-5,
1917
1264
  reasoning: 0
1918
1265
  },
1919
1266
  "gpt-4-turbo": {
1920
1267
  input: 1e-5,
1921
- output: 29999999999999997e-21,
1268
+ output: 3e-5,
1922
1269
  reasoning: 0
1923
1270
  },
1924
1271
  "gpt-4": {
1925
- input: 29999999999999997e-21,
1926
- output: 59999999999999995e-21,
1272
+ input: 3e-5,
1273
+ output: 6e-5,
1927
1274
  reasoning: 0
1928
1275
  }
1929
- }, modelPricing = pricing[model] || pricing["gpt-5"];
1930
- return tokens.prompt * modelPricing.input + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
1276
+ }, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
1277
+ return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
1931
1278
  }, deserializeDocument = (originalDoc, reviewedText) => {
1932
1279
  const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
1933
1280
  return applyTextChanges(result, parsed), result;
@@ -1943,7 +1290,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1943
1290
  type: "field",
1944
1291
  name: fieldName,
1945
1292
  text: unescapeXml(text2),
1946
- attributes: {},
1293
+ attributes: { type: "string" },
1947
1294
  children: []
1948
1295
  };
1949
1296
  if (stack.length > 0) {
@@ -2016,6 +1363,26 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2016
1363
  parent && parent.children.push(node);
2017
1364
  } else
2018
1365
  root.push(node);
1366
+ } else if (attributes.type === "string" && content && content.includes(closingTag))
1367
+ if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
1368
+ const parent = stack[stack.length - 1];
1369
+ parent && parent.children.push(node);
1370
+ } else
1371
+ root.push(node);
1372
+ else if (attributes.type === "string" && !content) {
1373
+ const contentLines = [];
1374
+ for (i++; i < lines.length; ) {
1375
+ const currentLine = lines[i];
1376
+ if (currentLine.trim() === "</field>")
1377
+ break;
1378
+ contentLines.push(currentLine), i++;
1379
+ }
1380
+ if (node.text = contentLines.join(`
1381
+ `).trim(), stack.length > 0) {
1382
+ const parent = stack[stack.length - 1];
1383
+ parent && parent.children.push(node);
1384
+ } else
1385
+ root.push(node);
2019
1386
  } else
2020
1387
  stack.push(node);
2021
1388
  continue;
@@ -2072,7 +1439,10 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2072
1439
  });
2073
1440
  return;
2074
1441
  }
2075
- node.text !== void 0 && !node.attributes.readonly && set(doc, path, node.text);
1442
+ node.text !== void 0 && !node.attributes.readonly && (get(doc, path) !== void 0 ? set(doc, path, node.text) : devLog.debug("Skipping field not present in original document", {
1443
+ path: path.join("."),
1444
+ attemptedValue: node.text
1445
+ }));
2076
1446
  }, applyBlockChanges = (block, blockNode) => {
2077
1447
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
2078
1448
  devLog.warn("Block key mismatch, skipping", {
@@ -2103,7 +1473,10 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2103
1473
  });
2104
1474
  return;
2105
1475
  }
2106
- fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text);
1476
+ fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not present in original", {
1477
+ fieldName,
1478
+ attemptedValue: fieldNode.text
1479
+ }));
2107
1480
  }, applySpanChanges = (span, spanNode) => {
2108
1481
  if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
2109
1482
  devLog.warn("Span key mismatch, skipping", {
@@ -2148,7 +1521,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2148
1521
  return;
2149
1522
  }
2150
1523
  if (typeof value == "string") {
2151
- value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
1524
+ value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
2152
1525
  return;
2153
1526
  }
2154
1527
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2185,7 +1558,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2185
1558
  return;
2186
1559
  }
2187
1560
  if (typeof value == "string" && value.length > 0) {
2188
- lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
1561
+ lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
2189
1562
  return;
2190
1563
  }
2191
1564
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2327,7 +1700,19 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2327
1700
  `Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
2328
1701
  );
2329
1702
  }
2330
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
1703
+ const translationValidation = validateTranslationCompleteness(sourceDocument, result, languageFieldName);
1704
+ return translationValidation.valid || (devLog.warn("Translation completeness warning", {
1705
+ ...logContext,
1706
+ skippedFields: translationValidation.skippedFields,
1707
+ warning: "GPT-5 may have skipped some important fields"
1708
+ }), devLog.info("Skipped field details", {
1709
+ ...logContext,
1710
+ skippedFields: translationValidation.skippedFields.map((field) => ({
1711
+ path: field.path,
1712
+ originalValue: field.originalValue,
1713
+ translatedValue: field.translatedValue
1714
+ }))
1715
+ })), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2331
1716
  tokensUsed: response.tokensUsed.total,
2332
1717
  estimatedCost: response.cost
2333
1718
  }), result;
@@ -2368,12 +1753,19 @@ ${sourceText}
2368
1753
 
2369
1754
  ## Instructions
2370
1755
 
2371
- 1. Translate all text content into ${targetLanguage}
2372
- 2. Follow the guidelines and terminology requirements above
2373
- 3. Preserve protected phrases exactly as they appear
2374
- 4. Return the document in THE EXACT SAME FORMAT
2375
- 5. **CRITICAL**: Only modify text content between tags
2376
- 6. Never add, remove, or change any tags, attributes, or structure
1756
+ 1. **TRANSLATE ALL TEXT FIELDS**: Every field marked with type="string" or containing text must be translated
1757
+ 2. **DO NOT SKIP SIMPLE FIELDS**: Pay special attention to simple fields like "title", "name", "description" - these are often the most important
1758
+ 3. Follow the guidelines and terminology requirements above
1759
+ 4. Preserve protected phrases exactly as they appear
1760
+ 5. Return the document in THE EXACT SAME FORMAT
1761
+ 6. **CRITICAL**: Only modify text content between tags
1762
+ 7. Never add, remove, or change any tags, attributes, or structure
1763
+
1764
+ **Common fields that must always be translated:**
1765
+ - title, name, heading, label
1766
+ - description, caption, alt text
1767
+ - All text within <field> tags
1768
+ - All text within <span> tags
2377
1769
 
2378
1770
  If you used significant reasoning to make translation decisions, you may include a brief summary:
2379
1771
  <reasoning>
@@ -2381,6 +1773,43 @@ Brief summary of key translation decisions and reasoning
2381
1773
  </reasoning>
2382
1774
 
2383
1775
  Then return the translated document:`;
1776
+ }, validateTranslationCompleteness = (sourceDoc, translatedDoc, languageFieldName) => {
1777
+ const skippedFields = [], checkField = (source, translated, path = []) => {
1778
+ if (path.length > 0) {
1779
+ const fieldName = path[path.length - 1];
1780
+ if (fieldName.startsWith("_") || fieldName === languageFieldName)
1781
+ return;
1782
+ }
1783
+ if (typeof source == "string" && typeof translated == "string") {
1784
+ if (source.length > 0 && source === translated) {
1785
+ const fieldName = path[path.length - 1] || "unknown";
1786
+ (["title", "name", "heading", "label", "description", "caption", "alt"].includes(
1787
+ fieldName
1788
+ ) || source.length > 10) && skippedFields.push({
1789
+ path: path.join("."),
1790
+ originalValue: source,
1791
+ translatedValue: translated
1792
+ });
1793
+ }
1794
+ return;
1795
+ }
1796
+ if (source && translated && typeof source == "object" && typeof translated == "object") {
1797
+ if (Array.isArray(source) && Array.isArray(translated)) {
1798
+ const maxLength = Math.min(source.length, translated.length);
1799
+ for (let i = 0; i < maxLength; i++)
1800
+ checkField(source[i], translated[i], [...path, String(i)]);
1801
+ } else if (!Array.isArray(source) && !Array.isArray(translated))
1802
+ for (const key of Object.keys(source))
1803
+ !key.startsWith("_") && key !== languageFieldName && checkField(source[key], translated[key], [
1804
+ ...path,
1805
+ key
1806
+ ]);
1807
+ }
1808
+ };
1809
+ return checkField(sourceDoc, translatedDoc), {
1810
+ valid: skippedFields.length === 0,
1811
+ skippedFields
1812
+ };
2384
1813
  }, assembleLanguageSpecs = ({
2385
1814
  sourceDocument,
2386
1815
  languageFieldName,
@@ -2501,22 +1930,55 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2501
1930
  translationPrompts,
2502
1931
  translationMode,
2503
1932
  openaiConfig,
2504
- releaseId
1933
+ releaseId,
1934
+ excludeFields = [],
1935
+ replaceExisting = !1,
1936
+ mainSourceDocumentId
2505
1937
  }) => {
2506
- devLog.debug("Applying field transforms before translation", {
1938
+ const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
1939
+ excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
2507
1940
  documentId: sourceDocument._id,
2508
- documentType: sourceDocument._type
1941
+ languageFieldName,
1942
+ originalExcludeFields: excludeFields,
1943
+ safeExcludeFields
1944
+ }), devLog.info("Filtering excluded fields before translation", {
1945
+ documentId: sourceDocument._id,
1946
+ documentType: sourceDocument._type,
1947
+ excludeFields: safeExcludeFields,
1948
+ excludeFieldsLength: safeExcludeFields?.length ?? 0,
1949
+ sourceHasTitle: "title" in sourceDocument,
1950
+ sourceTitleValue: sourceDocument.title
2509
1951
  });
2510
- const { transformedDocument, originalStructure } = applyTransforms({
2511
- document: sourceDocument,
2512
- registry: globalTransformRegistry,
2513
- schemaType: sourceDocument._type
1952
+ const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
1953
+ devLog.info("After filtering excluded fields", {
1954
+ documentId: sourceDocument._id,
1955
+ filteredHasTitle: "title" in filteredDocument,
1956
+ filteredTitleValue: filteredDocument.title,
1957
+ excludedValuesKeys: Object.keys(excludedValues),
1958
+ excludedValues
2514
1959
  });
1960
+ let documentToTranslate, originalStructure = {};
1961
+ if (translationMode === "openai-only") {
1962
+ devLog.debug("Applying field transforms before translation (OpenAI mode)", {
1963
+ documentId: sourceDocument._id,
1964
+ documentType: sourceDocument._type
1965
+ });
1966
+ const transformResult = applyTransforms({
1967
+ document: filteredDocument,
1968
+ registry: globalTransformRegistry,
1969
+ schemaType: sourceDocument._type
1970
+ });
1971
+ documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
1972
+ } else
1973
+ devLog.debug("Skipping field transforms (Sanity Agent mode)", {
1974
+ documentId: sourceDocument._id,
1975
+ documentType: sourceDocument._type
1976
+ }), documentToTranslate = filteredDocument;
2515
1977
  let translatedDoc;
2516
1978
  switch (translationMode) {
2517
1979
  case "sanity-only": {
2518
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2519
- sourceDocument: transformedDocument,
1980
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), translatedDoc = await getSanityAgentTranslation({
1981
+ sourceDocument: documentToTranslate,
2520
1982
  targetLanguage,
2521
1983
  languageFieldName,
2522
1984
  schemaId,
@@ -2528,9 +1990,16 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2528
1990
  case "openai-only": {
2529
1991
  if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2530
1992
  throw new Error("OpenAI is not configured but openai-only mode was selected");
2531
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
1993
+ setJobStatus(
1994
+ dispatch,
1995
+ sourceDocument._id,
1996
+ targetLanguage,
1997
+ "translating_with_openai",
1998
+ void 0,
1999
+ mainSourceDocumentId
2000
+ ), translatedDoc = await getOpenAITranslation({
2532
2001
  dispatch,
2533
- sourceDocument: transformedDocument,
2002
+ sourceDocument: documentToTranslate,
2534
2003
  targetLanguage,
2535
2004
  languageFieldName,
2536
2005
  translationPrompts,
@@ -2538,27 +2007,45 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2538
2007
  });
2539
2008
  break;
2540
2009
  }
2541
- case "sanity-with-review":
2542
- throw new Error("Sanity with review translation method is not implemented");
2543
2010
  default:
2544
2011
  throw new Error(`Invalid translation mode: ${translationMode}`);
2545
2012
  }
2546
2013
  if (!translatedDoc)
2547
2014
  return null;
2548
- devLog.debug("Restoring field transforms after translation", {
2015
+ let transformedResult;
2016
+ Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
2549
2017
  documentId: sourceDocument._id,
2550
2018
  documentType: sourceDocument._type
2551
- });
2552
- const finalDoc = restoreTransforms({
2019
+ }), transformedResult = restoreTransforms({
2553
2020
  translatedDocument: translatedDoc,
2554
2021
  originalStructure,
2555
2022
  registry: globalTransformRegistry,
2556
2023
  schemaType: sourceDocument._type
2557
- }), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2024
+ })) : transformedResult = translatedDoc;
2025
+ const excludedValuesToRestore = { ...excludedValues };
2026
+ delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
2027
+ documentId: sourceDocument._id,
2028
+ excludedFieldCount: Object.keys(excludedValues).length,
2029
+ excludedValuesKeys: Object.keys(excludedValues),
2030
+ languageFieldExcluded: languageFieldName in excludedValues,
2031
+ transformedResultHasTitle: "title" in transformedResult,
2032
+ transformedResultTitle: transformedResult.title
2033
+ });
2034
+ const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
2035
+ devLog.info("After restoring excluded fields", {
2036
+ documentId: sourceDocument._id,
2037
+ finalDocHasTitle: "title" in finalDoc,
2038
+ finalDocTitle: finalDoc.title
2039
+ });
2040
+ const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2558
2041
  ...finalDoc,
2559
2042
  _id: targetDocumentVersionId
2560
2043
  };
2561
- return await client.action({
2044
+ return replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
2045
+ documentId: sourceDocument._id,
2046
+ targetDocumentVersionId,
2047
+ releaseId
2048
+ }), await client.delete(targetDocumentVersionId)), await client.action({
2562
2049
  actionType: "sanity.action.document.version.create",
2563
2050
  publishedId: targetDocumentPublishedId,
2564
2051
  document: newDoc
@@ -2570,36 +2057,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2570
2057
  sourceLanguage,
2571
2058
  targetLanguage,
2572
2059
  languageFieldName,
2573
- shouldOverwriteFreshTranslations,
2574
2060
  schemaId,
2575
2061
  translationPrompts,
2576
2062
  translationMode,
2577
2063
  openaiConfig,
2578
- releaseId
2064
+ releaseId,
2065
+ excludeFields,
2066
+ replaceExisting,
2067
+ mainSourceDocumentId
2579
2068
  }) => {
2580
2069
  const logContext = {
2581
2070
  documentId: sourceDocument._id,
2582
2071
  targetLanguage,
2583
2072
  schemaId,
2584
- shouldOverwriteFreshTranslations,
2585
2073
  translationMode
2586
2074
  };
2587
- devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2075
+ devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
2588
2076
  try {
2589
2077
  const translationMetadata = await getTranslationMetadata({
2590
2078
  client,
2591
2079
  sourceDocument,
2592
2080
  sourceLanguage
2593
- }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
2594
- (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
2595
- );
2596
- if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2597
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2598
- targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
2599
- });
2600
- return;
2601
- }
2602
- const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2081
+ }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2603
2082
  dispatch,
2604
2083
  client,
2605
2084
  schemaId,
@@ -2610,15 +2089,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2610
2089
  translationPrompts,
2611
2090
  translationMode,
2612
2091
  openaiConfig,
2613
- releaseId
2092
+ releaseId,
2093
+ excludeFields,
2094
+ replaceExisting,
2095
+ mainSourceDocumentId
2614
2096
  });
2615
2097
  if (!newDocument) {
2616
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2098
+ devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
2099
+ dispatch,
2100
+ sourceDocument._id,
2101
+ targetLanguage,
2102
+ "error",
2103
+ {
2104
+ errorMessage: "Translation failed"
2105
+ },
2106
+ mainSourceDocumentId
2107
+ ), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2617
2108
  return;
2618
2109
  }
2619
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2620
- targetId: newDocument._id
2621
- }), await updateTranslationMetadata({
2110
+ return setJobStatus(
2111
+ dispatch,
2112
+ sourceDocument._id,
2113
+ targetLanguage,
2114
+ "updating_metadata",
2115
+ {
2116
+ targetId: newDocument._id
2117
+ },
2118
+ mainSourceDocumentId
2119
+ ), await updateTranslationMetadata({
2622
2120
  client,
2623
2121
  sourceDocument,
2624
2122
  sourceLanguage,
@@ -2626,17 +2124,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2626
2124
  targetLanguage,
2627
2125
  translationMetadata,
2628
2126
  isNewTranslation: !existingTranslationId
2629
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
2630
- targetId: newDocument._id
2631
- }), devLog.info("Translation completed successfully", {
2127
+ }), setJobStatus(
2128
+ dispatch,
2129
+ sourceDocument._id,
2130
+ targetLanguage,
2131
+ "completed",
2132
+ {
2133
+ targetId: newDocument._id
2134
+ },
2135
+ mainSourceDocumentId
2136
+ ), devLog.info("Translation completed successfully", {
2632
2137
  ...logContext,
2633
2138
  newDocumentId: newDocument._id
2634
2139
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
2635
2140
  } catch (error) {
2636
2141
  const errorMessage = error instanceof Error ? error.message : String(error);
2637
- devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2638
- errorMessage
2639
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2142
+ devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
2143
+ dispatch,
2144
+ sourceDocument._id,
2145
+ targetLanguage,
2146
+ "error",
2147
+ {
2148
+ errorMessage
2149
+ },
2150
+ mainSourceDocumentId
2151
+ ), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2640
2152
  return;
2641
2153
  }
2642
2154
  }, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
@@ -2645,12 +2157,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2645
2157
  targetLanguages,
2646
2158
  dispatch,
2647
2159
  languageFieldName,
2648
- shouldOverwriteFreshTranslations,
2649
2160
  schemaId,
2650
2161
  references = [],
2651
2162
  maxConcurrentTranslationJobs,
2652
2163
  translationMode,
2653
- openaiConfig
2164
+ openaiConfig,
2165
+ excludeFields,
2166
+ releaseConfig
2654
2167
  }) => {
2655
2168
  const logContext = {
2656
2169
  documentId: sourceDocument._id,
@@ -2659,28 +2172,36 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2659
2172
  referencesCount: references.length,
2660
2173
  maxConcurrentJobs: maxConcurrentTranslationJobs,
2661
2174
  translationMode,
2662
- shouldOverwriteFreshTranslations
2175
+ releaseMode: releaseConfig.mode
2663
2176
  };
2664
- let releaseId;
2177
+ let releaseId, createdNewRelease = !1;
2665
2178
  try {
2666
2179
  const sourceLanguage = sourceDocument[languageFieldName];
2667
2180
  if (!sourceLanguage)
2668
2181
  throw devLog.error("Source language not found"), new Error("Source language not found");
2669
- maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2182
+ if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2670
2183
  type: "SET_OVERALL_STATUS",
2671
2184
  payload: "translating"
2672
- });
2673
- 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({
2674
- metadata: {
2675
- title: releaseTitle,
2676
- description: releaseDescription,
2677
- releaseType: "undecided"
2678
- }
2679
- });
2680
- devLog.info("Content Release created", {
2681
- ...logContext,
2682
- releaseId: releaseId2
2683
- });
2185
+ }), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
2186
+ releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
2187
+ ...logContext,
2188
+ releaseId
2189
+ });
2190
+ else {
2191
+ const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
2192
+ releaseId = (await client.releases.create({
2193
+ metadata: {
2194
+ title: releaseTitle,
2195
+ description: releaseDescription,
2196
+ releaseType: "undecided"
2197
+ }
2198
+ })).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
2199
+ ...logContext,
2200
+ releaseId,
2201
+ releaseTitle
2202
+ });
2203
+ }
2204
+ registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
2684
2205
  const sourceDocuments = [
2685
2206
  {
2686
2207
  _id: sourceDocument._id,
@@ -2695,7 +2216,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2695
2216
  targetLanguages,
2696
2217
  sourceDocument
2697
2218
  });
2698
- setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2219
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
2699
2220
  const translationTasks = sourceDocumentIds.flatMap(
2700
2221
  (documentId) => targetLanguages.map(async (targetLanguage) => {
2701
2222
  try {
@@ -2703,7 +2224,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2703
2224
  const document = await client.withConfig({
2704
2225
  perspective: "drafts"
2705
2226
  }).getDocument(documentId);
2706
- if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2227
+ if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
2707
2228
  const versionedTargetDocumentId = await translateDocument({
2708
2229
  dispatch,
2709
2230
  client,
@@ -2711,12 +2232,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2711
2232
  sourceLanguage,
2712
2233
  targetLanguage,
2713
2234
  languageFieldName,
2714
- shouldOverwriteFreshTranslations,
2715
2235
  schemaId,
2716
2236
  translationPrompts,
2717
2237
  translationMode,
2718
2238
  openaiConfig,
2719
- releaseId: releaseId2
2239
+ releaseId,
2240
+ excludeFields,
2241
+ replaceExisting: releaseConfig.replaceExisting,
2242
+ // Pass main source document ID for active translations tracking
2243
+ mainSourceDocumentId: sourceDocument._id
2720
2244
  });
2721
2245
  versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
2722
2246
  }
@@ -2743,7 +2267,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2743
2267
  }), dispatch({
2744
2268
  type: "SET_OVERALL_STATUS",
2745
2269
  payload: "linking_references"
2746
- }), await Promise.all(
2270
+ }), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
2747
2271
  versionedTargetDocumentIds.map(async (documentId) => {
2748
2272
  await localizeReferences({
2749
2273
  client,
@@ -2754,18 +2278,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2754
2278
  })
2755
2279
  );
2756
2280
  const hasAnyFailures = failed > 0;
2757
- 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", {
2281
+ 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", {
2758
2282
  ...logContext,
2759
2283
  succeeded,
2760
2284
  failed
2761
- }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
2285
+ }), 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();
2762
2286
  } catch (error) {
2763
- throw devLog.error(
2287
+ if (devLog.error(
2764
2288
  "translateDocumentSet failed with unexpected error",
2765
2289
  error instanceof Error ? error : new Error(String(error)),
2766
2290
  logContext
2767
- ), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
2768
- }
2291
+ ), releaseId && createdNewRelease)
2292
+ try {
2293
+ await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
2294
+ ...logContext,
2295
+ releaseId
2296
+ });
2297
+ } catch (cleanupError) {
2298
+ devLog.error(
2299
+ "Failed to cleanup release",
2300
+ cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
2301
+ logContext
2302
+ );
2303
+ }
2304
+ throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
2305
+ }
2306
+ }, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
2307
+ 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]);
2308
+ return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2309
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2310
+ /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
2311
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
2312
+ ] }),
2313
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2314
+ "The selected release already has ",
2315
+ summaryText,
2316
+ " that will be overwritten if you continue:"
2317
+ ] }),
2318
+ /* @__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: [
2319
+ conflict.sourceDocumentTitle || conflict.sourceDocumentId,
2320
+ conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
2321
+ " \u2192 ",
2322
+ conflict.targetLanguage
2323
+ ] }, conflict.existingVersionId)) }) }) }),
2324
+ /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2325
+ /* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
2326
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2327
+ /* @__PURE__ */ jsx("strong", { children: "Cancel" }),
2328
+ " \u2014 Go back and select a different release, or create a new one."
2329
+ ] }),
2330
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2331
+ /* @__PURE__ */ jsx("strong", { children: "Replace" }),
2332
+ " \u2014 Delete the existing translations in this release and create new ones."
2333
+ ] })
2334
+ ] }),
2335
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
2336
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
2337
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
2338
+ ] })
2339
+ ] }) });
2769
2340
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
2770
2341
  const {
2771
2342
  document,
@@ -2774,37 +2345,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2774
2345
  dispatch,
2775
2346
  references,
2776
2347
  selectedTargetLanguages,
2777
- shouldOverwriteFreshTranslations,
2778
- translationMode
2779
- } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), handleTranslate = useCallback(() => {
2780
- translateDocumentSet({
2781
- sourceDocument: document,
2782
- targetLanguages: selectedTargetLanguages,
2348
+ translationMode,
2349
+ releaseConfig
2350
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
2351
+ useEffect(() => {
2352
+ releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2353
+ }, [releaseConfig.mode, conflictContext]);
2354
+ const startTranslation = useCallback(
2355
+ (replaceExisting, overrideReleaseId) => {
2356
+ setConflictContext(null), translateDocumentSet({
2357
+ sourceDocument: document,
2358
+ targetLanguages: selectedTargetLanguages,
2359
+ client,
2360
+ dispatch,
2361
+ languageFieldName: config.languageFieldName,
2362
+ schemaId: config.schemaId,
2363
+ references,
2364
+ maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2365
+ maxReferenceDepth: config.maxReferenceDepth,
2366
+ translationMode,
2367
+ openaiConfig: config.openai,
2368
+ excludeFields: config.excludeFields,
2369
+ releaseConfig: {
2370
+ ...releaseConfig,
2371
+ replaceExisting,
2372
+ // Use the override release ID if provided (from conflict context),
2373
+ // otherwise use the current releaseConfig value
2374
+ existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
2375
+ }
2376
+ });
2377
+ },
2378
+ [
2379
+ document,
2380
+ selectedTargetLanguages,
2783
2381
  client,
2784
2382
  dispatch,
2785
- languageFieldName: config.languageFieldName,
2786
- schemaId: config.schemaId,
2383
+ config.languageFieldName,
2384
+ config.schemaId,
2385
+ config.maxConcurrentTranslationJobs,
2386
+ config.maxReferenceDepth,
2387
+ config.openai,
2388
+ config.excludeFields,
2787
2389
  references,
2788
- shouldOverwriteFreshTranslations,
2789
- maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2790
- maxReferenceDepth: config.maxReferenceDepth,
2791
2390
  translationMode,
2792
- openaiConfig: config.openai
2793
- });
2794
- }, [
2795
- document,
2796
- selectedTargetLanguages,
2797
- client,
2798
- dispatch,
2799
- config.languageFieldName,
2800
- config.schemaId,
2801
- config.maxConcurrentTranslationJobs,
2802
- config.maxReferenceDepth,
2803
- config.openai,
2804
- references,
2805
- shouldOverwriteFreshTranslations,
2806
- translationMode
2807
- ]), handleSelectAll = useCallback(() => {
2391
+ releaseConfig
2392
+ ]
2393
+ ), handleTranslate = useCallback(async () => {
2394
+ if (releaseConfig.mode === "new") {
2395
+ startTranslation(!1);
2396
+ return;
2397
+ }
2398
+ if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
2399
+ const checkedReleaseId = releaseConfig.existingReleaseId;
2400
+ setIsCheckingConflicts(!0);
2401
+ const foundConflicts = await checkReleaseConflicts({
2402
+ client,
2403
+ releaseId: checkedReleaseId,
2404
+ sourceDocument: document,
2405
+ targetLanguages: selectedTargetLanguages,
2406
+ languageFieldName: config.languageFieldName,
2407
+ references
2408
+ });
2409
+ if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
2410
+ setConflictContext({
2411
+ releaseId: checkedReleaseId,
2412
+ conflicts: foundConflicts
2413
+ });
2414
+ return;
2415
+ }
2416
+ }
2417
+ startTranslation(!1);
2418
+ }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
2419
+ setConflictContext(null);
2420
+ }, []), handleReplaceConflict = useCallback(() => {
2421
+ conflictContext && startTranslation(!0, conflictContext.releaseId);
2422
+ }, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
2808
2423
  dispatch({
2809
2424
  type: "SET_SELECTED_TARGET_LANGUAGES",
2810
2425
  payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
@@ -2815,7 +2430,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2815
2430
  payload: []
2816
2431
  });
2817
2432
  }, [dispatch]);
2818
- return /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
2433
+ return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
2434
+ ReleaseConflictWarning,
2435
+ {
2436
+ conflicts: conflictContext.conflicts,
2437
+ onCancel: handleCancelConflict,
2438
+ onReplace: handleReplaceConflict
2439
+ }
2440
+ ) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
2819
2441
  /* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
2820
2442
  /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
2821
2443
  /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
@@ -2829,16 +2451,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2829
2451
  tone: "positive",
2830
2452
  size: 2,
2831
2453
  onClick: handleTranslate,
2832
- disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
2833
- children: "Translate"
2454
+ disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
2455
+ children: isCheckingConflicts ? "Checking..." : "Translate"
2834
2456
  }
2835
2457
  )
2836
2458
  ] })
2837
2459
  ] });
2460
+ }, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
2461
+ const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
2462
+ params: {
2463
+ documentId: undraftId(documentId),
2464
+ translationMetadataType: TRANSLATION_METADATA_TYPE
2465
+ },
2466
+ initialValue: null
2467
+ });
2468
+ return useEffect(() => {
2469
+ if (loading) {
2470
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
2471
+ return;
2472
+ }
2473
+ if (error) {
2474
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
2475
+ return;
2476
+ }
2477
+ if (data) {
2478
+ dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
2479
+ return;
2480
+ }
2481
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
2482
+ }, [data, loading, error, dispatch]), {
2483
+ data,
2484
+ loading,
2485
+ error
2486
+ };
2838
2487
  }, LanguageCheckbox = ({ lang, isLoading }) => {
2839
- 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(
2840
- (history) => history.language === lang.id && history.sourceRev !== document._rev
2841
- ), handleChange = () => {
2488
+ const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
2842
2489
  if (isDisabled) return;
2843
2490
  const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
2844
2491
  dispatch({
@@ -2856,16 +2503,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2856
2503
  disabled: isDisabled,
2857
2504
  tone: isChecked ? "primary" : "default",
2858
2505
  muted: !0,
2859
- children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: [
2860
- /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
2861
- /* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
2862
- /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
2863
- /* @__PURE__ */ jsx(Text, { children: lang.title }),
2864
- 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" })
2865
- ] })
2866
- ] }),
2867
- !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" }) })
2868
- ] })
2506
+ children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
2507
+ /* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
2508
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
2509
+ /* @__PURE__ */ jsx(Text, { children: lang.title }),
2510
+ isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
2511
+ ] })
2512
+ ] }) })
2869
2513
  }
2870
2514
  );
2871
2515
  }, LanguageSelectionPanel = () => {
@@ -2873,6 +2517,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2873
2517
  documentId: document._id
2874
2518
  });
2875
2519
  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)) }) });
2520
+ }, ReleaseSelectionPanel = () => {
2521
+ const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
2522
+ useEffect(() => {
2523
+ (async () => {
2524
+ setIsLoading(!0), setError(null);
2525
+ try {
2526
+ const result = await client.fetch(
2527
+ 'releases::all()[state == "active"] | order(_createdAt desc)'
2528
+ );
2529
+ setReleases(result), devLog.debug("Fetched active releases", {
2530
+ count: result.length,
2531
+ component: "ReleaseSelectionPanel"
2532
+ });
2533
+ } catch (err) {
2534
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
2535
+ setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
2536
+ component: "ReleaseSelectionPanel"
2537
+ });
2538
+ } finally {
2539
+ setIsLoading(!1);
2540
+ }
2541
+ })();
2542
+ }, [client]);
2543
+ const handleModeChange = useCallback(
2544
+ (mode) => {
2545
+ dispatch({
2546
+ type: "SET_RELEASE_CONFIG",
2547
+ payload: { mode }
2548
+ });
2549
+ },
2550
+ [dispatch]
2551
+ ), handleTitleChange = useCallback(
2552
+ (event) => {
2553
+ dispatch({
2554
+ type: "SET_RELEASE_CONFIG",
2555
+ payload: { newReleaseTitle: event.target.value }
2556
+ });
2557
+ },
2558
+ [dispatch]
2559
+ ), handleSelectRelease = useCallback(
2560
+ (releaseId) => {
2561
+ dispatch({
2562
+ type: "SET_RELEASE_CONFIG",
2563
+ payload: { existingReleaseId: releaseId }
2564
+ });
2565
+ },
2566
+ [dispatch]
2567
+ ), handleSearchChange = useCallback((event) => {
2568
+ setSearchQuery(event.target.value);
2569
+ }, []), filteredReleases = useMemo(() => {
2570
+ if (!searchQuery.trim())
2571
+ return releases;
2572
+ const query = searchQuery.toLowerCase();
2573
+ return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
2574
+ }, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
2575
+ month: "short",
2576
+ day: "numeric",
2577
+ year: "numeric"
2578
+ });
2579
+ return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2580
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2581
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
2582
+ /* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
2583
+ /* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
2584
+ ] }),
2585
+ /* @__PURE__ */ jsxs(
2586
+ Flex,
2587
+ {
2588
+ align: "center",
2589
+ gap: 3,
2590
+ as: "label",
2591
+ style: { cursor: "pointer" },
2592
+ onClick: () => handleModeChange("existing"),
2593
+ children: [
2594
+ /* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
2595
+ /* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
2596
+ ]
2597
+ }
2598
+ )
2599
+ ] }),
2600
+ releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
2601
+ TextInput,
2602
+ {
2603
+ value: releaseConfig.newReleaseTitle,
2604
+ onChange: handleTitleChange,
2605
+ placeholder: "Release title..."
2606
+ }
2607
+ ),
2608
+ releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2609
+ isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2610
+ /* @__PURE__ */ jsx(Spinner, { muted: !0 }),
2611
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
2612
+ ] }),
2613
+ error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
2614
+ !isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
2615
+ !isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2616
+ /* @__PURE__ */ jsx(
2617
+ TextInput,
2618
+ {
2619
+ icon: SearchIcon,
2620
+ value: searchQuery,
2621
+ onChange: handleSearchChange,
2622
+ placeholder: "Search releases..."
2623
+ }
2624
+ ),
2625
+ /* @__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) => {
2626
+ const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
2627
+ return /* @__PURE__ */ jsx(
2628
+ Card,
2629
+ {
2630
+ padding: 3,
2631
+ radius: 2,
2632
+ tone: isSelected ? "primary" : "default",
2633
+ style: { cursor: "pointer" },
2634
+ onClick: () => handleSelectRelease(releaseName),
2635
+ border: !0,
2636
+ muted: isSelected,
2637
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
2638
+ /* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
2639
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
2640
+ ] })
2641
+ },
2642
+ release._id
2643
+ );
2644
+ }) }) })
2645
+ ] })
2646
+ ] })
2647
+ ] }) });
2876
2648
  }, TranslationModePanel = () => {
2877
2649
  const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
2878
2650
  if (!config?.openai?.enabled || !config?.openai?.apiKey)
@@ -2891,7 +2663,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2891
2663
  return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2892
2664
  /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2893
2665
  /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
2894
- /* @__PURE__ */ jsx(Badge, { tone: "positive", children: "GPT-5 Available" })
2666
+ /* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
2895
2667
  ] }),
2896
2668
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2897
2669
  /* @__PURE__ */ jsx(
@@ -2921,32 +2693,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2921
2693
  /* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
2922
2694
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2923
2695
  /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2924
- /* @__PURE__ */ jsx(Text, { children: "GPT-5 Only" }),
2696
+ /* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
2925
2697
  /* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
2926
2698
  ] }),
2927
2699
  /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
2928
2700
  ] })
2929
2701
  ] })
2930
2702
  }
2931
- ),
2932
- /* @__PURE__ */ jsx(
2933
- Button,
2934
- {
2935
- type: "button",
2936
- mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
2937
- tone: translationMode === "sanity-with-review" ? "primary" : "default",
2938
- onClick: () => handleModeChange("sanity-with-review"),
2939
- children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2940
- /* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
2941
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2942
- /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2943
- /* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
2944
- /* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
2945
- ] }),
2946
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
2947
- ] })
2948
- ] })
2949
- }
2950
2703
  )
2951
2704
  ] }),
2952
2705
  translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
@@ -3109,41 +2862,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3109
2862
  ] }),
3110
2863
  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
3111
2864
  ] });
3112
- }, WriteSettingsPanel = () => {
3113
- const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
3114
- dispatch({
3115
- type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
3116
- payload: !shouldOverwriteFreshTranslations
3117
- });
3118
- };
3119
- return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3120
- /* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
3121
- /* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
3122
- Button,
3123
- {
3124
- type: "button",
3125
- mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
3126
- size: 2,
3127
- onClick: handleShouldOverwriteFreshTranslationsChange,
3128
- tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
3129
- children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3130
- /* @__PURE__ */ jsx(
3131
- Checkbox,
3132
- {
3133
- checked: shouldOverwriteFreshTranslations,
3134
- onChange: handleShouldOverwriteFreshTranslationsChange,
3135
- label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
3136
- }
3137
- ),
3138
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3139
- /* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
3140
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
3141
- ] })
3142
- ] })
3143
- }
3144
- ) })
3145
- ] });
3146
- }, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2865
+ }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3147
2866
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3148
2867
  /* @__PURE__ */ jsx(Label, { children: "Languages" }),
3149
2868
  /* @__PURE__ */ jsx(LanguageSelectionPanel, {})
@@ -3152,16 +2871,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3152
2871
  /* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
3153
2872
  /* @__PURE__ */ jsx(TranslationModePanel, {})
3154
2873
  ] }),
2874
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2875
+ /* @__PURE__ */ jsx(Label, { children: "Content Release" }),
2876
+ /* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
2877
+ ] }),
3155
2878
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3156
2879
  /* @__PURE__ */ jsx(Label, { children: "Settings" }),
3157
2880
  /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
3158
2881
  ] }),
3159
2882
  /* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
3160
2883
  ] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
3161
- const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
3162
- () => overallStatus === "completed" ? "Translation completed" : "Translation in progress",
3163
- [overallStatus]
3164
- ), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
2884
+ 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(() => {
2885
+ forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
2886
+ }, [dispatch, document._id]), openaiStats = useMemo(() => {
3165
2887
  if (translationMode === "sanity-only") return null;
3166
2888
  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);
3167
2889
  return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
@@ -3183,7 +2905,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3183
2905
  /* @__PURE__ */ jsxs(Code, { size: 1, children: [
3184
2906
  job._type,
3185
2907
  " \u2014 ",
3186
- job._id.slice(-6)
2908
+ job._id?.slice(-6) || "unknown"
3187
2909
  ] }),
3188
2910
  !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3189
2911
  job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
@@ -3217,18 +2939,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3217
2939
  /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
3218
2940
  /* @__PURE__ */ jsxs(Inline, { space: 4, children: [
3219
2941
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3220
- "\u{1F4CA} Total Tokens: ",
2942
+ "Total Tokens: ",
3221
2943
  /* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
3222
2944
  ] }),
3223
2945
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3224
- "\u{1F4B0} Total Cost: ",
2946
+ "Total Cost: ",
3225
2947
  /* @__PURE__ */ jsxs("strong", { children: [
3226
2948
  "$",
3227
2949
  openaiStats.totalCost.toFixed(4)
3228
2950
  ] })
3229
2951
  ] })
3230
2952
  ] })
3231
- ] }) })
2953
+ ] }) }),
2954
+ isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
3232
2955
  ] });
3233
2956
  }, ProgressBar = ({ progress, max = 100 }) => {
3234
2957
  const percentage = Math.min(progress / max * 100, 100);
@@ -3292,8 +3015,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3292
3015
  queued: "Queued...",
3293
3016
  preparing_translation: "Preparing translation...",
3294
3017
  translating: "Translating with Sanity Agent...",
3295
- translating_with_openai: "Translating with GPT-5...",
3296
- reviewing_with_openai: "Reviewing translation with GPT-5...",
3018
+ translating_with_openai: "Translating with OpenAI...",
3297
3019
  updating_metadata: "Saving metadata...",
3298
3020
  error: "Error",
3299
3021
  skipped: "Skipped",
@@ -3302,14 +3024,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3302
3024
  "translating",
3303
3025
  "linking_references",
3304
3026
  "completed"
3305
- ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3027
+ ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
3306
3028
  const { overallStatus, document } = useTranslationContext();
3307
- 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", {
3029
+ return useEffect(() => {
3030
+ onStatusChange?.(overallStatus);
3031
+ }, [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", {
3308
3032
  documentId: document._id,
3309
3033
  overallStatus
3310
3034
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3311
- }, getTranslateAction = (config) => ({ draft, published }) => {
3312
- const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
3035
+ }, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
3036
+ const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
3037
+ setCurrentStatus(status);
3038
+ }, []), handleClose = useCallback(() => {
3039
+ if (isTranslating) {
3040
+ if (!window.confirm(
3041
+ "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."
3042
+ ))
3043
+ return;
3044
+ devLog.warn("User closed dialog during active translation", {
3045
+ documentId: document?._id,
3046
+ component: "TranslateAction"
3047
+ });
3048
+ }
3049
+ setIsDialogOpen(!1);
3050
+ }, [isTranslating, document?._id]);
3313
3051
  return devLog.debug("TranslateAction initialized", {
3314
3052
  documentId: document?._id,
3315
3053
  documentType: document?._type
@@ -3317,7 +3055,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3317
3055
  documentType: document?._type,
3318
3056
  translatableTypes: config.translatableSchemaTypes
3319
3057
  }), null) : config ? {
3320
- label: "Translate",
3058
+ label: getButtonLabel(currentStatus),
3321
3059
  icon: TranslateIcon,
3322
3060
  onHandle: () => {
3323
3061
  devLog.info("Translation dialog opened", {
@@ -3328,9 +3066,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3328
3066
  },
3329
3067
  dialog: isDialogOpen && {
3330
3068
  type: "dialog",
3331
- onClose: () => setIsDialogOpen(!1),
3069
+ onClose: handleClose,
3332
3070
  header: "Translate Document",
3333
- content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3071
+ content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
3334
3072
  }
3335
3073
  } : (devLog.error("Missing config in TranslateAction"), null);
3336
3074
  }, translationGlossaryTerm = defineType({
@@ -3446,7 +3184,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3446
3184
  defineField({
3447
3185
  name: "changeHistory",
3448
3186
  title: "Change History",
3449
- description: "History of changes made via user feedback.",
3187
+ description: "History of changes made to translation settings.",
3450
3188
  type: "array",
3451
3189
  readOnly: !0,
3452
3190
  group: "history",
@@ -3756,14 +3494,18 @@ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.le
3756
3494
  })), {
3757
3495
  name: "sanity-plugin-ai-translation",
3758
3496
  document: {
3759
- actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
3497
+ actions: [getTranslateAction(config)]
3760
3498
  },
3761
3499
  schema: {
3762
3500
  types: [translationSettings, translationGlossaryTerm]
3763
3501
  }
3764
3502
  })), translationHistorySchema = translationHistory;
3765
3503
  export {
3504
+ COMMON_ENUM_FIELDS,
3505
+ PREDEFINED_EXCLUSIONS,
3766
3506
  aiTranslationPlugin,
3507
+ filterExcludedFields,
3508
+ restoreExcludedFields,
3767
3509
  richTextTransform,
3768
3510
  translationHistorySchema,
3769
3511
  vttCaptionTransform