@braze/sanity-plugin-ai-translation 0.2.2 → 0.2.4

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,68 @@
1
- import { useClient, defineType, defineField, pathToString, definePlugin } from "sanity";
1
+ import { useClient, getPublishedId, pathToString, defineType, defineField, definePlugin } from "sanity";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, WarningOutlineIcon, InfoOutlineIcon, ArrowLeftIcon, EditIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
4
- import { createContext, useContext, useEffect, useState, useReducer, useCallback, useMemo } from "react";
4
+ import { createContext, useReducer, useEffect, useContext, useState, useCallback, useMemo } from "react";
5
+ import defaults from "lodash.defaults";
5
6
  import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Select, TextInput, TextArea, Heading, Inline, Badge, Grid, Radio, Label, Code } from "@sanity/ui";
6
7
  import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
7
- import defaults from "lodash.defaults";
8
8
  import { toPlainText } from "@portabletext/react";
9
+ import { uuid } from "@sanity/uuid";
9
10
  import cloneDeep from "lodash.clonedeep";
10
11
  import get from "lodash.get";
11
12
  import set from "lodash.set";
12
- const TranslationContext = createContext({
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
+ info: (message, context) => {
15
+ isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
16
+ },
17
+ warn: (message, context) => {
18
+ isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
19
+ },
20
+ error: (message, error, context) => {
21
+ isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
22
+ },
23
+ debug: (message, data, context) => {
24
+ isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
25
+ },
26
+ group: (label, context) => {
27
+ isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
28
+ },
29
+ groupEnd: () => {
30
+ isLoggingEnabled() && console.groupEnd();
31
+ },
32
+ time: (label) => {
33
+ isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
34
+ },
35
+ timeEnd: (label) => {
36
+ isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
37
+ }
38
+ }, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
39
+ try {
40
+ const response = await fetch("https://api.openai.com/v1/models", {
41
+ method: "GET",
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`
44
+ }
45
+ });
46
+ if (!response.ok)
47
+ return {
48
+ valid: !1,
49
+ error: (await response.json()).error?.message || "Invalid API key"
50
+ };
51
+ const availableModels = (await response.json()).data.map((m) => m.id);
52
+ return availableModels.includes(model) ? {
53
+ valid: !0,
54
+ modelAvailable: availableModels
55
+ } : {
56
+ valid: !1,
57
+ error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
58
+ };
59
+ } catch (error) {
60
+ return {
61
+ valid: !1,
62
+ error: error instanceof Error ? error.message : "Network error"
63
+ };
64
+ }
65
+ }, TranslationContext = createContext({
13
66
  document: null,
14
67
  config: null,
15
68
  overallStatus: "idle",
@@ -24,14 +77,126 @@ const TranslationContext = createContext({
24
77
  openaiValidated: !1,
25
78
  dispatch: () => {
26
79
  }
27
- }), useTranslationContext = () => {
80
+ }), translationReducer = (context, action) => {
81
+ const logContext = {
82
+ documentId: context.document?._id,
83
+ actionType: action.type,
84
+ component: "translationReducer"
85
+ };
86
+ switch (devLog.debug("Translation action dispatched", {
87
+ ...logContext,
88
+ payload: action.payload
89
+ }), action.type) {
90
+ case "SET_OVERALL_STATUS":
91
+ return { ...context, overallStatus: action.payload };
92
+ case "SET_TRANSLATION_METADATA":
93
+ return { ...context, translationMetadata: action.payload };
94
+ case "SET_SELECTED_TARGET_LANGUAGES":
95
+ return {
96
+ ...context,
97
+ selectedTargetLanguages: action.payload
98
+ };
99
+ case "SET_TRANSLATION_MODE":
100
+ return {
101
+ ...context,
102
+ translationMode: action.payload
103
+ };
104
+ case "SET_OPENAI_VALIDATED":
105
+ return {
106
+ ...context,
107
+ openaiValidated: action.payload
108
+ };
109
+ case "SET_SHOULD_TRANSLATE_REFERENCES":
110
+ return {
111
+ ...context,
112
+ shouldTranslateReferences: action.payload
113
+ };
114
+ case "SET_MAX_REFERENCE_DEPTH":
115
+ return {
116
+ ...context,
117
+ maxReferenceDepth: action.payload
118
+ };
119
+ case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
120
+ return {
121
+ ...context,
122
+ shouldOverwriteFreshTranslations: action.payload
123
+ };
124
+ case "SET_REFERENCES":
125
+ return {
126
+ ...context,
127
+ references: action.payload
128
+ };
129
+ case "SET_TRANSLATION_JOB_STATUS": {
130
+ const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
131
+ return newTranslationJobs.set(jobKey, mergedJob), {
132
+ ...context,
133
+ translationJobs: newTranslationJobs
134
+ };
135
+ }
136
+ default:
137
+ return context;
138
+ }
139
+ }, TranslationContextProvider = ({ document, config, children }) => {
140
+ devLog.debug("TranslationContextProvider initialized", {
141
+ documentId: document._id,
142
+ documentType: document._type,
143
+ supportedLanguages: config.supportedLanguages?.map((l) => l.id),
144
+ component: "TranslationContextProvider"
145
+ });
146
+ const initialContext = {
147
+ document,
148
+ config,
149
+ overallStatus: "idle",
150
+ translationMetadata: null,
151
+ selectedTargetLanguages: [],
152
+ shouldOverwriteFreshTranslations: !1,
153
+ shouldTranslateReferences: !1,
154
+ maxReferenceDepth: 1,
155
+ references: [],
156
+ translationJobs: /* @__PURE__ */ new Map(),
157
+ translationMode: config?.openai?.defaultMode || "sanity-only",
158
+ openaiValidated: !1
159
+ }, [context, dispatch] = useReducer(translationReducer, initialContext);
160
+ return useEffect(() => {
161
+ (async () => {
162
+ if (config?.openai?.enabled && config?.openai?.apiKey)
163
+ try {
164
+ const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
165
+ dispatch({
166
+ type: "SET_OPENAI_VALIDATED",
167
+ payload: validation.valid
168
+ }), validation.valid ? devLog.info("OpenAI API key validated successfully", {
169
+ model: config.openai.model || "gpt-5",
170
+ component: "TranslationContextProvider"
171
+ }) : devLog.error("OpenAI API key validation failed", {
172
+ error: validation.error,
173
+ component: "TranslationContextProvider"
174
+ });
175
+ } catch (error) {
176
+ devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
177
+ component: "TranslationContextProvider"
178
+ }), dispatch({
179
+ type: "SET_OPENAI_VALIDATED",
180
+ payload: !1
181
+ });
182
+ }
183
+ })();
184
+ }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
185
+ documentId: document._id,
186
+ overallStatus: context.overallStatus,
187
+ selectedLanguages: context.selectedTargetLanguages,
188
+ translationMode: context.translationMode,
189
+ openaiEnabled: config?.openai?.enabled,
190
+ component: "TranslationContextProvider"
191
+ }), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
192
+ }, useTranslationContext = () => {
28
193
  const context = useContext(TranslationContext);
29
194
  if (!context.document || !context.config)
30
195
  throw new Error(
31
196
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
32
197
  );
33
198
  return context;
34
- }, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
199
+ }, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
35
200
  const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
36
201
  params: {
37
202
  documentId: undraftId(documentId),
@@ -58,31 +223,6 @@ const TranslationContext = createContext({
58
223
  loading,
59
224
  error
60
225
  };
61
- }, isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
62
- info: (message, context) => {
63
- isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
64
- },
65
- warn: (message, context) => {
66
- isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
67
- },
68
- error: (message, error, context) => {
69
- isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
70
- },
71
- debug: (message, data, context) => {
72
- isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
73
- },
74
- group: (label, context) => {
75
- isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
76
- },
77
- groupEnd: () => {
78
- isLoggingEnabled() && console.groupEnd();
79
- },
80
- time: (label) => {
81
- isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
82
- },
83
- timeEnd: (label) => {
84
- isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
85
- }
86
226
  };
87
227
  async function validateSuggestion(client, suggestion) {
88
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" };
@@ -191,12 +331,7 @@ async function analyzeFeedback({
191
331
  currentDocument,
192
332
  onProgress
193
333
  }) {
194
- devLog.info("Starting feedback analysis", {
195
- sourceLanguage,
196
- targetLanguage,
197
- hasUserFeedback: !!userFeedback,
198
- shouldIncludeEdits
199
- }), onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
334
+ onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
200
335
  const analysisPrompt = buildAnalysisPrompt({
201
336
  sourceLanguage,
202
337
  targetLanguage,
@@ -205,10 +340,8 @@ async function analyzeFeedback({
205
340
  sourceSnapshot,
206
341
  targetSnapshot,
207
342
  currentDocument
208
- }), analysis = await analyzeWithAI(client, analysisPrompt);
209
- devLog.info("AI analysis complete", { analysis });
210
- const suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
211
- devLog.info("Generated suggestions", { count: suggestions.length }), onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
343
+ }), analysis = await analyzeWithAI(client, analysisPrompt), suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
344
+ onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
212
345
  const validatedSuggestions = await Promise.all(
213
346
  suggestions.map(async (suggestion) => {
214
347
  const validation = await validateSuggestion(client, suggestion);
@@ -221,7 +354,10 @@ async function analyzeFeedback({
221
354
  };
222
355
  })
223
356
  );
224
- return devLog.info("Suggestions validated", {
357
+ return devLog.info("Feedback analysis complete", {
358
+ sourceLanguage,
359
+ targetLanguage,
360
+ total: validatedSuggestions.length,
225
361
  valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
226
362
  warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
227
363
  invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
@@ -235,24 +371,18 @@ async function applySuggestions({
235
371
  suggestions,
236
372
  onProgress
237
373
  }) {
238
- if (devLog.info("Applying suggestions", {
239
- sourceLanguage,
240
- targetLanguage,
241
- targetLanguageType: typeof targetLanguage,
242
- targetLanguageValue: targetLanguage,
243
- count: suggestions.filter((s) => s.approved).length
244
- }), !targetLanguage)
374
+ if (!targetLanguage)
245
375
  throw devLog.error("targetLanguage is undefined in applySuggestions!", {
246
376
  sourceLanguage,
247
377
  targetLanguage,
248
378
  suggestionsCount: suggestions.length
249
379
  }), new Error("targetLanguage is required but was undefined");
250
380
  const approvedSuggestions = suggestions.filter((s) => s.approved);
251
- if (approvedSuggestions.length === 0) {
252
- devLog.info("No approved suggestions to apply");
253
- return;
254
- }
255
- onProgress?.({ stage: "applying", message: "Applying updates to translation settings..." }), await applyImprovementsFromSuggestions(client, schemaId, targetLanguage, approvedSuggestions, onProgress), devLog.info("Suggestions applied successfully");
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
+ }));
256
386
  }
257
387
  function buildAnalysisPrompt({
258
388
  sourceLanguage,
@@ -420,7 +550,7 @@ function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage)
420
550
  type: "glossaryTerm",
421
551
  description: `${term.sourceValue} \u2192 ${term.targetValue}`,
422
552
  approved: !0,
423
- // Use the provided source/target languages instead of relying on AI to provide them
553
+ // Use explicit source/target languages from history instead of AI-provided values
424
554
  sourceLocale: sourceLanguage,
425
555
  sourceValue: term.sourceValue,
426
556
  targetLocale: targetLanguage,
@@ -470,49 +600,21 @@ function parseAIResponse(responseText) {
470
600
  }
471
601
  }
472
602
  async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
473
- devLog.info("Applying improvements from suggestions", {
474
- count: suggestions.length,
475
- targetLanguage,
476
- suggestions: suggestions.map((s) => ({
477
- type: s.type,
478
- approved: s.approved,
479
- sourceLocale: s.sourceLocale,
480
- targetLocale: s.targetLocale,
481
- sourceValue: s.sourceValue,
482
- targetValue: s.targetValue
483
- }))
484
- });
485
603
  const settingsId = "translation-settings";
486
604
  await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
487
605
  _id: settingsId,
488
606
  _type: "translationSettings"
489
607
  });
490
- 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()), glossaryTermsBeforeFilter = suggestions.filter((s) => s.type === "glossaryTerm");
491
- devLog.info("Glossary terms before filtering", {
492
- count: glossaryTermsBeforeFilter.length,
493
- terms: glossaryTermsBeforeFilter.map((s) => ({
494
- sourceLocale: s.sourceLocale,
495
- sourceValue: s.sourceValue,
496
- targetLocale: s.targetLocale,
497
- targetValue: s.targetValue,
498
- approved: s.approved
499
- }))
500
- });
501
- const glossaryTerms = suggestions.filter(
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(
502
609
  (s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
503
610
  );
504
- if (devLog.info("Filtered suggestions by type", {
505
- globalPrompts: globalPrompts.length,
506
- localePrompts: localePrompts.length,
507
- protectedPhrases: protectedPhrases.length,
508
- glossaryTerms: glossaryTerms.length
509
- }), (globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
611
+ if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
510
612
  stage: "applying",
511
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...`
512
614
  }), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
513
615
  onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
514
616
  const phrases = protectedPhrases.map((s) => s.description);
515
- devLog.debug("Applying protected phrases", { phrases }), updates.push(addProtectedPhrases(client, settingsId, phrases));
617
+ updates.push(addProtectedPhrases(client, settingsId, phrases));
516
618
  }
517
619
  if (glossaryTerms.length > 0) {
518
620
  onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
@@ -524,22 +626,17 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
524
626
  targetValue: s.targetValue
525
627
  // biome-ignore-end lint/style/noNonNullAssertion: safely handled
526
628
  }));
527
- devLog.debug("Applying glossary terms", { terms }), updates.push(addGlossaryTerms(client, terms));
629
+ updates.push(addGlossaryTerms(client, terms));
528
630
  }
529
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(
530
632
  client,
531
633
  settingsId,
532
634
  targetLanguage,
533
635
  suggestions.filter((s) => s.approved)
534
- ), devLog.info("All improvements applied successfully");
636
+ );
535
637
  }
536
638
  async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
537
- if (devLog.info("Updating prompts with deterministic template", {
538
- settingsId,
539
- targetLanguage,
540
- hasGlobalUpdates: globalPrompts.length > 0,
541
- hasLocaleUpdates: localePrompts.length > 0
542
- }), !targetLanguage)
639
+ if (!targetLanguage)
543
640
  throw devLog.error("targetLanguage is undefined or empty!", {
544
641
  targetLanguage,
545
642
  globalPrompts: globalPrompts.length,
@@ -555,11 +652,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
555
652
  ...updates.map((update) => `- ${update}`)
556
653
  ].filter(Boolean).join(`
557
654
  `);
558
- await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit(), devLog.info("Global prompt updated", {
559
- previousLength: currentGlobalPrompt.length,
560
- newLength: newGlobalPrompt.length,
561
- addedUpdates: updates.length
562
- });
655
+ await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
563
656
  }
564
657
  if (localePrompts.length > 0) {
565
658
  const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
@@ -572,12 +665,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
572
665
  ...updates.map((update) => `- ${update}`)
573
666
  ].filter(Boolean).join(`
574
667
  `), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
575
- await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit(), devLog.info("Locale prompt updated", {
576
- targetLanguage,
577
- previousLength: currentValue.length,
578
- newLength: newValue.length,
579
- addedUpdates: updates.length
580
- });
668
+ await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
581
669
  } else {
582
670
  const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
583
671
  `);
@@ -587,22 +675,17 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
587
675
  _key: targetLanguage,
588
676
  value: newValue
589
677
  }
590
- ]).commit(), devLog.info("Locale prompt created", {
591
- targetLanguage,
592
- length: newValue.length,
593
- addedUpdates: updates.length
594
- });
678
+ ]).commit();
595
679
  }
596
680
  }
597
681
  devLog.info("Prompts updated successfully", {
682
+ targetLanguage,
598
683
  globalUpdated: globalPrompts.length > 0,
599
684
  localeUpdated: localePrompts.length > 0
600
685
  });
601
686
  }
602
687
  async function addProtectedPhrases(client, settingsId, phrases) {
603
- devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
604
688
  const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
605
- devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
606
689
  let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
607
690
  const conflictingPhrases = [];
608
691
  for (const phrase of newPhrases)
@@ -610,12 +693,7 @@ async function addProtectedPhrases(client, settingsId, phrases) {
610
693
  'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
611
694
  { phrase }
612
695
  ) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
613
- if (newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length === 0) {
614
- devLog.info("No new protected phrases to add (all already exist)");
615
- return;
616
- }
617
- const result = await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit();
618
- devLog.info("Protected phrases added", { count: newPhrases.length, phrases: newPhrases, resultId: result._id });
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 }));
619
697
  }
620
698
  async function addGlossaryTerms(client, terms) {
621
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)", {
@@ -643,29 +721,13 @@ async function addGlossaryTerms(client, terms) {
643
721
  );
644
722
  if (existingTerm) {
645
723
  const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
646
- existingTargetIndex === -1 ? (await client.patch(existingTerm._id).append("translations", [
724
+ existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
647
725
  {
648
726
  _type: "internationalizedArrayStringValue",
649
727
  _key: targetLocale,
650
728
  value: targetValue
651
729
  }
652
- ]).commit(), devLog.info("Added target language to existing glossary term", {
653
- sourceLocale,
654
- sourceValue,
655
- targetLocale,
656
- targetValue
657
- })) : existingTargetValue !== targetValue ? (await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit(), devLog.info("Updated existing glossary term translation", {
658
- sourceLocale,
659
- sourceValue,
660
- targetLocale,
661
- oldValue: existingTargetValue,
662
- newValue: targetValue
663
- })) : devLog.info("Glossary term already has this exact translation", {
664
- sourceLocale,
665
- sourceValue,
666
- targetLocale,
667
- targetValue
668
- });
730
+ ]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
669
731
  } else
670
732
  await client.create({
671
733
  _type: "translationGlossaryTerm",
@@ -681,11 +743,10 @@ async function addGlossaryTerms(client, terms) {
681
743
  value: targetValue
682
744
  }
683
745
  ]
684
- }), devLog.info("Created new glossary term", { sourceLocale, sourceValue, targetLocale, targetValue });
746
+ });
685
747
  }
686
748
  devLog.info("Glossary terms processed", {
687
- totalRequested: terms.length,
688
- validAdded: validTerms.length,
749
+ processed: validTerms.length,
689
750
  skipped: terms.length - validTerms.length
690
751
  });
691
752
  }
@@ -698,10 +759,7 @@ async function recordChangeHistory(client, settingsId, targetLanguage, suggestio
698
759
  description: suggestion.description,
699
760
  locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
700
761
  }));
701
- historyEntries.length !== 0 && (await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit(), devLog.info("Change history recorded", {
702
- count: historyEntries.length,
703
- timestamp
704
- }));
762
+ historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
705
763
  }
706
764
  const ICON_SIZE = 64, progressIconMap = {
707
765
  in_progress: /* @__PURE__ */ jsx(Spinner, {}),
@@ -1150,145 +1208,6 @@ const ICON_SIZE = 64, progressIconMap = {
1150
1208
  )
1151
1209
  ] })
1152
1210
  ] });
1153
- }, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
1154
- try {
1155
- const response = await fetch("https://api.openai.com/v1/models", {
1156
- method: "GET",
1157
- headers: {
1158
- Authorization: `Bearer ${apiKey}`
1159
- }
1160
- });
1161
- if (!response.ok)
1162
- return {
1163
- valid: !1,
1164
- error: (await response.json()).error?.message || "Invalid API key"
1165
- };
1166
- const availableModels = (await response.json()).data.map((m) => m.id);
1167
- return availableModels.includes(model) ? {
1168
- valid: !0,
1169
- modelAvailable: availableModels
1170
- } : {
1171
- valid: !1,
1172
- error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
1173
- };
1174
- } catch (error) {
1175
- return {
1176
- valid: !1,
1177
- error: error instanceof Error ? error.message : "Network error"
1178
- };
1179
- }
1180
- }, translationReducer = (context, action) => {
1181
- const logContext = {
1182
- documentId: context.document?._id,
1183
- actionType: action.type,
1184
- component: "translationReducer"
1185
- };
1186
- switch (devLog.debug("Translation action dispatched", {
1187
- ...logContext,
1188
- payload: action.payload
1189
- }), action.type) {
1190
- case "SET_OVERALL_STATUS":
1191
- return { ...context, overallStatus: action.payload };
1192
- case "SET_TRANSLATION_METADATA":
1193
- return { ...context, translationMetadata: action.payload };
1194
- case "SET_SELECTED_TARGET_LANGUAGES":
1195
- return {
1196
- ...context,
1197
- selectedTargetLanguages: action.payload
1198
- };
1199
- case "SET_TRANSLATION_MODE":
1200
- return {
1201
- ...context,
1202
- translationMode: action.payload
1203
- };
1204
- case "SET_OPENAI_VALIDATED":
1205
- return {
1206
- ...context,
1207
- openaiValidated: action.payload
1208
- };
1209
- case "SET_SHOULD_TRANSLATE_REFERENCES":
1210
- return {
1211
- ...context,
1212
- shouldTranslateReferences: action.payload
1213
- };
1214
- case "SET_MAX_REFERENCE_DEPTH":
1215
- return {
1216
- ...context,
1217
- maxReferenceDepth: action.payload
1218
- };
1219
- case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
1220
- return {
1221
- ...context,
1222
- shouldOverwriteFreshTranslations: action.payload
1223
- };
1224
- case "SET_REFERENCES":
1225
- return {
1226
- ...context,
1227
- references: action.payload
1228
- };
1229
- case "SET_TRANSLATION_JOB_STATUS": {
1230
- const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs);
1231
- return newTranslationJobs.set(jobKey, defaults(action.payload, newTranslationJobs.get(jobKey))), {
1232
- ...context,
1233
- translationJobs: newTranslationJobs
1234
- };
1235
- }
1236
- default:
1237
- return context;
1238
- }
1239
- }, TranslationContextProvider = ({ document, config, children }) => {
1240
- devLog.debug("TranslationContextProvider initialized", {
1241
- documentId: document._id,
1242
- documentType: document._type,
1243
- supportedLanguages: config.supportedLanguages?.map((l) => l.id),
1244
- component: "TranslationContextProvider"
1245
- });
1246
- const initialContext = {
1247
- document,
1248
- config,
1249
- overallStatus: "idle",
1250
- translationMetadata: null,
1251
- selectedTargetLanguages: [],
1252
- shouldOverwriteFreshTranslations: !1,
1253
- shouldTranslateReferences: !1,
1254
- maxReferenceDepth: 1,
1255
- references: [],
1256
- translationJobs: /* @__PURE__ */ new Map(),
1257
- translationMode: config?.openai?.defaultMode || "sanity-only",
1258
- openaiValidated: !1
1259
- }, [context, dispatch] = useReducer(translationReducer, initialContext);
1260
- return useEffect(() => {
1261
- (async () => {
1262
- if (config?.openai?.enabled && config?.openai?.apiKey)
1263
- try {
1264
- const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
1265
- dispatch({
1266
- type: "SET_OPENAI_VALIDATED",
1267
- payload: validation.valid
1268
- }), validation.valid ? devLog.info("OpenAI API key validated successfully", {
1269
- model: config.openai.model || "gpt-5",
1270
- component: "TranslationContextProvider"
1271
- }) : devLog.error("OpenAI API key validation failed", {
1272
- error: validation.error,
1273
- component: "TranslationContextProvider"
1274
- });
1275
- } catch (error) {
1276
- devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
1277
- component: "TranslationContextProvider"
1278
- }), dispatch({
1279
- type: "SET_OPENAI_VALIDATED",
1280
- payload: !1
1281
- });
1282
- }
1283
- })();
1284
- }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
1285
- documentId: document._id,
1286
- overallStatus: context.overallStatus,
1287
- selectedLanguages: context.selectedTargetLanguages,
1288
- translationMode: context.translationMode,
1289
- openaiEnabled: config?.openai?.enabled,
1290
- component: "TranslationContextProvider"
1291
- }), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
1292
1211
  }, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
1293
1212
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
1294
1213
  return devLog.debug("ImproveTranslationPromptsAction initialized", {
@@ -1314,181 +1233,7 @@ const ICON_SIZE = 64, progressIconMap = {
1314
1233
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
1315
1234
  }
1316
1235
  } : (devLog.error("Missing config in TranslateAction"), null);
1317
- }, translationGlossaryTerm = defineType({
1318
- name: "translationGlossaryTerm",
1319
- title: "Translation Glossary Term",
1320
- icon: TranslateIcon,
1321
- type: "document",
1322
- fields: [
1323
- defineField({
1324
- name: "translations",
1325
- title: "Translations",
1326
- type: "internationalizedArrayString"
1327
- })
1328
- ],
1329
- preview: {
1330
- select: {
1331
- translations: "translations"
1332
- },
1333
- prepare({ translations }) {
1334
- const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
1335
- return {
1336
- title: enTranslation?.value || firstTranslation?.value || "No translation available"
1337
- };
1338
- }
1339
- }
1340
- }), translationHistory = defineField({
1341
- name: "translationHistory",
1342
- type: "array",
1343
- of: [
1344
- {
1345
- name: "targetLanguageHistory",
1346
- type: "object",
1347
- fields: [
1348
- defineField({
1349
- name: "language",
1350
- title: "Language",
1351
- description: "The language of the target document",
1352
- type: "string"
1353
- }),
1354
- defineField({
1355
- name: "sourceId",
1356
- title: "Source ID",
1357
- description: "The ID of the source document",
1358
- type: "string"
1359
- }),
1360
- defineField({
1361
- name: "sourceRev",
1362
- title: "Source Revision",
1363
- description: "The revision ID of the source document at the time of translation",
1364
- type: "string"
1365
- }),
1366
- defineField({
1367
- name: "sourceLanguage",
1368
- title: "Source Language",
1369
- description: "The language of the source document",
1370
- type: "string"
1371
- }),
1372
- defineField({
1373
- name: "sourceSnapshot",
1374
- title: "Source Snapshot",
1375
- description: "The snapshot of the source document at the time of translation",
1376
- type: "text"
1377
- }),
1378
- defineField({
1379
- name: "targetId",
1380
- title: "Target ID",
1381
- description: "The ID of the target document",
1382
- type: "string"
1383
- })
1384
- ]
1385
- }
1386
- ]
1387
- }), translationSettings = defineType({
1388
- name: "translationSettings",
1389
- title: "Translation Settings",
1390
- icon: TranslateIcon,
1391
- type: "document",
1392
- groups: [
1393
- {
1394
- name: "general",
1395
- title: "General",
1396
- default: !0
1397
- },
1398
- {
1399
- name: "history",
1400
- title: "History"
1401
- }
1402
- ],
1403
- fields: [
1404
- defineField({
1405
- name: "globalPrompt",
1406
- title: "Global Translation Prompt",
1407
- description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
1408
- type: "text",
1409
- rows: 5,
1410
- group: "general"
1411
- }),
1412
- defineField({
1413
- name: "localePrompts",
1414
- title: "Locale Prompts",
1415
- description: "Prompts that will be given to the Sanity AI Agent for each locale.",
1416
- type: "internationalizedArrayText",
1417
- group: "general"
1418
- }),
1419
- defineField({
1420
- name: "protectedPhrases",
1421
- title: "Protected Phrases",
1422
- description: "Phrases that will never be translated.",
1423
- type: "array",
1424
- of: [{ type: "string" }],
1425
- group: "general"
1426
- }),
1427
- defineField({
1428
- name: "changeHistory",
1429
- title: "Change History",
1430
- description: "History of changes made via user feedback.",
1431
- type: "array",
1432
- readOnly: !0,
1433
- group: "history",
1434
- of: [
1435
- {
1436
- type: "object",
1437
- fields: [
1438
- defineField({
1439
- name: "timestamp",
1440
- title: "Timestamp",
1441
- type: "datetime"
1442
- }),
1443
- defineField({
1444
- name: "changeType",
1445
- title: "Change Type",
1446
- type: "string",
1447
- options: {
1448
- list: [
1449
- { title: "Global Prompt", value: "globalPrompt" },
1450
- { title: "Locale Prompt", value: "localePrompt" },
1451
- { title: "Protected Phrase", value: "protectedPhrase" },
1452
- { title: "Glossary Term", value: "glossaryTerm" }
1453
- ]
1454
- }
1455
- }),
1456
- defineField({
1457
- name: "description",
1458
- title: "Description",
1459
- type: "text",
1460
- rows: 3
1461
- }),
1462
- defineField({
1463
- name: "locale",
1464
- title: "Locale",
1465
- type: "string",
1466
- description: "Target locale for locale-specific changes"
1467
- })
1468
- ],
1469
- preview: {
1470
- select: {
1471
- timestamp: "timestamp",
1472
- changeType: "changeType",
1473
- description: "description"
1474
- },
1475
- prepare({ timestamp, changeType, description }) {
1476
- return {
1477
- title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
1478
- subtitle: description
1479
- };
1480
- }
1481
- }
1482
- }
1483
- ]
1484
- })
1485
- ],
1486
- preview: {
1487
- prepare: () => ({
1488
- title: "Translation Settings"
1489
- })
1490
- }
1491
- });
1236
+ };
1492
1237
  class Semaphore {
1493
1238
  permits;
1494
1239
  queue = [];
@@ -1664,11 +1409,21 @@ const fetchTranslationPrompts = async ({
1664
1409
  const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
1665
1410
  `*[_id == "translation-settings"][0] {
1666
1411
  globalPrompt,
1667
- localePrompts[locale in $targetLanguages],
1412
+ localePrompts[_key in $targetLanguages],
1668
1413
  protectedPhrases
1669
1414
  }`,
1670
1415
  { targetLanguages }
1671
- ), translationGlossaryTerms = await client.fetch(
1416
+ );
1417
+ devLog.debug("Fetched translation settings", {
1418
+ hasSettings: !!translationSettings2,
1419
+ hasGlobalPrompt: !!translationSettings2?.globalPrompt,
1420
+ globalPromptLength: translationSettings2?.globalPrompt?.length,
1421
+ localePromptsCount: translationSettings2?.localePrompts?.length,
1422
+ localePrompts: translationSettings2?.localePrompts,
1423
+ protectedPhrasesCount: translationSettings2?.protectedPhrases?.length,
1424
+ targetLanguages
1425
+ });
1426
+ const translationGlossaryTerms = await client.fetch(
1672
1427
  `*[
1673
1428
  _type == "translationGlossaryTerm"
1674
1429
  && count(translations[_key in $targetLanguages]) > 0
@@ -1688,9 +1443,21 @@ const fetchTranslationPrompts = async ({
1688
1443
  )), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
1689
1444
  }
1690
1445
  let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
1691
- return sourceDocument && filteredProtectedPhrases && (filteredProtectedPhrases = filteredProtectedPhrases.filter(
1692
- (phrase) => protectedPhraseAppearsInDocument(phrase, sourceDocumentString)
1693
- )), {
1446
+ return sourceDocument && filteredProtectedPhrases && (devLog.debug("Filtering protected phrases", {
1447
+ totalPhrases: filteredProtectedPhrases.length,
1448
+ phrases: filteredProtectedPhrases,
1449
+ sourceDocumentStringLength: sourceDocumentString.length,
1450
+ sourceDocumentPreview: sourceDocumentString.substring(0, 500)
1451
+ }), filteredProtectedPhrases = filteredProtectedPhrases.filter((phrase) => {
1452
+ const appears = protectedPhraseAppearsInDocument(phrase, sourceDocumentString);
1453
+ return devLog.debug("Protected phrase match result", {
1454
+ phrase,
1455
+ appears
1456
+ }), appears;
1457
+ }), devLog.debug("Filtered protected phrases result", {
1458
+ remainingCount: filteredProtectedPhrases.length,
1459
+ remainingPhrases: filteredProtectedPhrases
1460
+ })), {
1694
1461
  globalPrompt: translationSettings2?.globalPrompt,
1695
1462
  protectedPhrases: filteredProtectedPhrases,
1696
1463
  localeInstructions: Object.fromEntries(
@@ -1755,7 +1522,12 @@ const fetchTranslationPrompts = async ({
1755
1522
  }
1756
1523
  };
1757
1524
  return recursiveSearch(obj, []), results;
1758
- }, localizeReferences = async ({ client, documentId, languageFieldName }) => {
1525
+ }, localizeReferences = async ({
1526
+ client,
1527
+ documentId,
1528
+ sourceLanguage,
1529
+ languageFieldName
1530
+ }) => {
1759
1531
  try {
1760
1532
  const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
1761
1533
  if (!doc) {
@@ -1777,25 +1549,26 @@ const fetchTranslationPrompts = async ({
1777
1549
  const refs = findKeyValues(doc, "_ref");
1778
1550
  if (!refs?.length)
1779
1551
  return;
1780
- await updateReferences(documentId, client, refs, targetLanguage);
1552
+ await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
1781
1553
  } catch (error) {
1782
1554
  devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
1783
1555
  documentId,
1784
1556
  component: "localizeReferences"
1785
1557
  });
1786
1558
  }
1787
- }, updateReferences = async (documentId, client, refs, targetLanguage) => {
1559
+ }, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
1788
1560
  if (!refs?.length)
1789
1561
  return [];
1790
- const refIds = refs.map((r) => undraftId(r.value)), translations = await client.fetch(
1791
- `*[_type == $translationMetadataSchemaType && count(translations[value._ref in $refIds]) > 0]
1792
- .translations[value._ref in $refIds]{
1793
- "sourceId": value._ref,
1794
- "targetId": ^.translations[_key == $targetLanguage][0].value._ref
1795
- }[defined(targetId)]`,
1562
+ const refIds = refs.map((r) => getPublishedId(r.value)), translations = await client.fetch(
1563
+ `*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
1564
+ "sourceId": translations[_key == $sourceLanguage][0].value._ref,
1565
+ "targetId": translations[_key == $targetLanguage][0].value._ref,
1566
+ "targetType": translations[_key == $sourceLanguage][0].value -> _type
1567
+ }`,
1796
1568
  {
1797
1569
  translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1798
1570
  refIds,
1571
+ sourceLanguage,
1799
1572
  targetLanguage
1800
1573
  }
1801
1574
  ), translationMap = /* @__PURE__ */ new Map();
@@ -1804,11 +1577,10 @@ const fetchTranslationPrompts = async ({
1804
1577
  const patch = client.patch(documentId);
1805
1578
  let hasChanges = !1;
1806
1579
  for (const ref of refs) {
1807
- const sourceId = undraftId(ref.value), newId = translationMap.get(sourceId);
1808
- newId && (patch.set({ [pathToString(ref.path)]: undraftId(newId) }), patch.set({ [`${pathToString(ref.path.slice(0, -1))}._weak`]: !0 }), patch.set({
1580
+ const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
1581
+ newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
1809
1582
  [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1810
- _type: "reference",
1811
- _weak: !1
1583
+ type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
1812
1584
  }
1813
1585
  }), hasChanges = !0);
1814
1586
  }
@@ -1833,7 +1605,111 @@ const fetchTranslationPrompts = async ({
1833
1605
  payload
1834
1606
  });
1835
1607
  }
1836
- }, getTranslationMetadata = async ({
1608
+ }, DEFAULT_OPTIONS = {
1609
+ maxRetries: 3,
1610
+ initialDelayMs: 1e3,
1611
+ maxDelayMs: 3e4,
1612
+ backoffMultiplier: 2,
1613
+ jitter: !0
1614
+ };
1615
+ function isRetryableError(error) {
1616
+ if (error instanceof Error) {
1617
+ const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
1618
+ "schema not found",
1619
+ "invalid schema",
1620
+ "not found",
1621
+ "unauthorized",
1622
+ "forbidden",
1623
+ "invalid api key",
1624
+ "missing api key",
1625
+ "validation failed",
1626
+ "invalid document",
1627
+ "source language not found",
1628
+ "invalid configuration",
1629
+ "type mismatch"
1630
+ ];
1631
+ for (const pattern of nonRetryablePatterns)
1632
+ if (message.includes(pattern) || name.includes(pattern))
1633
+ return !1;
1634
+ const retryablePatterns = [
1635
+ "failed to fetch",
1636
+ "network",
1637
+ "timeout",
1638
+ "econnreset",
1639
+ "econnrefused",
1640
+ "enotfound",
1641
+ "socket",
1642
+ "rate limit",
1643
+ "too many requests",
1644
+ "429",
1645
+ "500",
1646
+ "502",
1647
+ "503",
1648
+ "504",
1649
+ "internal server error",
1650
+ "bad gateway",
1651
+ "service unavailable",
1652
+ "gateway timeout",
1653
+ "temporarily unavailable",
1654
+ "try again"
1655
+ ];
1656
+ for (const pattern of retryablePatterns)
1657
+ if (message.includes(pattern) || name.includes(pattern))
1658
+ return !0;
1659
+ }
1660
+ if (typeof error == "object" && error !== null) {
1661
+ const statusCode = error.statusCode || error.status;
1662
+ if (statusCode) {
1663
+ if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
1664
+ return !0;
1665
+ if (statusCode >= 400 && statusCode < 500)
1666
+ return !1;
1667
+ }
1668
+ }
1669
+ return !0;
1670
+ }
1671
+ function calculateBackoffDelay(attempt, options) {
1672
+ const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
1673
+ let delay = initialDelayMs * backoffMultiplier ** attempt;
1674
+ if (delay = Math.min(delay, maxDelayMs), jitter) {
1675
+ const jitterFactor = 0.75 + Math.random() * 0.5;
1676
+ delay = Math.round(delay * jitterFactor);
1677
+ }
1678
+ return delay;
1679
+ }
1680
+ function sleep(ms) {
1681
+ return new Promise((resolve) => setTimeout(resolve, ms));
1682
+ }
1683
+ async function withRetry(fn, options = {}) {
1684
+ const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
1685
+ let lastError, attempts = 0;
1686
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
1687
+ attempts = attempt + 1;
1688
+ try {
1689
+ return { success: !0, result: await fn(), attempts };
1690
+ } catch (error) {
1691
+ if (lastError = error, attempt >= opts.maxRetries) {
1692
+ devLog.debug("Max retries exhausted", { attempt: attempts, error });
1693
+ break;
1694
+ }
1695
+ if (!shouldRetry(error)) {
1696
+ devLog.debug("Error is not retryable", { attempt: attempts, error });
1697
+ break;
1698
+ }
1699
+ const delay = calculateBackoffDelay(attempt, opts);
1700
+ devLog.debug("Scheduling retry", {
1701
+ attempt: attempts,
1702
+ nextAttempt: attempt + 2,
1703
+ delayMs: delay,
1704
+ error: error instanceof Error ? error.message : String(error)
1705
+ }), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
1706
+ }
1707
+ }
1708
+ return { success: !1, error: lastError, attempts };
1709
+ }
1710
+ const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1711
+ (translation) => translation._key === targetLanguage
1712
+ )?.value._ref || null, getTranslationMetadata = async ({
1837
1713
  client,
1838
1714
  sourceDocument,
1839
1715
  sourceLanguage
@@ -1882,7 +1758,7 @@ const fetchTranslationPrompts = async ({
1882
1758
  _type: "internationalizedArrayReferenceValue",
1883
1759
  value: {
1884
1760
  _type: "reference",
1885
- _ref: undraftId(newDocument._id),
1761
+ _ref: getPublishedId(newDocument._id),
1886
1762
  _weak: !0,
1887
1763
  _strengthenOnPublish: {
1888
1764
  _type: "reference",
@@ -1892,7 +1768,7 @@ const fetchTranslationPrompts = async ({
1892
1768
  }, newTranslationHistory = {
1893
1769
  _key: targetLanguage,
1894
1770
  language: targetLanguage,
1895
- sourceId: undraftId(sourceDocument._id),
1771
+ sourceId: getPublishedId(sourceDocument._id),
1896
1772
  sourceRev: sourceDocument._rev,
1897
1773
  sourceLanguage,
1898
1774
  sourceSnapshot: JSON.stringify(sourceDocument),
@@ -1922,69 +1798,377 @@ const fetchTranslationPrompts = async ({
1922
1798
  status
1923
1799
  })
1924
1800
  });
1925
- }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1926
- (translation) => translation._key === targetLanguage
1927
- )?.value._ref || null, determineTranslationTarget = ({
1928
- translationMetadata,
1929
- targetLanguage
1930
- }) => {
1931
- const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
1932
- return existingTranslationId ? {
1933
- // Update existing translation
1934
- operation: "createOrReplace",
1935
- _id: existingTranslationId
1936
- } : {
1937
- // Create new translation
1938
- operation: "create"
1939
- };
1940
- }, analyzeDocumentContent = (document) => {
1941
- let hasCodeBlocks = !1, hasInlineCode = !1, codeBlockCount = 0, inlineCodeCount = 0;
1942
- const hasMarkdown = !1;
1943
- let hasPortableText = !1, estimatedFieldCount = 0;
1944
- const traverse = (value, depth = 0) => {
1945
- value != null && (depth > 0 && typeof value == "object" && estimatedFieldCount++, Array.isArray(value) ? value.forEach((item) => {
1946
- item && typeof item == "object" && "_type" in item && item._type === "code" && (hasCodeBlocks = !0, codeBlockCount++, hasPortableText = !0), item && typeof item == "object" && "_type" in item && item._type === "block" && "children" in item && Array.isArray(item.children) && (hasPortableText = !0, item.children.forEach((child) => {
1947
- child && typeof child == "object" && "marks" in child && Array.isArray(child.marks) && child.marks.includes("code") && (hasInlineCode = !0, inlineCodeCount++);
1948
- })), traverse(item, depth + 1);
1949
- }) : typeof value == "object" && Object.values(value).forEach((val) => traverse(val, depth + 1)));
1801
+ };
1802
+ function shouldExcludeField(fieldPath, excludeFields) {
1803
+ for (const pattern of excludeFields) {
1804
+ if (fieldPath === pattern)
1805
+ return !0;
1806
+ if (pattern.startsWith("**.")) {
1807
+ const fieldName = pattern.substring(3), segments = fieldPath.split(".");
1808
+ if (segments[segments.length - 1]?.split("[")[0] === fieldName)
1809
+ return !0;
1810
+ } else if (!pattern.includes(".") && !pattern.includes("[")) {
1811
+ const segments = fieldPath.split(".");
1812
+ if (segments[segments.length - 1]?.split("[")[0] === pattern)
1813
+ return !0;
1814
+ } else if (pattern.includes("[*]")) {
1815
+ const regexPattern = pattern.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\\\[\*\\\]/g, "\\[\\d+\\]");
1816
+ if (new RegExp(`^${regexPattern}$`).test(fieldPath))
1817
+ return !0;
1818
+ }
1819
+ }
1820
+ return !1;
1821
+ }
1822
+ function filterExcludedFields(document, excludeFields = [], currentPath = "") {
1823
+ if (!excludeFields.length)
1824
+ return {
1825
+ filteredDocument: document,
1826
+ excludedValues: {}
1827
+ };
1828
+ const filteredDocument = {}, excludedValues = {};
1829
+ let hasExclusions = !1;
1830
+ for (const [key, value] of Object.entries(document)) {
1831
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
1832
+ if (shouldExcludeField(fieldPath, excludeFields)) {
1833
+ excludedValues[fieldPath] = value, hasExclusions = !0, devLog.debug("Excluding field from translation", {
1834
+ fieldPath,
1835
+ fieldType: typeof value,
1836
+ value: typeof value == "string" ? value : "[object]"
1837
+ });
1838
+ continue;
1839
+ }
1840
+ if (value && typeof value == "object" && !Array.isArray(value) && value._type !== "reference") {
1841
+ const nested = filterExcludedFields(value, excludeFields, fieldPath);
1842
+ filteredDocument[key] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (hasExclusions = !0);
1843
+ } else if (Array.isArray(value)) {
1844
+ const filteredArray = [];
1845
+ let arrayHasExclusions = !1;
1846
+ for (let i = 0; i < value.length; i++) {
1847
+ const arrayItem = value[i], itemPath = `${fieldPath}[${i}]`;
1848
+ if (arrayItem && typeof arrayItem == "object" && !Array.isArray(arrayItem) && arrayItem._type !== "reference") {
1849
+ const nested = filterExcludedFields(arrayItem, excludeFields, itemPath);
1850
+ filteredArray[i] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (arrayHasExclusions = !0);
1851
+ } else
1852
+ filteredArray[i] = arrayItem;
1853
+ }
1854
+ filteredDocument[key] = filteredArray, arrayHasExclusions && (hasExclusions = !0);
1855
+ } else
1856
+ filteredDocument[key] = value;
1857
+ }
1858
+ return hasExclusions && devLog.info("Filtered excluded fields from document", {
1859
+ documentId: document._id || "unknown",
1860
+ excludedFieldCount: Object.keys(excludedValues).length,
1861
+ excludedFields: Object.keys(excludedValues)
1862
+ }), {
1863
+ filteredDocument,
1864
+ excludedValues
1950
1865
  };
1951
- traverse(document);
1952
- let complexity = "simple";
1953
- return estimatedFieldCount > 50 ? complexity = "complex" : estimatedFieldCount > 20 && (complexity = "moderate"), {
1954
- hasCodeBlocks,
1955
- hasInlineCode,
1956
- codeBlockCount,
1957
- inlineCodeCount,
1958
- hasMarkdown,
1959
- hasPortableText,
1960
- complexity,
1961
- estimatedFieldCount
1866
+ }
1867
+ function restoreExcludedFields(translatedDocument, excludedValues) {
1868
+ if (!Object.keys(excludedValues).length)
1869
+ return translatedDocument;
1870
+ const result = { ...translatedDocument };
1871
+ for (const [fieldPath, value] of Object.entries(excludedValues))
1872
+ setNestedValue(result, fieldPath, value);
1873
+ return devLog.debug("Restored excluded fields to translated document", {
1874
+ documentId: result._id || "unknown",
1875
+ restoredFieldCount: Object.keys(excludedValues).length,
1876
+ restoredFields: Object.keys(excludedValues)
1877
+ }), result;
1878
+ }
1879
+ function setNestedValue(obj, path, value) {
1880
+ const segments = path.split(".");
1881
+ let current = obj;
1882
+ for (let i = 0; i < segments.length - 1; i++) {
1883
+ const segment = segments[i], arrayMatch2 = segment.match(/^(.+)\[(\d+)\]$/);
1884
+ if (arrayMatch2) {
1885
+ const [, fieldName, indexStr] = arrayMatch2, index = Number.parseInt(indexStr, 10);
1886
+ for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
1887
+ current[fieldName].push({});
1888
+ current = current[fieldName][index];
1889
+ } else
1890
+ (!(segment in current) || typeof current[segment] != "object") && (current[segment] = {}), current = current[segment];
1891
+ }
1892
+ const finalSegment = segments[segments.length - 1], arrayMatch = finalSegment.match(/^(.+)\[(\d+)\]$/);
1893
+ if (arrayMatch) {
1894
+ const [, fieldName, indexStr] = arrayMatch, index = Number.parseInt(indexStr, 10);
1895
+ for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
1896
+ current[fieldName].push(null);
1897
+ current[fieldName][index] = value;
1898
+ } else
1899
+ current[finalSegment] = value;
1900
+ }
1901
+ const COMMON_ENUM_FIELDS = [
1902
+ "status",
1903
+ "state",
1904
+ "type",
1905
+ "category",
1906
+ "priority",
1907
+ "visibility",
1908
+ "role",
1909
+ "permission",
1910
+ "level",
1911
+ "mode",
1912
+ "format",
1913
+ "variant",
1914
+ "size",
1915
+ "theme",
1916
+ "kind",
1917
+ "phase",
1918
+ "stage"
1919
+ ], PREDEFINED_EXCLUSIONS = {
1920
+ /** Common enum and status fields */
1921
+ enums: [...COMMON_ENUM_FIELDS],
1922
+ /** All metadata and system fields (starting with _) */
1923
+ metadata: ["_id", "_type", "_rev", "_createdAt", "_updatedAt"],
1924
+ /** Common technical and configuration fields */
1925
+ technical: [
1926
+ "slug",
1927
+ "handle",
1928
+ "key",
1929
+ "code",
1930
+ "id",
1931
+ "uuid",
1932
+ "token",
1933
+ "hash",
1934
+ "checksum",
1935
+ "version",
1936
+ "config",
1937
+ "settings"
1938
+ ],
1939
+ /** SEO and URL-related fields that often shouldn't be translated */
1940
+ seo: ["slug", "handle", "permalink", "canonical", "redirect", "robots", "sitemap"]
1941
+ };
1942
+ function applyTransforms({
1943
+ document,
1944
+ registry,
1945
+ schemaType,
1946
+ parentPath = ""
1947
+ }) {
1948
+ if (!document)
1949
+ return { transformedDocument: document, originalStructure: {} };
1950
+ if (typeof document != "object")
1951
+ return { transformedDocument: document, originalStructure: {} };
1952
+ if (Array.isArray(document)) {
1953
+ const originalStructure2 = {};
1954
+ return { transformedDocument: document.map((item, index) => {
1955
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
1956
+ document: item,
1957
+ registry,
1958
+ schemaType,
1959
+ parentPath: itemPath
1960
+ });
1961
+ return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
1962
+ }), originalStructure: originalStructure2 };
1963
+ }
1964
+ const transformedObj = {}, originalStructure = {};
1965
+ for (const [key, value] of Object.entries(document)) {
1966
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
1967
+ if (key.startsWith("_") && key !== "_type") {
1968
+ transformedObj[key] = value;
1969
+ continue;
1970
+ }
1971
+ const transform = registry.findTransform(schemaType, fieldPath, value, document);
1972
+ if (transform) {
1973
+ devLog.debug("Applying transform", { fieldPath, schemaType });
1974
+ const context = {
1975
+ fieldPath,
1976
+ schemaType,
1977
+ document
1978
+ }, transformedValue = transform.toTranslatable(value, context);
1979
+ transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
1980
+ value,
1981
+ transform
1982
+ };
1983
+ } else {
1984
+ const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
1985
+ document: value,
1986
+ registry,
1987
+ schemaType,
1988
+ parentPath: fieldPath
1989
+ });
1990
+ Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
1991
+ }
1992
+ }
1993
+ return { transformedDocument: transformedObj, originalStructure };
1994
+ }
1995
+ function restoreTransforms({
1996
+ translatedDocument,
1997
+ originalStructure,
1998
+ registry,
1999
+ schemaType,
2000
+ parentPath = ""
2001
+ }) {
2002
+ if (translatedDocument == null || typeof translatedDocument != "object")
2003
+ return translatedDocument;
2004
+ if (Array.isArray(translatedDocument))
2005
+ return translatedDocument.map((item, index) => {
2006
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
2007
+ return restoreTransforms({
2008
+ translatedDocument: item,
2009
+ originalStructure,
2010
+ registry,
2011
+ schemaType,
2012
+ parentPath: itemPath
2013
+ });
2014
+ });
2015
+ const originalData = originalStructure[parentPath];
2016
+ if (originalData) {
2017
+ const { value: originalValue, transform } = originalData, context = {
2018
+ fieldPath: parentPath,
2019
+ schemaType,
2020
+ document: translatedDocument
2021
+ };
2022
+ return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
2023
+ }
2024
+ const restoredObj = {}, translatedObj = translatedDocument;
2025
+ for (const [key, value] of Object.entries(translatedObj)) {
2026
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
2027
+ if (fieldOriginalData) {
2028
+ const { value: originalValue, transform } = fieldOriginalData, context = {
2029
+ fieldPath,
2030
+ schemaType,
2031
+ document: translatedDocument
2032
+ };
2033
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
2034
+ } else
2035
+ restoredObj[key] = restoreTransforms({
2036
+ translatedDocument: value,
2037
+ originalStructure,
2038
+ registry,
2039
+ schemaType,
2040
+ parentPath: fieldPath
2041
+ });
2042
+ }
2043
+ return restoredObj;
2044
+ }
2045
+ class TransformRegistry {
2046
+ transforms = [];
2047
+ /**
2048
+ * Register a transform with the registry.
2049
+ * @param transform - Field transform to register
2050
+ */
2051
+ register(transform) {
2052
+ devLog.debug("Registering field transform", {
2053
+ matcher: transform.matcher
2054
+ }), this.transforms.push(transform);
2055
+ }
2056
+ /**
2057
+ * Find a matching transform for the given field.
2058
+ * If multiple transforms match, returns the first one (order matters!).
2059
+ * @param schemaType - Document schema type
2060
+ * @param fieldPath - Path to the field
2061
+ * @param value - Optional field value for value-based matching
2062
+ * @param document - Optional full document for context
2063
+ * @returns Matching transform or undefined if none found
2064
+ */
2065
+ findTransform(schemaType, fieldPath, value, document) {
2066
+ const matches = [];
2067
+ for (const transform of this.transforms) {
2068
+ const { matcher } = transform;
2069
+ if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
2070
+ if (matcher.fieldPath) {
2071
+ if (typeof matcher.fieldPath == "string") {
2072
+ if (matcher.fieldPath !== fieldPath)
2073
+ continue;
2074
+ } else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
2075
+ continue;
2076
+ }
2077
+ if (matcher.fieldName) {
2078
+ const segments = fieldPath.split(".");
2079
+ if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
2080
+ continue;
2081
+ }
2082
+ if (matcher.valueMatcher) {
2083
+ if (value === void 0)
2084
+ continue;
2085
+ const context = {
2086
+ fieldPath,
2087
+ schemaType,
2088
+ document: document || {}
2089
+ };
2090
+ if (!matcher.valueMatcher(value, context))
2091
+ continue;
2092
+ }
2093
+ matches.push(transform);
2094
+ }
2095
+ }
2096
+ return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
2097
+ schemaType,
2098
+ fieldPath,
2099
+ matchCount: matches.length,
2100
+ firstMatcher: matches[0].matcher
2101
+ }), matches[0];
2102
+ }
2103
+ }
2104
+ 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) => {
2105
+ const model = request.model || "gpt-5", requestBody = {
2106
+ model: model === "gpt-5" ? "gpt-5.1" : model,
2107
+ // Map gpt-5 to gpt-5.1, use others as-is
2108
+ input: request.messages
2109
+ // Uses 'input' instead of 'messages'
1962
2110
  };
1963
- }, assembleLanguageSpecs = ({
1964
- sourceDocument,
1965
- languageFieldName,
1966
- targetLanguage
1967
- }) => {
1968
- const sourceLanugageId = sourceDocument[languageFieldName];
1969
- if (!sourceLanugageId)
1970
- throw new Error("[assembleLanguages] Source language ID is missing");
1971
- return {
1972
- fromLanguage: {
1973
- id: sourceLanugageId,
1974
- title: sourceLanugageId
2111
+ request.reasoningEffort && (requestBody.reasoning = {
2112
+ effort: request.reasoningEffort,
2113
+ summary: "auto"
2114
+ // Enable automatic reasoning summaries
2115
+ }), devLog.debug("OpenAI GPT-5 Request", {
2116
+ model: requestBody.model,
2117
+ reasoningEffort: request.reasoningEffort,
2118
+ messages: request.messages.map((m) => ({
2119
+ role: m.role,
2120
+ contentLength: m.content.length,
2121
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
2122
+ }))
2123
+ }), devLog.debug("OpenAI GPT-5 Full Prompt", {
2124
+ messages: request.messages
2125
+ });
2126
+ const response = await fetch("https://api.openai.com/v1/responses", {
2127
+ method: "POST",
2128
+ headers: {
2129
+ "Content-Type": "application/json",
2130
+ Authorization: `Bearer ${apiKey}`
1975
2131
  },
1976
- toLanguage: {
1977
- id: targetLanguage,
1978
- title: targetLanguage
1979
- }
2132
+ body: JSON.stringify(requestBody)
2133
+ });
2134
+ if (!response.ok) {
2135
+ const error = await response.json();
2136
+ throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
2137
+ }
2138
+ const result = await response.json(), tokensUsed = {
2139
+ prompt: result.usage?.input_tokens || 0,
2140
+ completion: result.usage?.output_tokens || 0,
2141
+ reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
2142
+ total: result.usage?.total_tokens || 0,
2143
+ cached: result.usage?.input_tokens_details?.cached_tokens || 0
2144
+ }, cost = calculateCost(tokensUsed, model);
2145
+ let content = "";
2146
+ if (result.output && Array.isArray(result.output)) {
2147
+ const messageItem = result.output.find((item) => item.type === "message");
2148
+ messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
2149
+ }
2150
+ return content || (content = result.output_text || result.text || ""), {
2151
+ content,
2152
+ tokensUsed,
2153
+ cost
1980
2154
  };
1981
- }, callGPT5 = async (apiKey, request) => {
1982
- const model = request.model || "gpt-5", isGPT5 = model === "gpt-5", useStreaming = !!request.onReasoningChunk, requestBody = {
2155
+ }, callChatCompletionsAPI = async (apiKey, request) => {
2156
+ const model = request.model || "gpt-4o", requestBody = {
1983
2157
  model,
1984
2158
  messages: request.messages,
1985
- stream: useStreaming
2159
+ temperature: request.temperature ?? 0.3
1986
2160
  };
1987
- isGPT5 || (requestBody.temperature = request.temperature ?? 0.3), isGPT5 && (requestBody.reasoning_effort = request.reasoningEffort || "medium", requestBody.verbosity = request.verbosity || "medium");
2161
+ devLog.debug("OpenAI Chat Completions Request", {
2162
+ model,
2163
+ temperature: request.temperature ?? 0.3,
2164
+ messages: request.messages.map((m) => ({
2165
+ role: m.role,
2166
+ contentLength: m.content.length,
2167
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
2168
+ }))
2169
+ }), devLog.debug("OpenAI Chat Completions Full Prompt", {
2170
+ messages: request.messages
2171
+ });
1988
2172
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
1989
2173
  method: "POST",
1990
2174
  headers: {
@@ -1995,110 +2179,116 @@ const fetchTranslationPrompts = async ({
1995
2179
  });
1996
2180
  if (!response.ok) {
1997
2181
  const error = await response.json();
1998
- throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
2182
+ throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
1999
2183
  }
2000
- if (useStreaming && response.body && request.onReasoningChunk)
2001
- return await handleStreamingResponse(response, request.onReasoningChunk, model);
2002
2184
  const result = await response.json(), usage = result.usage || {}, tokensUsed = {
2003
2185
  prompt: usage.prompt_tokens || 0,
2004
2186
  completion: usage.completion_tokens || 0,
2005
2187
  reasoning: usage.reasoning_tokens || 0,
2006
- // New in GPT-5
2007
- total: usage.total_tokens || 0
2008
- }, cost = calculateGPT5Cost(tokensUsed, request.model || "gpt-5"), messageContent = result.choices[0].message.content;
2009
- let reasoningSummary, actualContent = messageContent;
2010
- const reasoningMatch = messageContent.match(/^<reasoning>([\s\S]*?)<\/reasoning>\n\n([\s\S]*)$/i);
2011
- return reasoningMatch && (reasoningSummary = reasoningMatch[1].trim(), actualContent = reasoningMatch[2]), {
2012
- content: actualContent,
2013
- tokensUsed,
2014
- cost,
2015
- reasoningSummary
2016
- };
2017
- }, handleStreamingResponse = async (response, onReasoningChunk, model) => {
2018
- if (!response.body)
2019
- throw new Error("Response body is null");
2020
- const reader = response.body.getReader(), decoder = new TextDecoder();
2021
- let fullContent = "", inReasoning = !1, reasoningBuffer = "", contentBuffer = "", tokensUsed = { prompt: 0, completion: 0, reasoning: 0, total: 0 };
2022
- try {
2023
- for (; ; ) {
2024
- const { done, value } = await reader.read();
2025
- if (done) break;
2026
- const lines = decoder.decode(value, { stream: !0 }).split(`
2027
- `).filter((line) => line.trim() !== "");
2028
- for (const line of lines)
2029
- if (line.startsWith("data: ")) {
2030
- const data = line.slice(6);
2031
- if (data === "[DONE]") continue;
2032
- try {
2033
- const parsed = JSON.parse(data);
2034
- parsed.usage && (tokensUsed = {
2035
- prompt: parsed.usage.prompt_tokens || 0,
2036
- completion: parsed.usage.completion_tokens || 0,
2037
- reasoning: parsed.usage.reasoning_tokens || 0,
2038
- total: parsed.usage.total_tokens || 0
2039
- });
2040
- const delta = parsed.choices?.[0]?.delta?.content;
2041
- if (delta)
2042
- if (fullContent += delta, delta.includes("<reasoning>")) {
2043
- inReasoning = !0;
2044
- const parts = delta.split("<reasoning>");
2045
- reasoningBuffer += parts[1] || "";
2046
- } else if (delta.includes("</reasoning>")) {
2047
- inReasoning = !1;
2048
- const parts = delta.split("</reasoning>");
2049
- reasoningBuffer += parts[0] || "", contentBuffer += parts[1] || "";
2050
- } else inReasoning ? (reasoningBuffer += delta, onReasoningChunk(reasoningBuffer)) : contentBuffer += delta;
2051
- } catch {
2052
- }
2053
- }
2054
- }
2055
- } finally {
2056
- reader.releaseLock();
2057
- }
2058
- const cost = calculateGPT5Cost(tokensUsed, model);
2188
+ total: usage.total_tokens || 0,
2189
+ cached: usage.prompt_tokens_details?.cached_tokens || 0
2190
+ }, cost = calculateCost(tokensUsed, model);
2059
2191
  return {
2060
- content: contentBuffer || fullContent,
2192
+ content: result.choices[0].message.content || "",
2061
2193
  tokensUsed,
2062
- cost,
2063
- reasoningSummary: reasoningBuffer || void 0
2194
+ cost
2064
2195
  };
2065
- }, calculateGPT5Cost = (tokens, model) => {
2196
+ }, calculateCost = (tokens, model) => {
2066
2197
  const pricing = {
2067
2198
  "gpt-5": {
2068
- input: 2e-6,
2069
- // $0.002 per 1k input tokens
2199
+ input: 125e-8,
2070
2200
  output: 1e-5,
2071
- // $0.010 per 1k output tokens
2072
- reasoning: 5e-6
2073
- // $0.005 per 1k reasoning tokens
2201
+ reasoning: 0
2202
+ },
2203
+ "gpt-5.1": {
2204
+ input: 125e-8,
2205
+ output: 1e-5,
2206
+ reasoning: 0
2207
+ },
2208
+ "gpt-5-mini": {
2209
+ input: 25e-8,
2210
+ output: 2e-6,
2211
+ reasoning: 0
2212
+ },
2213
+ "gpt-5-nano": {
2214
+ input: 50000000000000004e-24,
2215
+ output: 40000000000000003e-23,
2216
+ reasoning: 0
2217
+ },
2218
+ "gpt-5-pro": {
2219
+ input: 15e-6,
2220
+ output: 12e-5,
2221
+ reasoning: 0
2074
2222
  },
2075
2223
  "gpt-4o": {
2076
- input: 5e-6,
2077
- output: 14999999999999999e-21,
2224
+ input: 25e-7,
2225
+ output: 1e-5,
2078
2226
  reasoning: 0
2079
- // No reasoning tokens in GPT-4
2080
2227
  },
2081
2228
  "gpt-4-turbo": {
2082
2229
  input: 1e-5,
2083
- output: 29999999999999997e-21,
2230
+ output: 3e-5,
2084
2231
  reasoning: 0
2085
2232
  },
2086
2233
  "gpt-4": {
2087
- input: 29999999999999997e-21,
2088
- output: 59999999999999995e-21,
2234
+ input: 3e-5,
2235
+ output: 6e-5,
2089
2236
  reasoning: 0
2090
2237
  }
2091
- }, modelPricing = pricing[model] || pricing["gpt-5"];
2092
- return tokens.prompt * modelPricing.input + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
2238
+ }, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
2239
+ return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
2093
2240
  }, deserializeDocument = (originalDoc, reviewedText) => {
2094
2241
  const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
2095
2242
  return applyTextChanges(result, parsed), result;
2096
2243
  }, parseReviewFormat = (text) => {
2097
2244
  const root = [], stack = [], lines = text.split(`
2098
2245
  `);
2099
- for (const line of lines) {
2100
- const trimmed = line.trim();
2246
+ for (let i = 0; i < lines.length; i++) {
2247
+ const trimmed = lines[i].trim();
2101
2248
  if (!trimmed) continue;
2249
+ const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
2250
+ if (compactStringMatch && !trimmed.includes("<")) {
2251
+ const [, fieldName, text2] = compactStringMatch, node = {
2252
+ type: "field",
2253
+ name: fieldName,
2254
+ text: unescapeXml(text2),
2255
+ attributes: { type: "string" },
2256
+ children: []
2257
+ };
2258
+ if (stack.length > 0) {
2259
+ const parent = stack[stack.length - 1];
2260
+ parent && parent.children.push(node);
2261
+ } else
2262
+ root.push(node);
2263
+ continue;
2264
+ }
2265
+ const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
2266
+ if (compactStartMatch) {
2267
+ const fieldName = compactStartMatch[1], contentLines = [];
2268
+ for (i++; i < lines.length; ) {
2269
+ const currentLine = lines[i];
2270
+ if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
2271
+ break;
2272
+ contentLines.push(currentLine), i++;
2273
+ }
2274
+ const content = contentLines.join(`
2275
+ `);
2276
+ let contentType = "translatable.vtt.captions";
2277
+ (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
2278
+ const node = {
2279
+ type: "field",
2280
+ name: fieldName,
2281
+ text: content,
2282
+ attributes: { type: contentType },
2283
+ children: []
2284
+ };
2285
+ if (stack.length > 0) {
2286
+ const parent = stack[stack.length - 1];
2287
+ parent && parent.children.push(node);
2288
+ } else
2289
+ root.push(node);
2290
+ continue;
2291
+ }
2102
2292
  const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
2103
2293
  if (openMatch) {
2104
2294
  const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
@@ -2121,6 +2311,26 @@ const fetchTranslationPrompts = async ({
2121
2311
  parent && parent.children.push(node);
2122
2312
  } else
2123
2313
  root.push(node);
2314
+ else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
2315
+ const contentLines = [];
2316
+ for (i++; i < lines.length; ) {
2317
+ const currentLine = lines[i];
2318
+ if (currentLine.trim().match(/^<\/field>$/))
2319
+ break;
2320
+ contentLines.push(currentLine), i++;
2321
+ }
2322
+ if (node.text = contentLines.join(`
2323
+ `), stack.length > 0) {
2324
+ const parent = stack[stack.length - 1];
2325
+ parent && parent.children.push(node);
2326
+ } else
2327
+ root.push(node);
2328
+ } else if (attributes.type === "string" && content && content.includes(closingTag))
2329
+ if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
2330
+ const parent = stack[stack.length - 1];
2331
+ parent && parent.children.push(node);
2332
+ } else
2333
+ root.push(node);
2124
2334
  else
2125
2335
  stack.push(node);
2126
2336
  continue;
@@ -2149,6 +2359,16 @@ const fetchTranslationPrompts = async ({
2149
2359
  applyFieldChanges(doc, path, node);
2150
2360
  }
2151
2361
  }, applyFieldChanges = (doc, path, node) => {
2362
+ if (node.attributes.type === "translatable.vtt.captions") {
2363
+ const fieldValue = get(doc, path);
2364
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
2365
+ return;
2366
+ }
2367
+ if (node.attributes.type === "translatable.richtext") {
2368
+ const fieldValue = get(doc, path);
2369
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
2370
+ return;
2371
+ }
2152
2372
  if (node.attributes.type === "array") {
2153
2373
  node.children.forEach((child, idx) => {
2154
2374
  applyFieldChanges(doc, [...path, idx], child);
@@ -2167,7 +2387,10 @@ const fetchTranslationPrompts = async ({
2167
2387
  });
2168
2388
  return;
2169
2389
  }
2170
- node.text !== void 0 && !node.attributes.readonly && set(doc, path, node.text);
2390
+ 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", {
2391
+ path: path.join("."),
2392
+ attemptedValue: node.text
2393
+ }));
2171
2394
  }, applyBlockChanges = (block, blockNode) => {
2172
2395
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
2173
2396
  devLog.warn("Block key mismatch, skipping", {
@@ -2198,7 +2421,10 @@ const fetchTranslationPrompts = async ({
2198
2421
  });
2199
2422
  return;
2200
2423
  }
2201
- fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text);
2424
+ fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not present in original", {
2425
+ fieldName,
2426
+ attemptedValue: fieldNode.text
2427
+ }));
2202
2428
  }, applySpanChanges = (span, spanNode) => {
2203
2429
  if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
2204
2430
  devLog.warn("Span key mismatch, skipping", {
@@ -2215,7 +2441,7 @@ const fetchTranslationPrompts = async ({
2215
2441
  if (path.length === 0) {
2216
2442
  if (isPlainObject(value))
2217
2443
  for (const [key, val] of Object.entries(value))
2218
- !key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
2444
+ !key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
2219
2445
  return;
2220
2446
  }
2221
2447
  const fieldName = path[path.length - 1];
@@ -2228,6 +2454,14 @@ const fetchTranslationPrompts = async ({
2228
2454
  return;
2229
2455
  }
2230
2456
  if (isPlainObject(value)) {
2457
+ if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
2458
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2459
+ return;
2460
+ }
2461
+ if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
2462
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2463
+ return;
2464
+ }
2231
2465
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2232
2466
  for (const [key, val] of Object.entries(value))
2233
2467
  key.startsWith("_") || serialize(val, [...path, key], indent + 1);
@@ -2235,7 +2469,7 @@ const fetchTranslationPrompts = async ({
2235
2469
  return;
2236
2470
  }
2237
2471
  if (typeof value == "string") {
2238
- value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2472
+ value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
2239
2473
  return;
2240
2474
  }
2241
2475
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2272,7 +2506,7 @@ const fetchTranslationPrompts = async ({
2272
2506
  return;
2273
2507
  }
2274
2508
  if (typeof value == "string" && value.length > 0) {
2275
- lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2509
+ lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
2276
2510
  return;
2277
2511
  }
2278
2512
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2283,491 +2517,124 @@ const fetchTranslationPrompts = async ({
2283
2517
  span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
2284
2518
  const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
2285
2519
  metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
2286
- }, escapeXml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"), isPlainObject = (val) => typeof val == "object" && val !== null && !Array.isArray(val), validateStructureIntegrity = (original, modified) => {
2287
- const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
2288
- for (const field of criticalFields)
2289
- original[field] !== modified[field] && errors.push({
2290
- path: [field],
2291
- expected: original[field],
2292
- actual: modified[field],
2293
- type: "metadata_changed"
2294
- });
2295
- return validateStructureRecursive(original, modified, [], errors), {
2296
- valid: errors.length === 0,
2297
- errors
2298
- };
2299
- }, validateStructureRecursive = (original, modified, path, errors) => {
2300
- if (typeof original != typeof modified) {
2301
- errors.push({
2302
- path,
2303
- expected: typeof original,
2304
- actual: typeof modified,
2305
- type: "type_mismatch"
2306
- });
2307
- return;
2308
- }
2309
- if (Array.isArray(original)) {
2310
- if (!Array.isArray(modified)) {
2311
- errors.push({
2312
- path,
2313
- expected: "array",
2314
- actual: typeof modified,
2315
- type: "type_mismatch"
2316
- });
2317
- return;
2318
- }
2319
- if (original.length !== modified.length) {
2320
- errors.push({
2321
- path,
2322
- expected: `length ${original.length}`,
2323
- actual: `length ${modified.length}`,
2324
- type: "type_mismatch"
2325
- });
2326
- return;
2327
- }
2328
- original.forEach((item, idx) => {
2329
- validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
2330
- });
2331
- return;
2332
- }
2333
- if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
2334
- const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
2335
- for (const key of origKeys)
2336
- key in modified || errors.push({
2337
- path: [...path, key],
2338
- expected: "present",
2339
- actual: "missing",
2340
- type: "missing"
2341
- });
2342
- for (const key of modKeys)
2343
- key in original || errors.push({
2344
- path: [...path, key],
2345
- expected: "absent",
2346
- actual: "present",
2347
- type: "extra"
2348
- });
2349
- const origObj = original, modObj = modified;
2350
- for (const key of origKeys)
2351
- key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
2352
- path: [...path, key],
2353
- expected: origObj[key],
2354
- actual: modObj[key],
2355
- type: "metadata_changed"
2356
- }) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
2357
- }
2358
- }, assembleTranslationPrompt = (context, components) => {
2359
- const sections = [
2360
- {
2361
- id: "agentDirectives",
2362
- content: buildAgentDirectivesSection(context),
2363
- required: !0,
2364
- priority: 0
2365
- },
2366
- {
2367
- id: "criticalRules",
2368
- content: buildCriticalRulesSection(context),
2369
- required: !0,
2370
- priority: 1
2371
- }
2372
- ];
2373
- components.glossaryTerms?.trim() && sections.push({
2374
- id: "glossaryTerms",
2375
- content: buildGlossarySection(components.glossaryTerms),
2376
- required: !1,
2377
- priority: 3
2378
- });
2379
- const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2380
- styleSection && sections.push({
2381
- id: "styleGuide",
2382
- content: styleSection,
2383
- required: !1,
2384
- priority: 4
2385
- }), sections.sort((a, b) => a.priority - b.priority);
2386
- let styleGuide = "";
2387
- const omittedSections = [], truncatedSections = [];
2388
- for (const section of sections) {
2389
- const prefix = styleGuide.length > 0 ? `
2390
-
2391
- ---
2392
-
2393
- ` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
2394
- if (availableCharacters <= 0) {
2395
- if (section.required) {
2396
- const truncatedContent = truncateSection(section.content, 0);
2397
- truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
2398
- } else
2399
- omittedSections.push(section.id);
2400
- continue;
2401
- }
2402
- if (section.content.length <= availableCharacters) {
2403
- styleGuide += prefix + section.content;
2404
- continue;
2405
- }
2406
- if (section.required) {
2407
- const truncatedContent = truncateSection(section.content, availableCharacters);
2408
- styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
2409
- } else
2410
- omittedSections.push(section.id);
2411
- }
2412
- return {
2413
- styleGuide,
2414
- omittedSections,
2415
- truncatedSections
2416
- };
2417
- }, truncateSection = (content, limit) => {
2418
- if (limit <= 0)
2419
- return "";
2420
- if (content.length <= limit)
2421
- return content;
2422
- if (limit <= 1)
2423
- return content.slice(0, limit);
2424
- const ellipsis = "\u2026";
2425
- if (limit <= ellipsis.length)
2426
- return content.slice(0, limit);
2427
- const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
2428
- return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
2429
- }, buildAgentDirectivesSection = (context) => {
2430
- const { targetLanguage, sourceLanguage } = context;
2431
- return `# AGENT WORKFLOW DIRECTIVES
2432
-
2433
- - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2434
- - Scan each field once, then move directly into translation work.
2435
- - Plan updates before editing so document structure remains intact.
2436
- - Default to medium reasoning effort and escalate only for ambiguous passages.
2437
- - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
2438
- }, buildCriticalRulesSection = (context) => {
2439
- const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2440
- return `# CRITICAL TRANSLATION RULES
2441
-
2442
- Follow these without exception:
2443
-
2444
- 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2445
- 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2446
- 3. Maintain all layout, markdown, rich-text marks, and data structure.
2447
- 4. Never invent metadata or field values that are absent in the source.
2448
- 5. Leave technical tokens (code, IDs, filenames) untouched.
2449
- 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2450
- }, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2451
-
2452
- Apply these mappings immediately after protected phrases and before style preferences.
2453
-
2454
- ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2455
- const components = [];
2456
- return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2457
- "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2458
- ), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
2459
- `) : null;
2460
- }, translateEntireDocument = async ({
2461
- dispatch,
2462
- client,
2463
- schemaId,
2464
- sourceDocument,
2465
- targetLanguage,
2466
- languageFieldName,
2467
- translationTarget,
2468
- translationPrompts,
2469
- translationMode,
2470
- openaiConfig,
2471
- releaseId,
2472
- isNewTranslation
2473
- }) => {
2474
- const logContext = {
2475
- documentId: sourceDocument._id,
2476
- targetLanguage,
2477
- schemaId,
2478
- operation: translationTarget.operation,
2479
- targetId: translationTarget._id,
2480
- translationMode,
2481
- releaseId,
2482
- isNewTranslation
2483
- };
2484
- devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
2485
- try {
2486
- let versionedDocumentId, baseDocumentId;
2487
- if (isNewTranslation) {
2488
- const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
2489
- ...docWithoutMeta,
2490
- _type: sourceDocument._type,
2491
- [languageFieldName]: targetLanguage
2492
- };
2493
- devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
2494
- actionType: "sanity.action.document.version.create",
2495
- publishedId: baseDocumentId,
2496
- document: {
2497
- ...newTranslationDoc,
2498
- _type: sourceDocument._type,
2499
- _id: `versions.${releaseId}.${baseDocumentId}`
2500
- }
2501
- }), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
2502
- ...logContext,
2503
- versionedDocumentId,
2504
- baseDocumentId
2505
- });
2506
- } else {
2507
- const existingTranslationId = translationTarget._id;
2508
- if (!existingTranslationId)
2509
- throw new Error("Translation target ID not found for update operation");
2510
- devLog.info("Creating version of existing translation in release", {
2511
- ...logContext,
2512
- existingTranslationId
2513
- });
2514
- const existingDoc = await client.getDocument(existingTranslationId);
2515
- if (!existingDoc)
2516
- throw new Error(`Existing translation document not found: ${existingTranslationId}`);
2517
- await client.action({
2518
- actionType: "sanity.action.document.version.create",
2519
- publishedId: existingTranslationId,
2520
- document: {
2521
- ...existingDoc,
2522
- _id: `versions.${releaseId}.${existingTranslationId}`
2523
- }
2524
- }), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
2525
- ...logContext,
2526
- versionedDocumentId
2527
- });
2528
- const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
2529
- await client.createOrReplace({
2530
- ...sourceContent,
2531
- _id: versionedDocumentId,
2532
- [languageFieldName]: targetLanguage
2533
- }), devLog.info("Source content cloned onto version", {
2534
- ...logContext,
2535
- versionedDocumentId
2536
- });
2537
- }
2538
- let translatedDocument;
2539
- switch (translationMode) {
2540
- case "openai-only": {
2541
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2542
- throw new Error("OpenAI is not configured but openai-only mode was selected");
2543
- devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
2544
- const versionedDoc = await client.getDocument(versionedDocumentId);
2545
- if (!versionedDoc)
2546
- throw new Error(`Versioned document not found: ${versionedDocumentId}`);
2547
- const translatedDoc = await translateInPlaceWithOpenAI({
2548
- dispatch,
2549
- sourceDocument,
2550
- targetDocument: versionedDoc,
2551
- targetLanguage,
2552
- languageFieldName,
2553
- translationPrompts,
2554
- openaiConfig,
2555
- isReview: !1
2556
- });
2557
- translatedDoc && (translatedDocument = await client.createOrReplace({
2558
- ...translatedDoc,
2559
- _id: versionedDocumentId
2560
- }));
2561
- break;
2562
- }
2563
- case "sanity-with-review":
2564
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2565
- throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
2566
- if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
2567
- dispatch,
2568
- client,
2569
- schemaId,
2570
- sourceDocument,
2571
- targetLanguage,
2572
- languageFieldName,
2573
- versionedDocumentId,
2574
- translationPrompts
2575
- }), translatedDocument) {
2576
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
2577
- const reviewedDoc = await translateInPlaceWithOpenAI({
2578
- dispatch,
2579
- sourceDocument,
2580
- targetDocument: translatedDocument,
2581
- targetLanguage,
2582
- languageFieldName,
2583
- translationPrompts,
2584
- openaiConfig,
2585
- isReview: !0
2586
- });
2587
- reviewedDoc && (translatedDocument = await client.createOrReplace({
2588
- ...reviewedDoc,
2589
- _id: versionedDocumentId
2590
- }));
2591
- }
2592
- break;
2593
- default:
2594
- devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
2595
- dispatch,
2596
- client,
2597
- schemaId,
2598
- sourceDocument,
2599
- targetLanguage,
2600
- languageFieldName,
2601
- versionedDocumentId,
2602
- translationPrompts
2603
- });
2604
- break;
2605
- }
2606
- return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
2607
- ...translatedDocument,
2608
- _id: baseDocumentId
2609
- } : void 0;
2610
- } catch (error) {
2611
- const errorMessage = error instanceof Error ? error.message : String(error);
2612
- devLog.error(
2613
- "translateEntireDocument failed",
2614
- error instanceof Error ? error : new Error(String(error)),
2615
- logContext
2616
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2617
- errorMessage
2618
- }), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
2619
- return;
2620
- }
2621
- }, translateWithSanityAgent = async ({
2622
- dispatch,
2623
- client,
2624
- schemaId,
2625
- sourceDocument,
2626
- targetLanguage,
2627
- languageFieldName,
2628
- versionedDocumentId,
2629
- translationPrompts
2630
- }) => {
2631
- const logContext = {
2632
- documentId: sourceDocument._id,
2633
- targetLanguage,
2634
- versionedDocumentId
2635
- };
2636
- try {
2637
- const { toLanguage, fromLanguage } = assembleLanguageSpecs({
2638
- sourceDocument,
2639
- languageFieldName,
2640
- targetLanguage
2641
- });
2642
- devLog.debug("Language specs assembled", {
2643
- ...logContext,
2644
- toLanguage,
2645
- fromLanguage
2646
- });
2647
- const documentAnalysis = analyzeDocumentContent(sourceDocument);
2648
- devLog.debug("Document analysis completed", {
2649
- ...logContext,
2650
- analysis: documentAnalysis
2651
- }), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
2652
- const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
2653
- {
2654
- documentAnalysis,
2655
- isUpdate: !1,
2656
- // Always fresh translation in a version
2657
- targetLanguage,
2658
- sourceLanguage: sourceDocument[languageFieldName]
2659
- },
2660
- {
2661
- globalPrompt: translationPrompts.globalPrompt,
2662
- localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2663
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2664
- protectedPhrases: translationPrompts.protectedPhrases
2665
- }
2666
- );
2667
- devLog.info("Structured style guide generated", {
2668
- ...logContext,
2669
- styleGuideLength: styleGuide.length,
2670
- omittedSections,
2671
- truncatedSections
2672
- }), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
2673
- ...logContext,
2674
- omittedSections,
2675
- truncatedSections
2520
+ }, escapeXml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"), isPlainObject = (val) => typeof val == "object" && val !== null && !Array.isArray(val), validateStructureIntegrity = (original, modified) => {
2521
+ const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
2522
+ for (const field of criticalFields)
2523
+ original[field] !== modified[field] && errors.push({
2524
+ path: [field],
2525
+ expected: original[field],
2526
+ actual: modified[field],
2527
+ type: "metadata_changed"
2676
2528
  });
2677
- const newDocument = await client.agent.action.translate({
2678
- schemaId,
2679
- toLanguage,
2680
- fromLanguage,
2681
- documentId: versionedDocumentId,
2682
- // Use versioned document ID
2683
- languageFieldPath: [languageFieldName],
2684
- styleGuide,
2685
- protectedPhrases: translationPrompts.protectedPhrases || [],
2686
- conditionalPaths: {
2687
- defaultHidden: !1,
2688
- defaultReadOnly: !1
2689
- }
2529
+ return validateStructureRecursive(original, modified, [], errors), {
2530
+ valid: errors.length === 0,
2531
+ errors
2532
+ };
2533
+ }, validateStructureRecursive = (original, modified, path, errors) => {
2534
+ if (typeof original != typeof modified) {
2535
+ errors.push({
2536
+ path,
2537
+ expected: typeof original,
2538
+ actual: typeof modified,
2539
+ type: "type_mismatch"
2690
2540
  });
2691
- return devLog.info("Sanity agent translation completed", {
2692
- ...logContext,
2693
- newDocumentId: newDocument._id,
2694
- newDocumentRev: newDocument._rev
2695
- }), newDocument;
2696
- } catch (error) {
2697
- const errorMessage = error instanceof Error ? error.message : String(error);
2698
- devLog.error(
2699
- "Sanity agent translation failed",
2700
- error instanceof Error ? error : new Error(String(error)),
2701
- logContext
2702
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2703
- errorMessage
2541
+ return;
2542
+ }
2543
+ if (Array.isArray(original)) {
2544
+ if (!Array.isArray(modified)) {
2545
+ errors.push({
2546
+ path,
2547
+ expected: "array",
2548
+ actual: typeof modified,
2549
+ type: "type_mismatch"
2550
+ });
2551
+ return;
2552
+ }
2553
+ if (original.length !== modified.length) {
2554
+ errors.push({
2555
+ path,
2556
+ expected: `length ${original.length}`,
2557
+ actual: `length ${modified.length}`,
2558
+ type: "type_mismatch"
2559
+ });
2560
+ return;
2561
+ }
2562
+ original.forEach((item, idx) => {
2563
+ validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
2704
2564
  });
2705
2565
  return;
2706
2566
  }
2707
- }, translateInPlaceWithOpenAI = async ({
2567
+ if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
2568
+ const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
2569
+ for (const key of origKeys)
2570
+ key in modified || errors.push({
2571
+ path: [...path, key],
2572
+ expected: "present",
2573
+ actual: "missing",
2574
+ type: "missing"
2575
+ });
2576
+ for (const key of modKeys)
2577
+ key in original || errors.push({
2578
+ path: [...path, key],
2579
+ expected: "absent",
2580
+ actual: "present",
2581
+ type: "extra"
2582
+ });
2583
+ const origObj = original, modObj = modified;
2584
+ for (const key of origKeys)
2585
+ key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
2586
+ path: [...path, key],
2587
+ expected: origObj[key],
2588
+ actual: modObj[key],
2589
+ type: "metadata_changed"
2590
+ }) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
2591
+ }
2592
+ }, getOpenAITranslation = async ({
2708
2593
  dispatch,
2709
2594
  sourceDocument,
2710
- targetDocument,
2711
- targetLanguage,
2712
2595
  languageFieldName,
2713
2596
  translationPrompts,
2714
- openaiConfig,
2715
- isReview
2597
+ targetLanguage,
2598
+ openaiConfig
2716
2599
  }) => {
2717
2600
  const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2718
2601
  sourceId: sourceDocument._id,
2719
- targetId: targetDocument._id,
2720
- targetLanguage,
2721
- sourceLanguage,
2722
- isReview
2602
+ sourceLanguage
2723
2603
  };
2724
2604
  try {
2725
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2726
- devLog.debug("Documents serialized for OpenAI", {
2605
+ const sourceText = serializeDocument(sourceDocument);
2606
+ devLog.info("Serialized document for OpenAI", {
2727
2607
  ...logContext,
2728
- sourceLength: sourceText.length,
2729
- targetLength: targetText.length
2608
+ characterCount: sourceText.length,
2609
+ estimatedTokens: Math.ceil(sourceText.length / 4),
2610
+ preview: sourceText.substring(0, 500)
2730
2611
  });
2731
- const prompt = isReview ? buildReviewPrompt({
2732
- sourceText,
2733
- translatedText: targetText,
2734
- sourceLanguage,
2735
- targetLanguage,
2736
- translationPrompts
2737
- }) : buildTranslationPrompt({
2612
+ const prompt = buildTranslationPrompt$1({
2738
2613
  sourceText,
2739
2614
  sourceLanguage,
2740
2615
  targetLanguage,
2741
2616
  translationPrompts
2742
- });
2743
- devLog.debug("Calling GPT-5", {
2744
- ...logContext,
2745
- promptLength: prompt.length
2746
- });
2747
- const response = await callGPT5(openaiConfig.apiKey, {
2617
+ }), response = await callGPT5(openaiConfig.apiKey, {
2748
2618
  messages: [
2749
2619
  {
2750
- role: "system",
2751
- content: isReview ? "You are a translation quality reviewer. Review and refine translations while preserving structure." : "You are a professional translator. Translate documents while preserving their exact structure."
2620
+ role: "developer",
2621
+ content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
2752
2622
  },
2753
2623
  { role: "user", content: prompt }
2754
2624
  ],
2755
2625
  model: openaiConfig.model,
2756
- reasoningEffort: openaiConfig.reasoningEffort,
2757
- verbosity: openaiConfig.verbosity,
2758
- onReasoningChunk: (reasoning) => {
2759
- const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
2760
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
2761
- reasoningProgress: reasoning
2762
- });
2763
- }
2626
+ reasoningEffort: openaiConfig.reasoningEffort
2764
2627
  });
2765
- devLog.info("OpenAI operation completed", {
2628
+ devLog.info("OpenAI translation response received", {
2766
2629
  ...logContext,
2767
- tokensUsed: response.tokensUsed.total,
2768
- cost: response.cost
2630
+ tokensUsed: response.tokensUsed,
2631
+ cost: response.cost,
2632
+ responseLength: response.content.length,
2633
+ responsePreview: response.content.substring(0, 500)
2769
2634
  });
2770
- const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
2635
+ const result = deserializeDocument(sourceDocument, response.content);
2636
+ result[languageFieldName] = targetLanguage;
2637
+ const validation = validateStructureIntegrity(sourceDocument, result);
2771
2638
  if (!validation.valid) {
2772
2639
  devLog.error("Structure validation failed", {
2773
2640
  ...logContext,
@@ -2781,17 +2648,26 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2781
2648
  `Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
2782
2649
  );
2783
2650
  }
2784
- devLog.info("Structure validation passed", logContext);
2785
- const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
2786
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
2651
+ const translationValidation = validateTranslationCompleteness(sourceDocument, result, languageFieldName);
2652
+ return translationValidation.valid || (devLog.warn("Translation completeness warning", {
2653
+ ...logContext,
2654
+ skippedFields: translationValidation.skippedFields,
2655
+ warning: "GPT-5 may have skipped some important fields"
2656
+ }), devLog.info("Skipped field details", {
2657
+ ...logContext,
2658
+ skippedFields: translationValidation.skippedFields.map((field) => ({
2659
+ path: field.path,
2660
+ originalValue: field.originalValue,
2661
+ translatedValue: field.translatedValue
2662
+ }))
2663
+ })), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2787
2664
  tokensUsed: response.tokensUsed.total,
2788
- estimatedCost: response.cost,
2789
- reasoningSummary: response.reasoningSummary
2665
+ estimatedCost: response.cost
2790
2666
  }), result;
2791
2667
  } catch (error) {
2792
2668
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2793
2669
  }
2794
- }, buildTranslationPrompt = ({
2670
+ }, buildTranslationPrompt$1 = ({
2795
2671
  sourceText,
2796
2672
  sourceLanguage,
2797
2673
  targetLanguage,
@@ -2825,12 +2701,19 @@ ${sourceText}
2825
2701
 
2826
2702
  ## Instructions
2827
2703
 
2828
- 1. Translate all text content into ${targetLanguage}
2829
- 2. Follow the guidelines and terminology requirements above
2830
- 3. Preserve protected phrases exactly as they appear
2831
- 4. Return the document in THE EXACT SAME FORMAT
2832
- 5. **CRITICAL**: Only modify text content between tags
2833
- 6. Never add, remove, or change any tags, attributes, or structure
2704
+ 1. **TRANSLATE ALL TEXT FIELDS**: Every field marked with type="string" or containing text must be translated
2705
+ 2. **DO NOT SKIP SIMPLE FIELDS**: Pay special attention to simple fields like "title", "name", "description" - these are often the most important
2706
+ 3. Follow the guidelines and terminology requirements above
2707
+ 4. Preserve protected phrases exactly as they appear
2708
+ 5. Return the document in THE EXACT SAME FORMAT
2709
+ 6. **CRITICAL**: Only modify text content between tags
2710
+ 7. Never add, remove, or change any tags, attributes, or structure
2711
+
2712
+ **Common fields that must always be translated:**
2713
+ - title, name, heading, label
2714
+ - description, caption, alt text
2715
+ - All text within <field> tags
2716
+ - All text within <span> tags
2834
2717
 
2835
2718
  If you used significant reasoning to make translation decisions, you may include a brief summary:
2836
2719
  <reasoning>
@@ -2838,73 +2721,267 @@ Brief summary of key translation decisions and reasoning
2838
2721
  </reasoning>
2839
2722
 
2840
2723
  Then return the translated document:`;
2841
- }, buildReviewPrompt = ({
2842
- sourceText,
2843
- translatedText,
2724
+ }, validateTranslationCompleteness = (sourceDoc, translatedDoc, languageFieldName) => {
2725
+ const skippedFields = [], checkField = (source, translated, path = []) => {
2726
+ if (path.length > 0) {
2727
+ const fieldName = path[path.length - 1];
2728
+ if (fieldName.startsWith("_") || fieldName === languageFieldName)
2729
+ return;
2730
+ }
2731
+ if (typeof source == "string" && typeof translated == "string") {
2732
+ if (source.length > 0 && source === translated) {
2733
+ const fieldName = path[path.length - 1] || "unknown";
2734
+ (["title", "name", "heading", "label", "description", "caption", "alt"].includes(
2735
+ fieldName
2736
+ ) || source.length > 10) && skippedFields.push({
2737
+ path: path.join("."),
2738
+ originalValue: source,
2739
+ translatedValue: translated
2740
+ });
2741
+ }
2742
+ return;
2743
+ }
2744
+ if (source && translated && typeof source == "object" && typeof translated == "object") {
2745
+ if (Array.isArray(source) && Array.isArray(translated)) {
2746
+ const maxLength = Math.min(source.length, translated.length);
2747
+ for (let i = 0; i < maxLength; i++)
2748
+ checkField(source[i], translated[i], [...path, String(i)]);
2749
+ } else if (!Array.isArray(source) && !Array.isArray(translated))
2750
+ for (const key of Object.keys(source))
2751
+ !key.startsWith("_") && key !== languageFieldName && checkField(source[key], translated[key], [
2752
+ ...path,
2753
+ key
2754
+ ]);
2755
+ }
2756
+ };
2757
+ return checkField(sourceDoc, translatedDoc), {
2758
+ valid: skippedFields.length === 0,
2759
+ skippedFields
2760
+ };
2761
+ }, assembleLanguageSpecs = ({
2762
+ sourceDocument,
2763
+ languageFieldName,
2764
+ targetLanguage
2765
+ }) => {
2766
+ const sourceLanugageId = sourceDocument[languageFieldName];
2767
+ if (!sourceLanugageId)
2768
+ throw new Error("[assembleLanguages] Source language ID is missing");
2769
+ return {
2770
+ fromLanguage: {
2771
+ id: sourceLanugageId,
2772
+ title: sourceLanugageId
2773
+ },
2774
+ toLanguage: {
2775
+ id: targetLanguage,
2776
+ title: targetLanguage
2777
+ }
2778
+ };
2779
+ }, buildTranslationPrompt = ({
2844
2780
  sourceLanguage,
2845
2781
  targetLanguage,
2846
- translationPrompts
2782
+ components,
2783
+ isUpdate = !1
2847
2784
  }) => {
2848
- const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2849
- return `# TRANSLATION QUALITY REVIEW
2850
-
2851
- Review this ${targetLanguage} translation and return a corrected version.
2852
-
2853
- ## Review Criteria
2785
+ const sections = [];
2786
+ sections.push(buildAgentDirectivesSection(sourceLanguage, targetLanguage)), sections.push(buildCriticalRulesSection(sourceLanguage, targetLanguage)), isUpdate && sections.push(buildUpdateInstructionsSection()), components.glossaryTerms?.trim() && sections.push(buildGlossarySection(components.glossaryTerms)), components.protectedPhrases?.length && sections.push(buildProtectedPhrasesSection(components.protectedPhrases));
2787
+ const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2788
+ return styleSection && sections.push(styleSection), sections.join(`
2854
2789
 
2855
- 1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
2856
- 2. **Terminology**: Are glossary terms used correctly?
2857
- 3. **Protected Phrases**: Are protected phrases preserved?
2858
- 4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
2859
- 5. **Style**: Does it follow the style guide?
2860
- 6. **Completeness**: Is all content translated?
2790
+ ---
2861
2791
 
2862
- ## Guidelines
2792
+ `);
2793
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2863
2794
 
2864
- ${globalPrompt ? `### General Guidelines
2865
- ${globalPrompt}
2866
- ` : ""}
2867
- ${localePrompt ? `### ${targetLanguage} Guidelines
2868
- ${localePrompt}
2869
- ` : ""}
2870
- ${glossaryTerms ? `### Required Terminology
2871
- ${glossaryTerms}
2872
- ` : ""}
2873
- ${protectedPhrases?.length ? `### Protected Phrases (must preserve)
2874
- ${protectedPhrases.join(", ")}
2875
- ` : ""}
2795
+ - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2796
+ - Scan each field once, then move directly into translation work.
2797
+ - Plan updates before editing so document structure remains intact.
2798
+ - Default to medium reasoning effort and escalate only for ambiguous passages.
2799
+ - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
2800
+ const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2801
+ return `# CRITICAL TRANSLATION RULES
2876
2802
 
2877
- ## Source Document (${sourceLanguage})
2803
+ Follow these without exception:
2878
2804
 
2879
- \`\`\`
2880
- ${sourceText}
2881
- \`\`\`
2805
+ 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2806
+ 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2807
+ 3. Maintain all layout, markdown, rich-text marks, and data structure.
2808
+ 4. Never invent metadata or field values that are absent in the source.
2809
+ 5. Leave technical tokens (code, IDs, filenames) untouched.
2810
+ 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2811
+ }, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
2882
2812
 
2883
- ## Current Translation (${targetLanguage})
2813
+ This document already has a translation; only adjust fields that changed.
2884
2814
 
2885
- \`\`\`
2886
- ${translatedText}
2887
- \`\`\`
2815
+ - Compare the latest source to the existing target text.
2816
+ - Keep prior terminology and style unless the source demands a change.
2817
+ - Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2888
2818
 
2889
- ## Instructions
2819
+ Apply these mappings immediately after protected phrases and before style preferences.
2890
2820
 
2891
- 1. Review the translation for quality issues
2892
- 2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
2893
- 3. Keep good translations as-is - only fix what needs fixing
2894
- 4. Return the corrected translation in THE EXACT SAME FORMAT
2895
- 5. **CRITICAL**: Only modify text content between tags
2896
- 6. Never add, remove, or change any tags, attributes, or structure
2821
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2897
2822
 
2898
- If you made corrections or have insights about the translation quality, include a brief summary:
2899
- <reasoning>
2900
- Brief summary of review findings and corrections made
2901
- </reasoning>
2823
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2902
2824
 
2903
- Then return the corrected translation document:`;
2825
+ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2826
+ const components = [];
2827
+ return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2828
+ "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2829
+ ), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
2830
+ `) : null;
2831
+ }, getSanityAgentTranslation = async ({
2832
+ sourceDocument,
2833
+ targetLanguage,
2834
+ languageFieldName,
2835
+ schemaId,
2836
+ client,
2837
+ translationPrompts
2838
+ }) => {
2839
+ const styleGuide = buildTranslationPrompt({
2840
+ sourceLanguage: sourceDocument[languageFieldName],
2841
+ targetLanguage,
2842
+ isUpdate: !1,
2843
+ // Always fresh translation in a version
2844
+ components: {
2845
+ globalPrompt: translationPrompts.globalPrompt,
2846
+ localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2847
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2848
+ protectedPhrases: translationPrompts.protectedPhrases
2849
+ }
2850
+ }), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2851
+ sourceDocument,
2852
+ languageFieldName,
2853
+ targetLanguage
2854
+ });
2855
+ return await client.agent.action.translate({
2856
+ schemaId,
2857
+ toLanguage,
2858
+ fromLanguage,
2859
+ documentId: sourceDocument._id,
2860
+ noWrite: !0,
2861
+ // Do not write to the document, just return the translated content
2862
+ languageFieldPath: [languageFieldName],
2863
+ styleGuide,
2864
+ protectedPhrases: translationPrompts.protectedPhrases,
2865
+ conditionalPaths: {
2866
+ defaultHidden: !1,
2867
+ defaultReadOnly: !1
2868
+ }
2869
+ });
2870
+ }, handleTranslation = async ({
2871
+ dispatch,
2872
+ client,
2873
+ schemaId,
2874
+ sourceDocument,
2875
+ targetLanguage,
2876
+ existingTranslationId,
2877
+ languageFieldName,
2878
+ translationPrompts,
2879
+ translationMode,
2880
+ openaiConfig,
2881
+ releaseId,
2882
+ excludeFields = []
2883
+ }) => {
2884
+ const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
2885
+ excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
2886
+ documentId: sourceDocument._id,
2887
+ languageFieldName,
2888
+ originalExcludeFields: excludeFields,
2889
+ safeExcludeFields
2890
+ }), devLog.info("Filtering excluded fields before translation", {
2891
+ documentId: sourceDocument._id,
2892
+ documentType: sourceDocument._type,
2893
+ excludeFields: safeExcludeFields,
2894
+ excludeFieldsLength: safeExcludeFields?.length ?? 0,
2895
+ sourceHasTitle: "title" in sourceDocument,
2896
+ sourceTitleValue: sourceDocument.title
2897
+ });
2898
+ const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
2899
+ devLog.info("After filtering excluded fields", {
2900
+ documentId: sourceDocument._id,
2901
+ filteredHasTitle: "title" in filteredDocument,
2902
+ filteredTitleValue: filteredDocument.title,
2903
+ excludedValuesKeys: Object.keys(excludedValues),
2904
+ excludedValues
2905
+ }), devLog.debug("Applying field transforms before translation", {
2906
+ documentId: sourceDocument._id,
2907
+ documentType: sourceDocument._type
2908
+ });
2909
+ const { transformedDocument, originalStructure } = applyTransforms({
2910
+ document: filteredDocument,
2911
+ registry: globalTransformRegistry,
2912
+ schemaType: sourceDocument._type
2913
+ });
2914
+ let translatedDoc;
2915
+ switch (translationMode) {
2916
+ case "sanity-only": {
2917
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2918
+ sourceDocument: transformedDocument,
2919
+ targetLanguage,
2920
+ languageFieldName,
2921
+ schemaId,
2922
+ client,
2923
+ translationPrompts
2924
+ });
2925
+ break;
2926
+ }
2927
+ case "openai-only": {
2928
+ if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2929
+ throw new Error("OpenAI is not configured but openai-only mode was selected");
2930
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2931
+ dispatch,
2932
+ sourceDocument: transformedDocument,
2933
+ targetLanguage,
2934
+ languageFieldName,
2935
+ translationPrompts,
2936
+ openaiConfig
2937
+ });
2938
+ break;
2939
+ }
2940
+ case "sanity-with-review":
2941
+ throw new Error("Sanity with review translation method is not implemented");
2942
+ default:
2943
+ throw new Error(`Invalid translation mode: ${translationMode}`);
2944
+ }
2945
+ if (!translatedDoc)
2946
+ return null;
2947
+ devLog.debug("Restoring field transforms after translation", {
2948
+ documentId: sourceDocument._id,
2949
+ documentType: sourceDocument._type
2950
+ });
2951
+ const transformedResult = restoreTransforms({
2952
+ translatedDocument: translatedDoc,
2953
+ originalStructure,
2954
+ registry: globalTransformRegistry,
2955
+ schemaType: sourceDocument._type
2956
+ }), excludedValuesToRestore = { ...excludedValues };
2957
+ delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
2958
+ documentId: sourceDocument._id,
2959
+ excludedFieldCount: Object.keys(excludedValues).length,
2960
+ excludedValuesKeys: Object.keys(excludedValues),
2961
+ languageFieldExcluded: languageFieldName in excludedValues,
2962
+ transformedResultHasTitle: "title" in transformedResult,
2963
+ transformedResultTitle: transformedResult.title
2964
+ });
2965
+ const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
2966
+ devLog.info("After restoring excluded fields", {
2967
+ documentId: sourceDocument._id,
2968
+ finalDocHasTitle: "title" in finalDoc,
2969
+ finalDocTitle: finalDoc.title
2970
+ });
2971
+ const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2972
+ ...finalDoc,
2973
+ _id: targetDocumentVersionId
2974
+ };
2975
+ return await client.action({
2976
+ actionType: "sanity.action.document.version.create",
2977
+ publishedId: targetDocumentPublishedId,
2978
+ document: newDoc
2979
+ }), newDoc;
2904
2980
  }, translateDocument = async ({
2905
2981
  dispatch,
2906
2982
  client,
2907
2983
  sourceDocument,
2984
+ sourceLanguage,
2908
2985
  targetLanguage,
2909
2986
  languageFieldName,
2910
2987
  shouldOverwriteFreshTranslations,
@@ -2912,7 +2989,8 @@ Then return the corrected translation document:`;
2912
2989
  translationPrompts,
2913
2990
  translationMode,
2914
2991
  openaiConfig,
2915
- releaseId
2992
+ releaseId,
2993
+ excludeFields
2916
2994
  }) => {
2917
2995
  const logContext = {
2918
2996
  documentId: sourceDocument._id,
@@ -2923,62 +3001,65 @@ Then return the corrected translation document:`;
2923
3001
  };
2924
3002
  devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2925
3003
  try {
2926
- const sourceLanguage = sourceDocument[languageFieldName];
2927
- if (devLog.debug("Checking source document language", {
2928
- ...logContext,
2929
- sourceLanguage,
2930
- languageFieldName
2931
- }), !sourceLanguage) {
2932
- const error = `Source document ${sourceDocument._id} does not have a language`;
2933
- throw devLog.error(error, void 0, logContext), new Error(error);
2934
- }
2935
- devLog.debug("Getting translation metadata", logContext);
2936
3004
  const translationMetadata = await getTranslationMetadata({
2937
3005
  client,
2938
3006
  sourceDocument,
2939
3007
  sourceLanguage
2940
3008
  }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
2941
3009
  (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
2942
- ) ?? !1;
2943
- if (devLog.debug("Checking if translation is already fresh", {
2944
- ...logContext,
2945
- isAlreadyFreshTranslation,
2946
- currentRev: sourceDocument._rev
2947
- }), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2948
- devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
3010
+ );
3011
+ if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
3012
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2949
3013
  targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
3014
+ });
3015
+ return;
3016
+ }
3017
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
3018
+ () => handleTranslation({
3019
+ dispatch,
3020
+ client,
3021
+ schemaId,
3022
+ sourceDocument,
3023
+ targetLanguage,
3024
+ existingTranslationId,
3025
+ languageFieldName,
3026
+ translationPrompts,
3027
+ translationMode,
3028
+ openaiConfig,
3029
+ releaseId,
3030
+ excludeFields
3031
+ }),
3032
+ {
3033
+ maxRetries: 3,
3034
+ initialDelayMs: 1e3,
3035
+ isRetryable: isRetryableError,
3036
+ onRetry: (attempt, error, delayMs) => {
3037
+ devLog.warn("Translation failed, scheduling retry", {
3038
+ ...logContext,
3039
+ attempt,
3040
+ delayMs,
3041
+ error: error instanceof Error ? error.message : String(error)
3042
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
3043
+ retryCount: attempt,
3044
+ errorMessage: error instanceof Error ? error.message : String(error)
3045
+ });
3046
+ }
3047
+ }
3048
+ );
3049
+ if (!translationResult.success || !translationResult.result) {
3050
+ const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
3051
+ devLog.error("Translation failed after retries", translationResult.error, {
3052
+ ...logContext,
3053
+ attempts: translationResult.attempts
3054
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
3055
+ errorMessage,
3056
+ retryCount: translationResult.attempts - 1
3057
+ // -1 because attempts includes initial try
2950
3058
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2951
3059
  return;
2952
3060
  }
2953
- const translationTarget = determineTranslationTarget({
2954
- translationMetadata,
2955
- targetLanguage
2956
- }), isNewTranslation = translationTarget.operation === "create";
2957
- devLog.debug("Translation target determined", {
2958
- ...logContext,
2959
- operation: translationTarget.operation,
2960
- isNewTranslation
2961
- });
2962
- const newDocument = await translateEntireDocument({
2963
- dispatch,
2964
- client,
2965
- schemaId,
2966
- sourceDocument,
2967
- targetLanguage,
2968
- languageFieldName,
2969
- translationTarget,
2970
- translationPrompts,
2971
- translationMode,
2972
- openaiConfig,
2973
- releaseId,
2974
- isNewTranslation
2975
- });
2976
- if (!newDocument)
2977
- throw devLog.error("Failed to create translation document", void 0, logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd(), new Error("Failed to create translation document");
2978
- return devLog.debug("Translation document created", {
2979
- ...logContext,
2980
- newDocumentId: newDocument._id
2981
- }), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
3061
+ const newDocument = translationResult.result;
3062
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2982
3063
  targetId: newDocument._id
2983
3064
  }), await updateTranslationMetadata({
2984
3065
  client,
@@ -2987,8 +3068,10 @@ Then return the corrected translation document:`;
2987
3068
  newDocument,
2988
3069
  targetLanguage,
2989
3070
  translationMetadata,
2990
- isNewTranslation
2991
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"), devLog.info("Translation completed successfully", {
3071
+ isNewTranslation: !existingTranslationId
3072
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
3073
+ targetId: newDocument._id
3074
+ }), devLog.info("Translation completed successfully", {
2992
3075
  ...logContext,
2993
3076
  newDocumentId: newDocument._id
2994
3077
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
@@ -3010,81 +3093,66 @@ Then return the corrected translation document:`;
3010
3093
  references = [],
3011
3094
  maxConcurrentTranslationJobs,
3012
3095
  translationMode,
3013
- openaiConfig
3096
+ openaiConfig,
3097
+ excludeFields
3014
3098
  }) => {
3015
3099
  const logContext = {
3016
3100
  documentId: sourceDocument._id,
3101
+ documentRev: sourceDocument._rev,
3017
3102
  targetLanguages,
3018
3103
  referencesCount: references.length,
3019
3104
  maxConcurrentJobs: maxConcurrentTranslationJobs,
3020
3105
  translationMode,
3021
3106
  shouldOverwriteFreshTranslations
3022
3107
  };
3023
- devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
3024
3108
  let releaseId;
3025
3109
  try {
3026
3110
  const sourceLanguage = sourceDocument[languageFieldName];
3027
3111
  if (!sourceLanguage)
3028
3112
  throw devLog.error("Source language not found"), new Error("Source language not found");
3029
- maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
3030
- ...logContext,
3031
- newPermits: maxConcurrentTranslationJobs
3032
- }), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
3113
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
3033
3114
  type: "SET_OVERALL_STATUS",
3034
3115
  payload: "translating"
3035
3116
  });
3036
- 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()}`;
3037
- devLog.info("Creating Content Release for translation batch", {
3038
- ...logContext,
3039
- releaseTitle
3040
- });
3041
- const { releaseId: newReleaseId } = await client.releases.create({
3117
+ 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({
3042
3118
  metadata: {
3043
3119
  title: releaseTitle,
3044
3120
  description: releaseDescription,
3045
- releaseType: "asap"
3121
+ releaseType: "undecided"
3046
3122
  }
3047
3123
  });
3048
- releaseId = newReleaseId, devLog.info("Content Release created", {
3124
+ devLog.info("Content Release created", {
3049
3125
  ...logContext,
3050
- releaseId
3126
+ releaseId: releaseId2
3051
3127
  });
3052
3128
  const sourceDocuments = [
3053
3129
  {
3054
3130
  _id: sourceDocument._id,
3055
3131
  _type: sourceDocument._type,
3056
- // TODO: Handle this properly
3132
+ // TODO: Extract document title more robustly across schema types
3057
3133
  title: sourceDocument.name || sourceDocument.title
3058
3134
  },
3059
3135
  ...references
3060
- ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), targetDocumentIds = [];
3061
- devLog.debug("Prepared document set for translation", {
3062
- ...logContext,
3063
- sourceDocumentIds: sourceDocumentIds.length,
3064
- totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
3065
- });
3066
- const translationPrompts = await fetchTranslationPrompts({
3136
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
3067
3137
  client,
3068
3138
  sourceLanguage,
3069
3139
  targetLanguages,
3070
3140
  sourceDocument
3071
3141
  });
3072
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
3142
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
3073
3143
  const translationTasks = sourceDocumentIds.flatMap(
3074
3144
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3075
3145
  try {
3076
3146
  await translationSemaphore.withPermit(async () => {
3077
3147
  const document = await client.withConfig({
3078
- // This enables us to detect documents that might be stale due to unpublished changes
3079
- perspective: "raw"
3080
- }).fetch("*[_id == $id][0]", {
3081
- id: documentId
3082
- });
3083
- if (document && releaseId) {
3084
- const targetDocumentId = await translateDocument({
3148
+ perspective: "drafts"
3149
+ }).getDocument(documentId);
3150
+ if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
3151
+ const versionedTargetDocumentId = await translateDocument({
3085
3152
  dispatch,
3086
3153
  client,
3087
3154
  sourceDocument: document,
3155
+ sourceLanguage,
3088
3156
  targetLanguage,
3089
3157
  languageFieldName,
3090
3158
  shouldOverwriteFreshTranslations,
@@ -3092,9 +3160,10 @@ Then return the corrected translation document:`;
3092
3160
  translationPrompts,
3093
3161
  translationMode,
3094
3162
  openaiConfig,
3095
- releaseId
3163
+ releaseId: releaseId2,
3164
+ excludeFields
3096
3165
  });
3097
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
3166
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
3098
3167
  }
3099
3168
  });
3100
3169
  } catch (error) {
@@ -3110,25 +3179,21 @@ Then return the corrected translation document:`;
3110
3179
  }
3111
3180
  })
3112
3181
  ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
3113
- devLog.info("Translation tasks summary", {
3182
+ devLog.info("Translation tasks completed", {
3114
3183
  ...logContext,
3115
3184
  total: results.length,
3116
3185
  succeeded,
3117
- failed
3118
- }), devLog.info("All translation tasks completed", {
3119
- ...logContext,
3120
- targetDocumentIds: targetDocumentIds.length
3186
+ failed,
3187
+ targetDocuments: versionedTargetDocumentIds.length
3121
3188
  }), dispatch({
3122
3189
  type: "SET_OVERALL_STATUS",
3123
3190
  payload: "linking_references"
3124
- }), devLog.debug("Starting reference localization", {
3125
- ...logContext,
3126
- documentsToLocalize: targetDocumentIds.length
3127
3191
  }), await Promise.all(
3128
- targetDocumentIds.map(async (documentId) => {
3192
+ versionedTargetDocumentIds.map(async (documentId) => {
3129
3193
  await localizeReferences({
3130
3194
  client,
3131
3195
  documentId,
3196
+ sourceLanguage,
3132
3197
  languageFieldName
3133
3198
  });
3134
3199
  })
@@ -3138,26 +3203,13 @@ Then return the corrected translation document:`;
3138
3203
  ...logContext,
3139
3204
  succeeded,
3140
3205
  failed
3141
- }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : (devLog.info("Document set translation completed successfully", logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
3206
+ }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
3142
3207
  } catch (error) {
3143
- if (devLog.error(
3208
+ throw devLog.error(
3144
3209
  "translateDocumentSet failed with unexpected error",
3145
3210
  error instanceof Error ? error : new Error(String(error)),
3146
3211
  logContext
3147
- ), releaseId)
3148
- try {
3149
- await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
3150
- ...logContext,
3151
- releaseId
3152
- });
3153
- } catch (cleanupError) {
3154
- devLog.error(
3155
- "Failed to cleanup release",
3156
- cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
3157
- logContext
3158
- );
3159
- }
3160
- throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
3212
+ ), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
3161
3213
  }
3162
3214
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
3163
3215
  const {
@@ -3182,7 +3234,8 @@ Then return the corrected translation document:`;
3182
3234
  maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
3183
3235
  maxReferenceDepth: config.maxReferenceDepth,
3184
3236
  translationMode,
3185
- openaiConfig: config.openai
3237
+ openaiConfig: config.openai,
3238
+ excludeFields: config.excludeFields
3186
3239
  });
3187
3240
  }, [
3188
3241
  document,
@@ -3194,6 +3247,7 @@ Then return the corrected translation document:`;
3194
3247
  config.maxConcurrentTranslationJobs,
3195
3248
  config.maxReferenceDepth,
3196
3249
  config.openai,
3250
+ config.excludeFields,
3197
3251
  references,
3198
3252
  shouldOverwriteFreshTranslations,
3199
3253
  translationMode
@@ -3576,36 +3630,29 @@ Then return the corrected translation document:`;
3576
3630
  /* @__PURE__ */ jsxs(Code, { size: 1, children: [
3577
3631
  job._type,
3578
3632
  " \u2014 ",
3579
- job._id.slice(-6)
3580
- ] }),
3581
- !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3582
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3583
- job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3584
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
3585
- /* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
3586
- ] }) })
3633
+ job._id?.slice(-6) || "unknown"
3587
3634
  ] }),
3588
- job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3589
- /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3590
- job.tokensUsed.toLocaleString(),
3591
- " tokens (~$",
3592
- job.estimatedCost?.toFixed(4),
3593
- ")"
3594
- ] }),
3595
- job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3596
- "\u{1F4A1} ",
3597
- job.reasoningSummary
3598
- ] }) })
3635
+ !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3636
+ job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3637
+ job.tokensUsed.toLocaleString(),
3638
+ " tokens (~$",
3639
+ job.estimatedCost?.toFixed(4),
3640
+ ")"
3599
3641
  ] }) : null,
3600
3642
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3601
3643
  "Error: ",
3602
3644
  job.errorMessage
3645
+ ] }) }) : null,
3646
+ job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3647
+ "Retrying after: ",
3648
+ job.errorMessage
3603
3649
  ] }) }) : null
3604
3650
  ] })
3605
3651
  ] }),
3606
3652
  /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
3607
3653
  job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
3608
- job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
3654
+ job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
3655
+ job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
3609
3656
  job.targetId ? /* @__PURE__ */ jsx(
3610
3657
  TranslationOpenerButton,
3611
3658
  {
@@ -3685,12 +3732,25 @@ Then return the corrected translation document:`;
3685
3732
  content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
3686
3733
  children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
3687
3734
  }
3688
- ), ErrorBadge = () => /* @__PURE__ */ jsx(
3735
+ ), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3736
+ Tooltip,
3737
+ {
3738
+ portal: !0,
3739
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
3740
+ children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
3741
+ "Failed",
3742
+ retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
3743
+ ] })
3744
+ }
3745
+ ), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3689
3746
  Tooltip,
3690
3747
  {
3691
3748
  portal: !0,
3692
- content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
3693
- children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
3749
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
3750
+ children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
3751
+ "Retry ",
3752
+ retryCount || 1
3753
+ ] })
3694
3754
  }
3695
3755
  ), getStatusLabel = (status) => ({
3696
3756
  idle: "Idle",
@@ -3700,6 +3760,7 @@ Then return the corrected translation document:`;
3700
3760
  translating_with_openai: "Translating with GPT-5...",
3701
3761
  reviewing_with_openai: "Reviewing translation with GPT-5...",
3702
3762
  updating_metadata: "Saving metadata...",
3763
+ retrying: "Retrying...",
3703
3764
  error: "Error",
3704
3765
  skipped: "Skipped",
3705
3766
  completed: "Completed"
@@ -3709,26 +3770,9 @@ Then return the corrected translation document:`;
3709
3770
  "completed"
3710
3771
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3711
3772
  const { overallStatus, document } = useTranslationContext();
3712
- return devLog.debug("TranslationDialog rendered", {
3713
- documentId: document._id,
3714
- overallStatus,
3715
- component: "TranslationDialog"
3716
- }), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
3717
- documentId: document._id,
3718
- overallStatus,
3719
- component: "TranslationDialog"
3720
- }), /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen })) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? (devLog.debug("Showing translation progress panel", {
3773
+ 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", {
3721
3774
  documentId: document._id,
3722
- overallStatus,
3723
- component: "TranslationDialog"
3724
- }), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
3725
- documentId: document._id,
3726
- overallStatus,
3727
- component: "TranslationDialog"
3728
- }), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
3729
- documentId: document._id,
3730
- overallStatus,
3731
- component: "TranslationDialog"
3775
+ overallStatus
3732
3776
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3733
3777
  }, getTranslateAction = (config) => ({ draft, published }) => {
3734
3778
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
@@ -3755,7 +3799,427 @@ Then return the corrected translation document:`;
3755
3799
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3756
3800
  }
3757
3801
  } : (devLog.error("Missing config in TranslateAction"), null);
3758
- }, aiTranslationPlugin = definePlugin((config) => ({
3802
+ }, translationGlossaryTerm = defineType({
3803
+ name: "translationGlossaryTerm",
3804
+ title: "Translation Glossary Term",
3805
+ icon: TranslateIcon,
3806
+ type: "document",
3807
+ fields: [
3808
+ defineField({
3809
+ name: "translations",
3810
+ title: "Translations",
3811
+ type: "internationalizedArrayString"
3812
+ })
3813
+ ],
3814
+ preview: {
3815
+ select: {
3816
+ translations: "translations"
3817
+ },
3818
+ prepare({ translations }) {
3819
+ const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
3820
+ return {
3821
+ title: enTranslation?.value || firstTranslation?.value || "No translation available"
3822
+ };
3823
+ }
3824
+ }
3825
+ }), translationHistory = defineField({
3826
+ name: "translationHistory",
3827
+ type: "array",
3828
+ of: [
3829
+ {
3830
+ name: "targetLanguageHistory",
3831
+ type: "object",
3832
+ fields: [
3833
+ defineField({
3834
+ name: "language",
3835
+ title: "Language",
3836
+ description: "The language of the target document",
3837
+ type: "string"
3838
+ }),
3839
+ defineField({
3840
+ name: "sourceId",
3841
+ title: "Source ID",
3842
+ description: "The ID of the source document",
3843
+ type: "string"
3844
+ }),
3845
+ defineField({
3846
+ name: "sourceRev",
3847
+ title: "Source Revision",
3848
+ description: "The revision ID of the source document at the time of translation",
3849
+ type: "string"
3850
+ }),
3851
+ defineField({
3852
+ name: "sourceLanguage",
3853
+ title: "Source Language",
3854
+ description: "The language of the source document",
3855
+ type: "string"
3856
+ }),
3857
+ defineField({
3858
+ name: "sourceSnapshot",
3859
+ title: "Source Snapshot",
3860
+ description: "The snapshot of the source document at the time of translation",
3861
+ type: "text"
3862
+ }),
3863
+ defineField({
3864
+ name: "targetId",
3865
+ title: "Target ID",
3866
+ description: "The ID of the target document",
3867
+ type: "string"
3868
+ })
3869
+ ]
3870
+ }
3871
+ ]
3872
+ }), translationSettings = defineType({
3873
+ name: "translationSettings",
3874
+ title: "Translation Settings",
3875
+ icon: TranslateIcon,
3876
+ type: "document",
3877
+ groups: [
3878
+ {
3879
+ name: "general",
3880
+ title: "General",
3881
+ default: !0
3882
+ },
3883
+ {
3884
+ name: "history",
3885
+ title: "History"
3886
+ }
3887
+ ],
3888
+ fields: [
3889
+ defineField({
3890
+ name: "globalPrompt",
3891
+ title: "Global Translation Prompt",
3892
+ description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
3893
+ type: "text",
3894
+ rows: 5,
3895
+ group: "general"
3896
+ }),
3897
+ defineField({
3898
+ name: "localePrompts",
3899
+ title: "Locale Prompts",
3900
+ description: "Prompts that will be given to the Sanity AI Agent for each locale.",
3901
+ type: "internationalizedArrayText",
3902
+ group: "general"
3903
+ }),
3904
+ defineField({
3905
+ name: "protectedPhrases",
3906
+ title: "Protected Phrases",
3907
+ description: "Phrases that will never be translated.",
3908
+ type: "array",
3909
+ of: [{ type: "string" }],
3910
+ group: "general"
3911
+ }),
3912
+ defineField({
3913
+ name: "changeHistory",
3914
+ title: "Change History",
3915
+ description: "History of changes made via user feedback.",
3916
+ type: "array",
3917
+ readOnly: !0,
3918
+ group: "history",
3919
+ of: [
3920
+ {
3921
+ type: "object",
3922
+ fields: [
3923
+ defineField({
3924
+ name: "timestamp",
3925
+ title: "Timestamp",
3926
+ type: "datetime"
3927
+ }),
3928
+ defineField({
3929
+ name: "changeType",
3930
+ title: "Change Type",
3931
+ type: "string",
3932
+ options: {
3933
+ list: [
3934
+ { title: "Global Prompt", value: "globalPrompt" },
3935
+ { title: "Locale Prompt", value: "localePrompt" },
3936
+ { title: "Protected Phrase", value: "protectedPhrase" },
3937
+ { title: "Glossary Term", value: "glossaryTerm" }
3938
+ ]
3939
+ }
3940
+ }),
3941
+ defineField({
3942
+ name: "description",
3943
+ title: "Description",
3944
+ type: "text",
3945
+ rows: 3
3946
+ }),
3947
+ defineField({
3948
+ name: "locale",
3949
+ title: "Locale",
3950
+ type: "string",
3951
+ description: "Target locale for locale-specific changes"
3952
+ })
3953
+ ],
3954
+ preview: {
3955
+ select: {
3956
+ timestamp: "timestamp",
3957
+ changeType: "changeType",
3958
+ description: "description"
3959
+ },
3960
+ prepare({ timestamp, changeType, description }) {
3961
+ return {
3962
+ title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
3963
+ subtitle: description
3964
+ };
3965
+ }
3966
+ }
3967
+ }
3968
+ ]
3969
+ })
3970
+ ],
3971
+ preview: {
3972
+ prepare: () => ({
3973
+ title: "Translation Settings"
3974
+ })
3975
+ }
3976
+ });
3977
+ function isPortableText(value) {
3978
+ return Array.isArray(value) && value.some((item) => item?._type === "block");
3979
+ }
3980
+ const richTextTransform = {
3981
+ // Matcher combines field name pattern with value-based detection
3982
+ // fieldPath pattern acts as a pre-filter for performance
3983
+ // valueMatcher definitively checks if the value is actually Portable Text
3984
+ matcher: {
3985
+ // Pre-filter: Match all fields to enable detection based solely on value
3986
+ fieldPath: /.*/,
3987
+ // Definitive check: Verify the value is actually Portable Text
3988
+ valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
3989
+ },
3990
+ toTranslatable: (value, context) => {
3991
+ if (!Array.isArray(value) || !isPortableText(value))
3992
+ return {
3993
+ _type: "translatable.richtext",
3994
+ translationText: JSON.stringify(value),
3995
+ _originalValue: value
3996
+ };
3997
+ devLog.info("Applying rich text transform with XML serialization", {
3998
+ fieldPath: context.fieldPath,
3999
+ blockCount: value.length
4000
+ });
4001
+ try {
4002
+ const tempDoc = {
4003
+ _id: "temp",
4004
+ _type: "richTextWrapper",
4005
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4006
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4007
+ _rev: "temp",
4008
+ content: value
4009
+ }, serialized = serializeDocument(tempDoc);
4010
+ devLog.debug("Serialized rich text document", {
4011
+ fieldPath: context.fieldPath,
4012
+ serializedLength: serialized.length,
4013
+ preview: serialized.substring(0, 200)
4014
+ });
4015
+ const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
4016
+ if (match?.[1]) {
4017
+ const result = {
4018
+ _type: "translatable.richtext",
4019
+ translationText: match[1].trim(),
4020
+ _originalValue: value
4021
+ };
4022
+ return devLog.debug("Successfully extracted and wrapped rich text", {
4023
+ fieldPath: context.fieldPath,
4024
+ translationTextLength: result.translationText.length
4025
+ }), result;
4026
+ }
4027
+ return devLog.warn("Failed to serialize rich text content - regex did not match", {
4028
+ fieldPath: context.fieldPath,
4029
+ serializedPreview: serialized.substring(0, 500)
4030
+ }), {
4031
+ _type: "translatable.richtext",
4032
+ translationText: JSON.stringify(value),
4033
+ _originalValue: value
4034
+ };
4035
+ } catch (error) {
4036
+ return devLog.error("Error serializing rich text", error, {
4037
+ fieldPath: context.fieldPath
4038
+ }), {
4039
+ _type: "translatable.richtext",
4040
+ translationText: JSON.stringify(value),
4041
+ _originalValue: value
4042
+ };
4043
+ }
4044
+ },
4045
+ fromTranslatable: (translatedValue, originalValue, context) => {
4046
+ if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
4047
+ return devLog.debug("Value is not a translatable rich text object, returning original", {
4048
+ fieldPath: context.fieldPath,
4049
+ hasCorrectType: translatedValue._type === "translatable.richtext"
4050
+ }), originalValue;
4051
+ devLog.info("Deserializing rich text content", {
4052
+ fieldPath: context.fieldPath,
4053
+ translationTextLength: translatedValue.translationText.length
4054
+ });
4055
+ try {
4056
+ const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
4057
+ ${translationText}
4058
+ </field>`;
4059
+ devLog.debug("Prepared wrapped serialized content", {
4060
+ fieldPath: context.fieldPath,
4061
+ wrappedLength: wrappedSerialized.length,
4062
+ preview: wrappedSerialized.substring(0, 300)
4063
+ });
4064
+ const tempOriginalDoc = {
4065
+ _id: "temp",
4066
+ _type: "richTextWrapper",
4067
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4068
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4069
+ _rev: "temp",
4070
+ content: originalValue
4071
+ }, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
4072
+ return devLog.debug("Deserialization result", {
4073
+ fieldPath: context.fieldPath,
4074
+ hasContent: !!result.content,
4075
+ contentType: Array.isArray(result.content) ? "array" : typeof result.content,
4076
+ contentLength: Array.isArray(result.content) ? result.content.length : void 0
4077
+ }), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
4078
+ fieldPath: context.fieldPath,
4079
+ resultKeys: Object.keys(result)
4080
+ }), originalValue);
4081
+ } catch (error) {
4082
+ return devLog.error("Failed to deserialize rich text", error, {
4083
+ fieldPath: context.fieldPath
4084
+ }), originalValue;
4085
+ }
4086
+ }
4087
+ }, vttCaptionTransform = {
4088
+ matcher: {
4089
+ fieldName: "captions",
4090
+ schemaType: "video"
4091
+ },
4092
+ toTranslatable: (captionsArray, context) => {
4093
+ if (devLog.info("VTT captions toTranslatable - input:", {
4094
+ fieldPath: context.fieldPath,
4095
+ isArray: Array.isArray(captionsArray),
4096
+ count: Array.isArray(captionsArray) ? captionsArray.length : 0,
4097
+ firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
4098
+ }), !Array.isArray(captionsArray))
4099
+ return devLog.debug("VTT caption field is not an array, returning empty transform", {
4100
+ fieldPath: context.fieldPath,
4101
+ value: captionsArray
4102
+ }), {
4103
+ _type: "translatable.vtt.captions",
4104
+ translationText: "",
4105
+ _originalMetadata: []
4106
+ };
4107
+ devLog.info("Applying VTT caption hybrid transform", {
4108
+ fieldPath: context.fieldPath,
4109
+ count: captionsArray.length
4110
+ });
4111
+ const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
4112
+ const { text: _text, ...metadata } = caption;
4113
+ return {
4114
+ ...metadata,
4115
+ _originalIndex: index
4116
+ };
4117
+ });
4118
+ return devLog.info("VTT captions serialized:", {
4119
+ fieldPath: context.fieldPath,
4120
+ translationTextLength: translationText.length,
4121
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
4122
+ metadataCount: originalMetadata.length,
4123
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
4124
+ }), {
4125
+ _type: "translatable.vtt.captions",
4126
+ translationText,
4127
+ _originalMetadata: originalMetadata
4128
+ };
4129
+ },
4130
+ fromTranslatable: (translatedValue, _originalValue, context) => {
4131
+ if (devLog.info("VTT captions fromTranslatable - input:", {
4132
+ fieldPath: context.fieldPath,
4133
+ valueType: typeof translatedValue,
4134
+ isCorrectType: translatedValue._type === "translatable.vtt.captions",
4135
+ hasText: !!translatedValue.translationText,
4136
+ hasMetadata: Array.isArray(translatedValue._originalMetadata)
4137
+ }), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
4138
+ return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
4139
+ fieldPath: context.fieldPath,
4140
+ valueType: typeof translatedValue,
4141
+ translatedValue
4142
+ }), [];
4143
+ devLog.info("Processing translated VTT captions", {
4144
+ fieldPath: context.fieldPath,
4145
+ metadataCount: translatedValue._originalMetadata.length
4146
+ });
4147
+ const { translationText, _originalMetadata } = translatedValue;
4148
+ devLog.info("VTT captions translationText:", {
4149
+ fieldPath: context.fieldPath,
4150
+ translationTextLength: translationText.length,
4151
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
4152
+ });
4153
+ const result = deserializeVTTCaptionText(translationText, _originalMetadata);
4154
+ return devLog.info("VTT captions final result:", {
4155
+ fieldPath: context.fieldPath,
4156
+ resultCount: result.length,
4157
+ resultExample: result.length > 0 ? result[0] : null
4158
+ }), result;
4159
+ }
4160
+ };
4161
+ function serializeVTTCaptionText(captions) {
4162
+ if (devLog.debug("serializeVTTCaptionText - input:", {
4163
+ captionsCount: captions.length,
4164
+ firstCaption: captions.length > 0 ? captions[0] : null
4165
+ }), !Array.isArray(captions) || captions.length === 0)
4166
+ return "";
4167
+ const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
4168
+ text: caption.text,
4169
+ hasText: typeof caption.text == "string"
4170
+ }), `[${index}] ${caption.text || ""}`)).join(`
4171
+ `);
4172
+ return devLog.debug("serializeVTTCaptionText - output:", {
4173
+ resultLength: result.length,
4174
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
4175
+ }), result;
4176
+ }
4177
+ function deserializeVTTCaptionText(translationText, originalMetadata) {
4178
+ if (devLog.debug("deserializeVTTCaptionText - input:", {
4179
+ translationTextLength: translationText.length,
4180
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
4181
+ metadataCount: originalMetadata.length,
4182
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
4183
+ }), !translationText || translationText.trim() === "")
4184
+ return [];
4185
+ const result = [], lines = translationText.split(`
4186
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/;
4187
+ for (const line of lines) {
4188
+ const match = line.match(lineRegex);
4189
+ if (!match) {
4190
+ devLog.warn("Failed to parse caption line", { line });
4191
+ continue;
4192
+ }
4193
+ const index = Number.parseInt(match[1], 10), text = match[2];
4194
+ devLog.debug("Found caption:", {
4195
+ index,
4196
+ text
4197
+ });
4198
+ const metadata = originalMetadata.find((m) => m._originalIndex === index);
4199
+ if (metadata) {
4200
+ devLog.debug(`Found matching metadata for index ${index}`);
4201
+ const { _originalIndex, ...metadataWithoutIndex } = metadata;
4202
+ result.push({
4203
+ ...metadataWithoutIndex,
4204
+ text
4205
+ });
4206
+ } else
4207
+ devLog.warn("Missing metadata for caption", { index }), result.push({
4208
+ _type: "vtt.cue",
4209
+ _key: `caption-${index}`,
4210
+ text,
4211
+ startTime: 0,
4212
+ endTime: 0
4213
+ });
4214
+ }
4215
+ return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
4216
+ resultCount: result.length,
4217
+ resultExample: result.length > 0 ? result[0] : null
4218
+ }), result;
4219
+ }
4220
+ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
4221
+ globalTransformRegistry.register(transform);
4222
+ })), {
3759
4223
  name: "sanity-plugin-ai-translation",
3760
4224
  document: {
3761
4225
  actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
@@ -3765,7 +4229,13 @@ Then return the corrected translation document:`;
3765
4229
  }
3766
4230
  })), translationHistorySchema = translationHistory;
3767
4231
  export {
4232
+ COMMON_ENUM_FIELDS,
4233
+ PREDEFINED_EXCLUSIONS,
3768
4234
  aiTranslationPlugin,
3769
- translationHistorySchema
4235
+ filterExcludedFields,
4236
+ restoreExcludedFields,
4237
+ richTextTransform,
4238
+ translationHistorySchema,
4239
+ vttCaptionTransform
3770
4240
  };
3771
4241
  //# sourceMappingURL=index.js.map