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

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 = [];
@@ -1755,7 +1500,12 @@ const fetchTranslationPrompts = async ({
1755
1500
  }
1756
1501
  };
1757
1502
  return recursiveSearch(obj, []), results;
1758
- }, localizeReferences = async ({ client, documentId, languageFieldName }) => {
1503
+ }, localizeReferences = async ({
1504
+ client,
1505
+ documentId,
1506
+ sourceLanguage,
1507
+ languageFieldName
1508
+ }) => {
1759
1509
  try {
1760
1510
  const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
1761
1511
  if (!doc) {
@@ -1777,25 +1527,26 @@ const fetchTranslationPrompts = async ({
1777
1527
  const refs = findKeyValues(doc, "_ref");
1778
1528
  if (!refs?.length)
1779
1529
  return;
1780
- await updateReferences(documentId, client, refs, targetLanguage);
1530
+ await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
1781
1531
  } catch (error) {
1782
1532
  devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
1783
1533
  documentId,
1784
1534
  component: "localizeReferences"
1785
1535
  });
1786
1536
  }
1787
- }, updateReferences = async (documentId, client, refs, targetLanguage) => {
1537
+ }, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
1788
1538
  if (!refs?.length)
1789
1539
  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)]`,
1540
+ const refIds = refs.map((r) => getPublishedId(r.value)), translations = await client.fetch(
1541
+ `*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
1542
+ "sourceId": translations[_key == $sourceLanguage][0].value._ref,
1543
+ "targetId": translations[_key == $targetLanguage][0].value._ref,
1544
+ "targetType": translations[_key == $sourceLanguage][0].value -> _type
1545
+ }`,
1796
1546
  {
1797
1547
  translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1798
1548
  refIds,
1549
+ sourceLanguage,
1799
1550
  targetLanguage
1800
1551
  }
1801
1552
  ), translationMap = /* @__PURE__ */ new Map();
@@ -1804,11 +1555,10 @@ const fetchTranslationPrompts = async ({
1804
1555
  const patch = client.patch(documentId);
1805
1556
  let hasChanges = !1;
1806
1557
  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({
1558
+ const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
1559
+ newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
1809
1560
  [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1810
- _type: "reference",
1811
- _weak: !1
1561
+ type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
1812
1562
  }
1813
1563
  }), hasChanges = !0);
1814
1564
  }
@@ -1833,7 +1583,9 @@ const fetchTranslationPrompts = async ({
1833
1583
  payload
1834
1584
  });
1835
1585
  }
1836
- }, getTranslationMetadata = async ({
1586
+ }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1587
+ (translation) => translation._key === targetLanguage
1588
+ )?.value._ref || null, getTranslationMetadata = async ({
1837
1589
  client,
1838
1590
  sourceDocument,
1839
1591
  sourceLanguage
@@ -1882,7 +1634,7 @@ const fetchTranslationPrompts = async ({
1882
1634
  _type: "internationalizedArrayReferenceValue",
1883
1635
  value: {
1884
1636
  _type: "reference",
1885
- _ref: undraftId(newDocument._id),
1637
+ _ref: getPublishedId(newDocument._id),
1886
1638
  _weak: !0,
1887
1639
  _strengthenOnPublish: {
1888
1640
  _type: "reference",
@@ -1892,7 +1644,7 @@ const fetchTranslationPrompts = async ({
1892
1644
  }, newTranslationHistory = {
1893
1645
  _key: targetLanguage,
1894
1646
  language: targetLanguage,
1895
- sourceId: undraftId(sourceDocument._id),
1647
+ sourceId: getPublishedId(sourceDocument._id),
1896
1648
  sourceRev: sourceDocument._rev,
1897
1649
  sourceLanguage,
1898
1650
  sourceSnapshot: JSON.stringify(sourceDocument),
@@ -1913,79 +1665,223 @@ const fetchTranslationPrompts = async ({
1913
1665
  component: "updateTranslationMetadata"
1914
1666
  }), error;
1915
1667
  }
1916
- }, setJobStatus = (dispatch, _id, language, status, payload) => {
1917
- dispatch({
1918
- type: "SET_TRANSLATION_JOB_STATUS",
1919
- payload: defaults(payload, {
1920
- _id,
1921
- language,
1922
- status
1923
- })
1924
- });
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)));
1950
- };
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
1668
+ }, setJobStatus = (dispatch, _id, language, status, payload) => {
1669
+ dispatch({
1670
+ type: "SET_TRANSLATION_JOB_STATUS",
1671
+ payload: defaults(payload, {
1672
+ _id,
1673
+ language,
1674
+ status
1675
+ })
1676
+ });
1677
+ };
1678
+ function applyTransforms({
1679
+ document,
1680
+ registry,
1681
+ schemaType,
1682
+ parentPath = ""
1683
+ }) {
1684
+ if (!document)
1685
+ return { transformedDocument: document, originalStructure: {} };
1686
+ if (typeof document != "object")
1687
+ return { transformedDocument: document, originalStructure: {} };
1688
+ if (Array.isArray(document)) {
1689
+ const originalStructure2 = {};
1690
+ return { transformedDocument: document.map((item, index) => {
1691
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
1692
+ document: item,
1693
+ registry,
1694
+ schemaType,
1695
+ parentPath: itemPath
1696
+ });
1697
+ return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
1698
+ }), originalStructure: originalStructure2 };
1699
+ }
1700
+ const transformedObj = {}, originalStructure = {};
1701
+ for (const [key, value] of Object.entries(document)) {
1702
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
1703
+ if (key.startsWith("_") && key !== "_type") {
1704
+ transformedObj[key] = value;
1705
+ continue;
1706
+ }
1707
+ const transform = registry.findTransform(schemaType, fieldPath, value, document);
1708
+ if (transform) {
1709
+ devLog.debug("Applying transform", { fieldPath, schemaType });
1710
+ const context = {
1711
+ fieldPath,
1712
+ schemaType,
1713
+ document
1714
+ }, transformedValue = transform.toTranslatable(value, context);
1715
+ transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
1716
+ value,
1717
+ transform
1718
+ };
1719
+ } else {
1720
+ const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
1721
+ document: value,
1722
+ registry,
1723
+ schemaType,
1724
+ parentPath: fieldPath
1725
+ });
1726
+ Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
1727
+ }
1728
+ }
1729
+ return { transformedDocument: transformedObj, originalStructure };
1730
+ }
1731
+ function restoreTransforms({
1732
+ translatedDocument,
1733
+ originalStructure,
1734
+ registry,
1735
+ schemaType,
1736
+ parentPath = ""
1737
+ }) {
1738
+ if (translatedDocument == null || typeof translatedDocument != "object")
1739
+ return translatedDocument;
1740
+ if (Array.isArray(translatedDocument))
1741
+ return translatedDocument.map((item, index) => {
1742
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
1743
+ return restoreTransforms({
1744
+ translatedDocument: item,
1745
+ originalStructure,
1746
+ registry,
1747
+ schemaType,
1748
+ parentPath: itemPath
1749
+ });
1750
+ });
1751
+ const originalData = originalStructure[parentPath];
1752
+ if (originalData) {
1753
+ const { value: originalValue, transform } = originalData, context = {
1754
+ fieldPath: parentPath,
1755
+ schemaType,
1756
+ document: translatedDocument
1757
+ };
1758
+ return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
1759
+ }
1760
+ const restoredObj = {}, translatedObj = translatedDocument;
1761
+ for (const [key, value] of Object.entries(translatedObj)) {
1762
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
1763
+ if (fieldOriginalData) {
1764
+ const { value: originalValue, transform } = fieldOriginalData, context = {
1765
+ fieldPath,
1766
+ schemaType,
1767
+ document: translatedDocument
1768
+ };
1769
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
1770
+ } else
1771
+ restoredObj[key] = restoreTransforms({
1772
+ translatedDocument: value,
1773
+ originalStructure,
1774
+ registry,
1775
+ schemaType,
1776
+ parentPath: fieldPath
1777
+ });
1778
+ }
1779
+ return restoredObj;
1780
+ }
1781
+ class TransformRegistry {
1782
+ transforms = [];
1783
+ /**
1784
+ * Register a transform with the registry.
1785
+ * @param transform - Field transform to register
1786
+ */
1787
+ register(transform) {
1788
+ devLog.debug("Registering field transform", {
1789
+ matcher: transform.matcher
1790
+ }), this.transforms.push(transform);
1791
+ }
1792
+ /**
1793
+ * Find a matching transform for the given field.
1794
+ * If multiple transforms match, returns the first one (order matters!).
1795
+ * @param schemaType - Document schema type
1796
+ * @param fieldPath - Path to the field
1797
+ * @param value - Optional field value for value-based matching
1798
+ * @param document - Optional full document for context
1799
+ * @returns Matching transform or undefined if none found
1800
+ */
1801
+ findTransform(schemaType, fieldPath, value, document) {
1802
+ const matches = [];
1803
+ for (const transform of this.transforms) {
1804
+ const { matcher } = transform;
1805
+ if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
1806
+ if (matcher.fieldPath) {
1807
+ if (typeof matcher.fieldPath == "string") {
1808
+ if (matcher.fieldPath !== fieldPath)
1809
+ continue;
1810
+ } else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
1811
+ continue;
1812
+ }
1813
+ if (matcher.fieldName) {
1814
+ const segments = fieldPath.split(".");
1815
+ if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
1816
+ continue;
1817
+ }
1818
+ if (matcher.valueMatcher) {
1819
+ if (value === void 0)
1820
+ continue;
1821
+ const context = {
1822
+ fieldPath,
1823
+ schemaType,
1824
+ document: document || {}
1825
+ };
1826
+ if (!matcher.valueMatcher(value, context))
1827
+ continue;
1828
+ }
1829
+ matches.push(transform);
1830
+ }
1831
+ }
1832
+ return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
1833
+ schemaType,
1834
+ fieldPath,
1835
+ matchCount: matches.length,
1836
+ firstMatcher: matches[0].matcher
1837
+ }), matches[0];
1838
+ }
1839
+ }
1840
+ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1841
+ const model = request.model || "gpt-5", requestBody = {
1842
+ model: "gpt-5.1",
1843
+ input: request.messages
1844
+ // Uses 'input' instead of 'messages'
1962
1845
  };
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
1846
+ request.reasoningEffort && (requestBody.reasoning = {
1847
+ effort: request.reasoningEffort,
1848
+ summary: "auto"
1849
+ // Enable automatic reasoning summaries
1850
+ });
1851
+ const response = await fetch("https://api.openai.com/v1/responses", {
1852
+ method: "POST",
1853
+ headers: {
1854
+ "Content-Type": "application/json",
1855
+ Authorization: `Bearer ${apiKey}`
1975
1856
  },
1976
- toLanguage: {
1977
- id: targetLanguage,
1978
- title: targetLanguage
1979
- }
1857
+ body: JSON.stringify(requestBody)
1858
+ });
1859
+ if (!response.ok) {
1860
+ const error = await response.json();
1861
+ throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
1862
+ }
1863
+ const result = await response.json(), tokensUsed = {
1864
+ prompt: result.usage?.input_tokens || 0,
1865
+ completion: result.usage?.output_tokens || 0,
1866
+ reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
1867
+ total: result.usage?.total_tokens || 0
1868
+ }, cost = calculateCost(tokensUsed, model);
1869
+ let content = "";
1870
+ if (result.output && Array.isArray(result.output)) {
1871
+ const messageItem = result.output.find((item) => item.type === "message");
1872
+ messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
1873
+ }
1874
+ return content || (content = result.output_text || result.text || ""), {
1875
+ content,
1876
+ tokensUsed,
1877
+ cost
1980
1878
  };
1981
- }, callGPT5 = async (apiKey, request) => {
1982
- const model = request.model || "gpt-5", isGPT5 = model === "gpt-5", useStreaming = !!request.onReasoningChunk, requestBody = {
1879
+ }, callChatCompletionsAPI = async (apiKey, request) => {
1880
+ const model = request.model || "gpt-4o", requestBody = {
1983
1881
  model,
1984
1882
  messages: request.messages,
1985
- stream: useStreaming
1986
- };
1987
- isGPT5 || (requestBody.temperature = request.temperature ?? 0.3), isGPT5 && (requestBody.reasoning_effort = request.reasoningEffort || "medium", requestBody.verbosity = request.verbosity || "medium");
1988
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
1883
+ temperature: request.temperature ?? 0.3
1884
+ }, response = await fetch("https://api.openai.com/v1/chat/completions", {
1989
1885
  method: "POST",
1990
1886
  headers: {
1991
1887
  "Content-Type": "application/json",
@@ -1995,88 +1891,30 @@ const fetchTranslationPrompts = async ({
1995
1891
  });
1996
1892
  if (!response.ok) {
1997
1893
  const error = await response.json();
1998
- throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
1894
+ throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
1999
1895
  }
2000
- if (useStreaming && response.body && request.onReasoningChunk)
2001
- return await handleStreamingResponse(response, request.onReasoningChunk, model);
2002
1896
  const result = await response.json(), usage = result.usage || {}, tokensUsed = {
2003
1897
  prompt: usage.prompt_tokens || 0,
2004
1898
  completion: usage.completion_tokens || 0,
2005
1899
  reasoning: usage.reasoning_tokens || 0,
2006
- // New in GPT-5
2007
1900
  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);
1901
+ }, cost = calculateCost(tokensUsed, model);
2059
1902
  return {
2060
- content: contentBuffer || fullContent,
1903
+ content: result.choices[0].message.content || "",
2061
1904
  tokensUsed,
2062
- cost,
2063
- reasoningSummary: reasoningBuffer || void 0
1905
+ cost
2064
1906
  };
2065
- }, calculateGPT5Cost = (tokens, model) => {
1907
+ }, calculateCost = (tokens, model) => {
2066
1908
  const pricing = {
2067
1909
  "gpt-5": {
2068
1910
  input: 2e-6,
2069
- // $0.002 per 1k input tokens
2070
1911
  output: 1e-5,
2071
- // $0.010 per 1k output tokens
2072
1912
  reasoning: 5e-6
2073
- // $0.005 per 1k reasoning tokens
2074
1913
  },
2075
1914
  "gpt-4o": {
2076
1915
  input: 5e-6,
2077
1916
  output: 14999999999999999e-21,
2078
1917
  reasoning: 0
2079
- // No reasoning tokens in GPT-4
2080
1918
  },
2081
1919
  "gpt-4-turbo": {
2082
1920
  input: 1e-5,
@@ -2096,9 +1934,52 @@ const fetchTranslationPrompts = async ({
2096
1934
  }, parseReviewFormat = (text) => {
2097
1935
  const root = [], stack = [], lines = text.split(`
2098
1936
  `);
2099
- for (const line of lines) {
2100
- const trimmed = line.trim();
1937
+ for (let i = 0; i < lines.length; i++) {
1938
+ const trimmed = lines[i].trim();
2101
1939
  if (!trimmed) continue;
1940
+ const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
1941
+ if (compactStringMatch && !trimmed.includes("<")) {
1942
+ const [, fieldName, text2] = compactStringMatch, node = {
1943
+ type: "field",
1944
+ name: fieldName,
1945
+ text: unescapeXml(text2),
1946
+ attributes: {},
1947
+ children: []
1948
+ };
1949
+ if (stack.length > 0) {
1950
+ const parent = stack[stack.length - 1];
1951
+ parent && parent.children.push(node);
1952
+ } else
1953
+ root.push(node);
1954
+ continue;
1955
+ }
1956
+ const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
1957
+ if (compactStartMatch) {
1958
+ const fieldName = compactStartMatch[1], contentLines = [];
1959
+ for (i++; i < lines.length; ) {
1960
+ const currentLine = lines[i];
1961
+ if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
1962
+ break;
1963
+ contentLines.push(currentLine), i++;
1964
+ }
1965
+ const content = contentLines.join(`
1966
+ `);
1967
+ let contentType = "translatable.vtt.captions";
1968
+ (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
1969
+ const node = {
1970
+ type: "field",
1971
+ name: fieldName,
1972
+ text: content,
1973
+ attributes: { type: contentType },
1974
+ children: []
1975
+ };
1976
+ if (stack.length > 0) {
1977
+ const parent = stack[stack.length - 1];
1978
+ parent && parent.children.push(node);
1979
+ } else
1980
+ root.push(node);
1981
+ continue;
1982
+ }
2102
1983
  const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
2103
1984
  if (openMatch) {
2104
1985
  const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
@@ -2121,7 +2002,21 @@ const fetchTranslationPrompts = async ({
2121
2002
  parent && parent.children.push(node);
2122
2003
  } else
2123
2004
  root.push(node);
2124
- else
2005
+ else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
2006
+ const contentLines = [];
2007
+ for (i++; i < lines.length; ) {
2008
+ const currentLine = lines[i];
2009
+ if (currentLine.trim().match(/^<\/field>$/))
2010
+ break;
2011
+ contentLines.push(currentLine), i++;
2012
+ }
2013
+ if (node.text = contentLines.join(`
2014
+ `), stack.length > 0) {
2015
+ const parent = stack[stack.length - 1];
2016
+ parent && parent.children.push(node);
2017
+ } else
2018
+ root.push(node);
2019
+ } else
2125
2020
  stack.push(node);
2126
2021
  continue;
2127
2022
  }
@@ -2149,6 +2044,16 @@ const fetchTranslationPrompts = async ({
2149
2044
  applyFieldChanges(doc, path, node);
2150
2045
  }
2151
2046
  }, applyFieldChanges = (doc, path, node) => {
2047
+ if (node.attributes.type === "translatable.vtt.captions") {
2048
+ const fieldValue = get(doc, path);
2049
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
2050
+ return;
2051
+ }
2052
+ if (node.attributes.type === "translatable.richtext") {
2053
+ const fieldValue = get(doc, path);
2054
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
2055
+ return;
2056
+ }
2152
2057
  if (node.attributes.type === "array") {
2153
2058
  node.children.forEach((child, idx) => {
2154
2059
  applyFieldChanges(doc, [...path, idx], child);
@@ -2215,7 +2120,7 @@ const fetchTranslationPrompts = async ({
2215
2120
  if (path.length === 0) {
2216
2121
  if (isPlainObject(value))
2217
2122
  for (const [key, val] of Object.entries(value))
2218
- !key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
2123
+ !key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
2219
2124
  return;
2220
2125
  }
2221
2126
  const fieldName = path[path.length - 1];
@@ -2228,6 +2133,14 @@ const fetchTranslationPrompts = async ({
2228
2133
  return;
2229
2134
  }
2230
2135
  if (isPlainObject(value)) {
2136
+ if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
2137
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2138
+ return;
2139
+ }
2140
+ if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
2141
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2142
+ return;
2143
+ }
2231
2144
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2232
2145
  for (const [key, val] of Object.entries(value))
2233
2146
  key.startsWith("_") || serialize(val, [...path, key], indent + 1);
@@ -2235,7 +2148,7 @@ const fetchTranslationPrompts = async ({
2235
2148
  return;
2236
2149
  }
2237
2150
  if (typeof value == "string") {
2238
- value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2151
+ value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
2239
2152
  return;
2240
2153
  }
2241
2154
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2275,499 +2188,132 @@ const fetchTranslationPrompts = async ({
2275
2188
  lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2276
2189
  return;
2277
2190
  }
2278
- (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
2279
- }, serializeSpan = (span, _path, indent, lines) => {
2280
- const indentStr = " ".repeat(indent);
2281
- if (!span || typeof span != "object") return;
2282
- const metadata = {};
2283
- span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
2284
- const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
2285
- 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
2191
+ (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
2192
+ }, serializeSpan = (span, _path, indent, lines) => {
2193
+ const indentStr = " ".repeat(indent);
2194
+ if (!span || typeof span != "object") return;
2195
+ const metadata = {};
2196
+ span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
2197
+ const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
2198
+ metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
2199
+ }, 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) => {
2200
+ const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
2201
+ for (const field of criticalFields)
2202
+ original[field] !== modified[field] && errors.push({
2203
+ path: [field],
2204
+ expected: original[field],
2205
+ actual: modified[field],
2206
+ type: "metadata_changed"
2676
2207
  });
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
- }
2208
+ return validateStructureRecursive(original, modified, [], errors), {
2209
+ valid: errors.length === 0,
2210
+ errors
2211
+ };
2212
+ }, validateStructureRecursive = (original, modified, path, errors) => {
2213
+ if (typeof original != typeof modified) {
2214
+ errors.push({
2215
+ path,
2216
+ expected: typeof original,
2217
+ actual: typeof modified,
2218
+ type: "type_mismatch"
2690
2219
  });
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
2220
+ return;
2221
+ }
2222
+ if (Array.isArray(original)) {
2223
+ if (!Array.isArray(modified)) {
2224
+ errors.push({
2225
+ path,
2226
+ expected: "array",
2227
+ actual: typeof modified,
2228
+ type: "type_mismatch"
2229
+ });
2230
+ return;
2231
+ }
2232
+ if (original.length !== modified.length) {
2233
+ errors.push({
2234
+ path,
2235
+ expected: `length ${original.length}`,
2236
+ actual: `length ${modified.length}`,
2237
+ type: "type_mismatch"
2238
+ });
2239
+ return;
2240
+ }
2241
+ original.forEach((item, idx) => {
2242
+ validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
2704
2243
  });
2705
2244
  return;
2706
2245
  }
2707
- }, translateInPlaceWithOpenAI = async ({
2246
+ if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
2247
+ const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
2248
+ for (const key of origKeys)
2249
+ key in modified || errors.push({
2250
+ path: [...path, key],
2251
+ expected: "present",
2252
+ actual: "missing",
2253
+ type: "missing"
2254
+ });
2255
+ for (const key of modKeys)
2256
+ key in original || errors.push({
2257
+ path: [...path, key],
2258
+ expected: "absent",
2259
+ actual: "present",
2260
+ type: "extra"
2261
+ });
2262
+ const origObj = original, modObj = modified;
2263
+ for (const key of origKeys)
2264
+ key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
2265
+ path: [...path, key],
2266
+ expected: origObj[key],
2267
+ actual: modObj[key],
2268
+ type: "metadata_changed"
2269
+ }) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
2270
+ }
2271
+ }, getOpenAITranslation = async ({
2708
2272
  dispatch,
2709
2273
  sourceDocument,
2710
- targetDocument,
2711
- targetLanguage,
2712
2274
  languageFieldName,
2713
2275
  translationPrompts,
2714
- openaiConfig,
2715
- isReview
2276
+ targetLanguage,
2277
+ openaiConfig
2716
2278
  }) => {
2717
2279
  const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2718
2280
  sourceId: sourceDocument._id,
2719
- targetId: targetDocument._id,
2720
- targetLanguage,
2721
- sourceLanguage,
2722
- isReview
2281
+ sourceLanguage
2723
2282
  };
2724
2283
  try {
2725
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2726
- devLog.debug("Documents serialized for OpenAI", {
2284
+ const sourceText = serializeDocument(sourceDocument);
2285
+ devLog.info("Serialized document for OpenAI", {
2727
2286
  ...logContext,
2728
- sourceLength: sourceText.length,
2729
- targetLength: targetText.length
2287
+ characterCount: sourceText.length,
2288
+ estimatedTokens: Math.ceil(sourceText.length / 4),
2289
+ preview: sourceText.substring(0, 500)
2730
2290
  });
2731
- const prompt = isReview ? buildReviewPrompt({
2732
- sourceText,
2733
- translatedText: targetText,
2734
- sourceLanguage,
2735
- targetLanguage,
2736
- translationPrompts
2737
- }) : buildTranslationPrompt({
2291
+ const prompt = buildTranslationPrompt$1({
2738
2292
  sourceText,
2739
2293
  sourceLanguage,
2740
2294
  targetLanguage,
2741
2295
  translationPrompts
2742
- });
2743
- devLog.debug("Calling GPT-5", {
2744
- ...logContext,
2745
- promptLength: prompt.length
2746
- });
2747
- const response = await callGPT5(openaiConfig.apiKey, {
2296
+ }), response = await callGPT5(openaiConfig.apiKey, {
2748
2297
  messages: [
2749
2298
  {
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."
2299
+ role: "developer",
2300
+ content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
2752
2301
  },
2753
2302
  { role: "user", content: prompt }
2754
2303
  ],
2755
2304
  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
- }
2305
+ reasoningEffort: openaiConfig.reasoningEffort
2764
2306
  });
2765
- devLog.info("OpenAI operation completed", {
2307
+ devLog.info("OpenAI translation response received", {
2766
2308
  ...logContext,
2767
- tokensUsed: response.tokensUsed.total,
2768
- cost: response.cost
2309
+ tokensUsed: response.tokensUsed,
2310
+ cost: response.cost,
2311
+ responseLength: response.content.length,
2312
+ responsePreview: response.content.substring(0, 500)
2769
2313
  });
2770
- const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
2314
+ const result = deserializeDocument(sourceDocument, response.content);
2315
+ result[languageFieldName] = targetLanguage;
2316
+ const validation = validateStructureIntegrity(sourceDocument, result);
2771
2317
  if (!validation.valid) {
2772
2318
  devLog.error("Structure validation failed", {
2773
2319
  ...logContext,
@@ -2781,17 +2327,14 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2781
2327
  `Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
2782
2328
  );
2783
2329
  }
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, {
2330
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2787
2331
  tokensUsed: response.tokensUsed.total,
2788
- estimatedCost: response.cost,
2789
- reasoningSummary: response.reasoningSummary
2332
+ estimatedCost: response.cost
2790
2333
  }), result;
2791
2334
  } catch (error) {
2792
2335
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2793
2336
  }
2794
- }, buildTranslationPrompt = ({
2337
+ }, buildTranslationPrompt$1 = ({
2795
2338
  sourceText,
2796
2339
  sourceLanguage,
2797
2340
  targetLanguage,
@@ -2838,73 +2381,193 @@ Brief summary of key translation decisions and reasoning
2838
2381
  </reasoning>
2839
2382
 
2840
2383
  Then return the translated document:`;
2841
- }, buildReviewPrompt = ({
2842
- sourceText,
2843
- translatedText,
2384
+ }, assembleLanguageSpecs = ({
2385
+ sourceDocument,
2386
+ languageFieldName,
2387
+ targetLanguage
2388
+ }) => {
2389
+ const sourceLanugageId = sourceDocument[languageFieldName];
2390
+ if (!sourceLanugageId)
2391
+ throw new Error("[assembleLanguages] Source language ID is missing");
2392
+ return {
2393
+ fromLanguage: {
2394
+ id: sourceLanugageId,
2395
+ title: sourceLanugageId
2396
+ },
2397
+ toLanguage: {
2398
+ id: targetLanguage,
2399
+ title: targetLanguage
2400
+ }
2401
+ };
2402
+ }, buildTranslationPrompt = ({
2844
2403
  sourceLanguage,
2845
2404
  targetLanguage,
2846
- translationPrompts
2405
+ components,
2406
+ isUpdate = !1
2847
2407
  }) => {
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
2408
+ const sections = [];
2409
+ 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));
2410
+ const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2411
+ return styleSection && sections.push(styleSection), sections.join(`
2854
2412
 
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?
2413
+ ---
2861
2414
 
2862
- ## Guidelines
2415
+ `);
2416
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2863
2417
 
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
- ` : ""}
2418
+ - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2419
+ - Scan each field once, then move directly into translation work.
2420
+ - Plan updates before editing so document structure remains intact.
2421
+ - Default to medium reasoning effort and escalate only for ambiguous passages.
2422
+ - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
2423
+ const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2424
+ return `# CRITICAL TRANSLATION RULES
2876
2425
 
2877
- ## Source Document (${sourceLanguage})
2426
+ Follow these without exception:
2878
2427
 
2879
- \`\`\`
2880
- ${sourceText}
2881
- \`\`\`
2428
+ 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2429
+ 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2430
+ 3. Maintain all layout, markdown, rich-text marks, and data structure.
2431
+ 4. Never invent metadata or field values that are absent in the source.
2432
+ 5. Leave technical tokens (code, IDs, filenames) untouched.
2433
+ 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2434
+ }, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
2882
2435
 
2883
- ## Current Translation (${targetLanguage})
2436
+ This document already has a translation; only adjust fields that changed.
2884
2437
 
2885
- \`\`\`
2886
- ${translatedText}
2887
- \`\`\`
2438
+ - Compare the latest source to the existing target text.
2439
+ - Keep prior terminology and style unless the source demands a change.
2440
+ - Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2888
2441
 
2889
- ## Instructions
2442
+ Apply these mappings immediately after protected phrases and before style preferences.
2890
2443
 
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
2444
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2897
2445
 
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>
2446
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2902
2447
 
2903
- Then return the corrected translation document:`;
2448
+ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2449
+ const components = [];
2450
+ return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2451
+ "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2452
+ ), 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(`
2453
+ `) : null;
2454
+ }, getSanityAgentTranslation = async ({
2455
+ sourceDocument,
2456
+ targetLanguage,
2457
+ languageFieldName,
2458
+ schemaId,
2459
+ client,
2460
+ translationPrompts
2461
+ }) => {
2462
+ const styleGuide = buildTranslationPrompt({
2463
+ sourceLanguage: sourceDocument[languageFieldName],
2464
+ targetLanguage,
2465
+ isUpdate: !1,
2466
+ // Always fresh translation in a version
2467
+ components: {
2468
+ globalPrompt: translationPrompts.globalPrompt,
2469
+ localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2470
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2471
+ protectedPhrases: translationPrompts.protectedPhrases
2472
+ }
2473
+ }), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2474
+ sourceDocument,
2475
+ languageFieldName,
2476
+ targetLanguage
2477
+ });
2478
+ return await client.agent.action.translate({
2479
+ schemaId,
2480
+ toLanguage,
2481
+ fromLanguage,
2482
+ documentId: sourceDocument._id,
2483
+ noWrite: !0,
2484
+ // Do not write to the document, just return the translated content
2485
+ languageFieldPath: [languageFieldName],
2486
+ styleGuide,
2487
+ protectedPhrases: translationPrompts.protectedPhrases,
2488
+ conditionalPaths: {
2489
+ defaultHidden: !1,
2490
+ defaultReadOnly: !1
2491
+ }
2492
+ });
2493
+ }, handleTranslation = async ({
2494
+ dispatch,
2495
+ client,
2496
+ schemaId,
2497
+ sourceDocument,
2498
+ targetLanguage,
2499
+ existingTranslationId,
2500
+ languageFieldName,
2501
+ translationPrompts,
2502
+ translationMode,
2503
+ openaiConfig,
2504
+ releaseId
2505
+ }) => {
2506
+ devLog.debug("Applying field transforms before translation", {
2507
+ documentId: sourceDocument._id,
2508
+ documentType: sourceDocument._type
2509
+ });
2510
+ const { transformedDocument, originalStructure } = applyTransforms({
2511
+ document: sourceDocument,
2512
+ registry: globalTransformRegistry,
2513
+ schemaType: sourceDocument._type
2514
+ });
2515
+ let translatedDoc;
2516
+ switch (translationMode) {
2517
+ case "sanity-only": {
2518
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2519
+ sourceDocument: transformedDocument,
2520
+ targetLanguage,
2521
+ languageFieldName,
2522
+ schemaId,
2523
+ client,
2524
+ translationPrompts
2525
+ });
2526
+ break;
2527
+ }
2528
+ case "openai-only": {
2529
+ if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2530
+ throw new Error("OpenAI is not configured but openai-only mode was selected");
2531
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2532
+ dispatch,
2533
+ sourceDocument: transformedDocument,
2534
+ targetLanguage,
2535
+ languageFieldName,
2536
+ translationPrompts,
2537
+ openaiConfig
2538
+ });
2539
+ break;
2540
+ }
2541
+ case "sanity-with-review":
2542
+ throw new Error("Sanity with review translation method is not implemented");
2543
+ default:
2544
+ throw new Error(`Invalid translation mode: ${translationMode}`);
2545
+ }
2546
+ if (!translatedDoc)
2547
+ return null;
2548
+ devLog.debug("Restoring field transforms after translation", {
2549
+ documentId: sourceDocument._id,
2550
+ documentType: sourceDocument._type
2551
+ });
2552
+ const finalDoc = restoreTransforms({
2553
+ translatedDocument: translatedDoc,
2554
+ originalStructure,
2555
+ registry: globalTransformRegistry,
2556
+ schemaType: sourceDocument._type
2557
+ }), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2558
+ ...finalDoc,
2559
+ _id: targetDocumentVersionId
2560
+ };
2561
+ return await client.action({
2562
+ actionType: "sanity.action.document.version.create",
2563
+ publishedId: targetDocumentPublishedId,
2564
+ document: newDoc
2565
+ }), newDoc;
2904
2566
  }, translateDocument = async ({
2905
2567
  dispatch,
2906
2568
  client,
2907
2569
  sourceDocument,
2570
+ sourceLanguage,
2908
2571
  targetLanguage,
2909
2572
  languageFieldName,
2910
2573
  shouldOverwriteFreshTranslations,
@@ -2923,62 +2586,37 @@ Then return the corrected translation document:`;
2923
2586
  };
2924
2587
  devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2925
2588
  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
2589
  const translationMetadata = await getTranslationMetadata({
2937
2590
  client,
2938
2591
  sourceDocument,
2939
2592
  sourceLanguage
2940
2593
  }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
2941
2594
  (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", {
2595
+ );
2596
+ if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2597
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2949
2598
  targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
2950
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2951
- return;
2952
- }
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({
2599
+ });
2600
+ return;
2601
+ }
2602
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2963
2603
  dispatch,
2964
2604
  client,
2965
2605
  schemaId,
2966
2606
  sourceDocument,
2967
2607
  targetLanguage,
2608
+ existingTranslationId,
2968
2609
  languageFieldName,
2969
- translationTarget,
2970
2610
  translationPrompts,
2971
2611
  translationMode,
2972
2612
  openaiConfig,
2973
- releaseId,
2974
- isNewTranslation
2613
+ releaseId
2975
2614
  });
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", {
2615
+ if (!newDocument) {
2616
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2617
+ return;
2618
+ }
2619
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2982
2620
  targetId: newDocument._id
2983
2621
  }), await updateTranslationMetadata({
2984
2622
  client,
@@ -2987,8 +2625,10 @@ Then return the corrected translation document:`;
2987
2625
  newDocument,
2988
2626
  targetLanguage,
2989
2627
  translationMetadata,
2990
- isNewTranslation
2991
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"), devLog.info("Translation completed successfully", {
2628
+ isNewTranslation: !existingTranslationId
2629
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
2630
+ targetId: newDocument._id
2631
+ }), devLog.info("Translation completed successfully", {
2992
2632
  ...logContext,
2993
2633
  newDocumentId: newDocument._id
2994
2634
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
@@ -3014,77 +2654,61 @@ Then return the corrected translation document:`;
3014
2654
  }) => {
3015
2655
  const logContext = {
3016
2656
  documentId: sourceDocument._id,
2657
+ documentRev: sourceDocument._rev,
3017
2658
  targetLanguages,
3018
2659
  referencesCount: references.length,
3019
2660
  maxConcurrentJobs: maxConcurrentTranslationJobs,
3020
2661
  translationMode,
3021
2662
  shouldOverwriteFreshTranslations
3022
2663
  };
3023
- devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
3024
2664
  let releaseId;
3025
2665
  try {
3026
2666
  const sourceLanguage = sourceDocument[languageFieldName];
3027
2667
  if (!sourceLanguage)
3028
2668
  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({
2669
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
3033
2670
  type: "SET_OVERALL_STATUS",
3034
2671
  payload: "translating"
3035
2672
  });
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({
2673
+ const releaseTitle = `[AI Translation] ${sourceDocument.title || sourceDocument._id.split("-").slice(0, 2).join("-")} (${sourceDocument._type}) ${sourceLanguage} \u2192 ${targetLanguages.join(", ")}`, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`, { releaseId: releaseId2 } = await client.releases.create({
3042
2674
  metadata: {
3043
2675
  title: releaseTitle,
3044
2676
  description: releaseDescription,
3045
- releaseType: "asap"
2677
+ releaseType: "undecided"
3046
2678
  }
3047
2679
  });
3048
- releaseId = newReleaseId, devLog.info("Content Release created", {
2680
+ devLog.info("Content Release created", {
3049
2681
  ...logContext,
3050
- releaseId
2682
+ releaseId: releaseId2
3051
2683
  });
3052
2684
  const sourceDocuments = [
3053
2685
  {
3054
2686
  _id: sourceDocument._id,
3055
2687
  _type: sourceDocument._type,
3056
- // TODO: Handle this properly
2688
+ // TODO: Extract document title more robustly across schema types
3057
2689
  title: sourceDocument.name || sourceDocument.title
3058
2690
  },
3059
2691
  ...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({
2692
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
3067
2693
  client,
3068
2694
  sourceLanguage,
3069
2695
  targetLanguages,
3070
2696
  sourceDocument
3071
2697
  });
3072
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2698
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
3073
2699
  const translationTasks = sourceDocumentIds.flatMap(
3074
2700
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3075
2701
  try {
3076
2702
  await translationSemaphore.withPermit(async () => {
3077
2703
  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({
2704
+ perspective: "drafts"
2705
+ }).getDocument(documentId);
2706
+ if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2707
+ const versionedTargetDocumentId = await translateDocument({
3085
2708
  dispatch,
3086
2709
  client,
3087
2710
  sourceDocument: document,
2711
+ sourceLanguage,
3088
2712
  targetLanguage,
3089
2713
  languageFieldName,
3090
2714
  shouldOverwriteFreshTranslations,
@@ -3092,9 +2716,9 @@ Then return the corrected translation document:`;
3092
2716
  translationPrompts,
3093
2717
  translationMode,
3094
2718
  openaiConfig,
3095
- releaseId
2719
+ releaseId: releaseId2
3096
2720
  });
3097
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
2721
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
3098
2722
  }
3099
2723
  });
3100
2724
  } catch (error) {
@@ -3110,25 +2734,21 @@ Then return the corrected translation document:`;
3110
2734
  }
3111
2735
  })
3112
2736
  ), 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", {
2737
+ devLog.info("Translation tasks completed", {
3114
2738
  ...logContext,
3115
2739
  total: results.length,
3116
2740
  succeeded,
3117
- failed
3118
- }), devLog.info("All translation tasks completed", {
3119
- ...logContext,
3120
- targetDocumentIds: targetDocumentIds.length
2741
+ failed,
2742
+ targetDocuments: versionedTargetDocumentIds.length
3121
2743
  }), dispatch({
3122
2744
  type: "SET_OVERALL_STATUS",
3123
2745
  payload: "linking_references"
3124
- }), devLog.debug("Starting reference localization", {
3125
- ...logContext,
3126
- documentsToLocalize: targetDocumentIds.length
3127
2746
  }), await Promise.all(
3128
- targetDocumentIds.map(async (documentId) => {
2747
+ versionedTargetDocumentIds.map(async (documentId) => {
3129
2748
  await localizeReferences({
3130
2749
  client,
3131
2750
  documentId,
2751
+ sourceLanguage,
3132
2752
  languageFieldName
3133
2753
  });
3134
2754
  })
@@ -3138,26 +2758,13 @@ Then return the corrected translation document:`;
3138
2758
  ...logContext,
3139
2759
  succeeded,
3140
2760
  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();
2761
+ }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
3142
2762
  } catch (error) {
3143
- if (devLog.error(
2763
+ throw devLog.error(
3144
2764
  "translateDocumentSet failed with unexpected error",
3145
2765
  error instanceof Error ? error : new Error(String(error)),
3146
2766
  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;
2767
+ ), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
3161
2768
  }
3162
2769
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
3163
2770
  const {
@@ -3578,24 +3185,12 @@ Then return the corrected translation document:`;
3578
3185
  " \u2014 ",
3579
3186
  job._id.slice(-6)
3580
3187
  ] }),
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
- ] }) })
3587
- ] }),
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
- ] }) })
3188
+ !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3189
+ job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3190
+ job.tokensUsed.toLocaleString(),
3191
+ " tokens (~$",
3192
+ job.estimatedCost?.toFixed(4),
3193
+ ")"
3599
3194
  ] }) : null,
3600
3195
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3601
3196
  "Error: ",
@@ -3709,26 +3304,9 @@ Then return the corrected translation document:`;
3709
3304
  "completed"
3710
3305
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3711
3306
  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", {
3307
+ return STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen }) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationStatusPanel, {}) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? /* @__PURE__ */ jsx("div", { children: "Error occurred" }) : (devLog.warn("Unhandled translation dialog state", {
3721
3308
  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"
3309
+ overallStatus
3732
3310
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3733
3311
  }, getTranslateAction = (config) => ({ draft, published }) => {
3734
3312
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
@@ -3755,7 +3333,427 @@ Then return the corrected translation document:`;
3755
3333
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3756
3334
  }
3757
3335
  } : (devLog.error("Missing config in TranslateAction"), null);
3758
- }, aiTranslationPlugin = definePlugin((config) => ({
3336
+ }, translationGlossaryTerm = defineType({
3337
+ name: "translationGlossaryTerm",
3338
+ title: "Translation Glossary Term",
3339
+ icon: TranslateIcon,
3340
+ type: "document",
3341
+ fields: [
3342
+ defineField({
3343
+ name: "translations",
3344
+ title: "Translations",
3345
+ type: "internationalizedArrayString"
3346
+ })
3347
+ ],
3348
+ preview: {
3349
+ select: {
3350
+ translations: "translations"
3351
+ },
3352
+ prepare({ translations }) {
3353
+ const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
3354
+ return {
3355
+ title: enTranslation?.value || firstTranslation?.value || "No translation available"
3356
+ };
3357
+ }
3358
+ }
3359
+ }), translationHistory = defineField({
3360
+ name: "translationHistory",
3361
+ type: "array",
3362
+ of: [
3363
+ {
3364
+ name: "targetLanguageHistory",
3365
+ type: "object",
3366
+ fields: [
3367
+ defineField({
3368
+ name: "language",
3369
+ title: "Language",
3370
+ description: "The language of the target document",
3371
+ type: "string"
3372
+ }),
3373
+ defineField({
3374
+ name: "sourceId",
3375
+ title: "Source ID",
3376
+ description: "The ID of the source document",
3377
+ type: "string"
3378
+ }),
3379
+ defineField({
3380
+ name: "sourceRev",
3381
+ title: "Source Revision",
3382
+ description: "The revision ID of the source document at the time of translation",
3383
+ type: "string"
3384
+ }),
3385
+ defineField({
3386
+ name: "sourceLanguage",
3387
+ title: "Source Language",
3388
+ description: "The language of the source document",
3389
+ type: "string"
3390
+ }),
3391
+ defineField({
3392
+ name: "sourceSnapshot",
3393
+ title: "Source Snapshot",
3394
+ description: "The snapshot of the source document at the time of translation",
3395
+ type: "text"
3396
+ }),
3397
+ defineField({
3398
+ name: "targetId",
3399
+ title: "Target ID",
3400
+ description: "The ID of the target document",
3401
+ type: "string"
3402
+ })
3403
+ ]
3404
+ }
3405
+ ]
3406
+ }), translationSettings = defineType({
3407
+ name: "translationSettings",
3408
+ title: "Translation Settings",
3409
+ icon: TranslateIcon,
3410
+ type: "document",
3411
+ groups: [
3412
+ {
3413
+ name: "general",
3414
+ title: "General",
3415
+ default: !0
3416
+ },
3417
+ {
3418
+ name: "history",
3419
+ title: "History"
3420
+ }
3421
+ ],
3422
+ fields: [
3423
+ defineField({
3424
+ name: "globalPrompt",
3425
+ title: "Global Translation Prompt",
3426
+ description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
3427
+ type: "text",
3428
+ rows: 5,
3429
+ group: "general"
3430
+ }),
3431
+ defineField({
3432
+ name: "localePrompts",
3433
+ title: "Locale Prompts",
3434
+ description: "Prompts that will be given to the Sanity AI Agent for each locale.",
3435
+ type: "internationalizedArrayText",
3436
+ group: "general"
3437
+ }),
3438
+ defineField({
3439
+ name: "protectedPhrases",
3440
+ title: "Protected Phrases",
3441
+ description: "Phrases that will never be translated.",
3442
+ type: "array",
3443
+ of: [{ type: "string" }],
3444
+ group: "general"
3445
+ }),
3446
+ defineField({
3447
+ name: "changeHistory",
3448
+ title: "Change History",
3449
+ description: "History of changes made via user feedback.",
3450
+ type: "array",
3451
+ readOnly: !0,
3452
+ group: "history",
3453
+ of: [
3454
+ {
3455
+ type: "object",
3456
+ fields: [
3457
+ defineField({
3458
+ name: "timestamp",
3459
+ title: "Timestamp",
3460
+ type: "datetime"
3461
+ }),
3462
+ defineField({
3463
+ name: "changeType",
3464
+ title: "Change Type",
3465
+ type: "string",
3466
+ options: {
3467
+ list: [
3468
+ { title: "Global Prompt", value: "globalPrompt" },
3469
+ { title: "Locale Prompt", value: "localePrompt" },
3470
+ { title: "Protected Phrase", value: "protectedPhrase" },
3471
+ { title: "Glossary Term", value: "glossaryTerm" }
3472
+ ]
3473
+ }
3474
+ }),
3475
+ defineField({
3476
+ name: "description",
3477
+ title: "Description",
3478
+ type: "text",
3479
+ rows: 3
3480
+ }),
3481
+ defineField({
3482
+ name: "locale",
3483
+ title: "Locale",
3484
+ type: "string",
3485
+ description: "Target locale for locale-specific changes"
3486
+ })
3487
+ ],
3488
+ preview: {
3489
+ select: {
3490
+ timestamp: "timestamp",
3491
+ changeType: "changeType",
3492
+ description: "description"
3493
+ },
3494
+ prepare({ timestamp, changeType, description }) {
3495
+ return {
3496
+ title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
3497
+ subtitle: description
3498
+ };
3499
+ }
3500
+ }
3501
+ }
3502
+ ]
3503
+ })
3504
+ ],
3505
+ preview: {
3506
+ prepare: () => ({
3507
+ title: "Translation Settings"
3508
+ })
3509
+ }
3510
+ });
3511
+ function isPortableText(value) {
3512
+ return Array.isArray(value) && value.some((item) => item?._type === "block");
3513
+ }
3514
+ const richTextTransform = {
3515
+ // Matcher combines field name pattern with value-based detection
3516
+ // fieldPath pattern acts as a pre-filter for performance
3517
+ // valueMatcher definitively checks if the value is actually Portable Text
3518
+ matcher: {
3519
+ // Pre-filter: Match all fields to enable detection based solely on value
3520
+ fieldPath: /.*/,
3521
+ // Definitive check: Verify the value is actually Portable Text
3522
+ valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
3523
+ },
3524
+ toTranslatable: (value, context) => {
3525
+ if (!Array.isArray(value) || !isPortableText(value))
3526
+ return {
3527
+ _type: "translatable.richtext",
3528
+ translationText: JSON.stringify(value),
3529
+ _originalValue: value
3530
+ };
3531
+ devLog.info("Applying rich text transform with XML serialization", {
3532
+ fieldPath: context.fieldPath,
3533
+ blockCount: value.length
3534
+ });
3535
+ try {
3536
+ const tempDoc = {
3537
+ _id: "temp",
3538
+ _type: "richTextWrapper",
3539
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3540
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3541
+ _rev: "temp",
3542
+ content: value
3543
+ }, serialized = serializeDocument(tempDoc);
3544
+ devLog.debug("Serialized rich text document", {
3545
+ fieldPath: context.fieldPath,
3546
+ serializedLength: serialized.length,
3547
+ preview: serialized.substring(0, 200)
3548
+ });
3549
+ const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
3550
+ if (match?.[1]) {
3551
+ const result = {
3552
+ _type: "translatable.richtext",
3553
+ translationText: match[1].trim(),
3554
+ _originalValue: value
3555
+ };
3556
+ return devLog.debug("Successfully extracted and wrapped rich text", {
3557
+ fieldPath: context.fieldPath,
3558
+ translationTextLength: result.translationText.length
3559
+ }), result;
3560
+ }
3561
+ return devLog.warn("Failed to serialize rich text content - regex did not match", {
3562
+ fieldPath: context.fieldPath,
3563
+ serializedPreview: serialized.substring(0, 500)
3564
+ }), {
3565
+ _type: "translatable.richtext",
3566
+ translationText: JSON.stringify(value),
3567
+ _originalValue: value
3568
+ };
3569
+ } catch (error) {
3570
+ return devLog.error("Error serializing rich text", error, {
3571
+ fieldPath: context.fieldPath
3572
+ }), {
3573
+ _type: "translatable.richtext",
3574
+ translationText: JSON.stringify(value),
3575
+ _originalValue: value
3576
+ };
3577
+ }
3578
+ },
3579
+ fromTranslatable: (translatedValue, originalValue, context) => {
3580
+ if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
3581
+ return devLog.debug("Value is not a translatable rich text object, returning original", {
3582
+ fieldPath: context.fieldPath,
3583
+ hasCorrectType: translatedValue._type === "translatable.richtext"
3584
+ }), originalValue;
3585
+ devLog.info("Deserializing rich text content", {
3586
+ fieldPath: context.fieldPath,
3587
+ translationTextLength: translatedValue.translationText.length
3588
+ });
3589
+ try {
3590
+ const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
3591
+ ${translationText}
3592
+ </field>`;
3593
+ devLog.debug("Prepared wrapped serialized content", {
3594
+ fieldPath: context.fieldPath,
3595
+ wrappedLength: wrappedSerialized.length,
3596
+ preview: wrappedSerialized.substring(0, 300)
3597
+ });
3598
+ const tempOriginalDoc = {
3599
+ _id: "temp",
3600
+ _type: "richTextWrapper",
3601
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3602
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3603
+ _rev: "temp",
3604
+ content: originalValue
3605
+ }, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
3606
+ return devLog.debug("Deserialization result", {
3607
+ fieldPath: context.fieldPath,
3608
+ hasContent: !!result.content,
3609
+ contentType: Array.isArray(result.content) ? "array" : typeof result.content,
3610
+ contentLength: Array.isArray(result.content) ? result.content.length : void 0
3611
+ }), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
3612
+ fieldPath: context.fieldPath,
3613
+ resultKeys: Object.keys(result)
3614
+ }), originalValue);
3615
+ } catch (error) {
3616
+ return devLog.error("Failed to deserialize rich text", error, {
3617
+ fieldPath: context.fieldPath
3618
+ }), originalValue;
3619
+ }
3620
+ }
3621
+ }, vttCaptionTransform = {
3622
+ matcher: {
3623
+ fieldName: "captions",
3624
+ schemaType: "video"
3625
+ },
3626
+ toTranslatable: (captionsArray, context) => {
3627
+ if (devLog.info("VTT captions toTranslatable - input:", {
3628
+ fieldPath: context.fieldPath,
3629
+ isArray: Array.isArray(captionsArray),
3630
+ count: Array.isArray(captionsArray) ? captionsArray.length : 0,
3631
+ firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
3632
+ }), !Array.isArray(captionsArray))
3633
+ return devLog.debug("VTT caption field is not an array, returning empty transform", {
3634
+ fieldPath: context.fieldPath,
3635
+ value: captionsArray
3636
+ }), {
3637
+ _type: "translatable.vtt.captions",
3638
+ translationText: "",
3639
+ _originalMetadata: []
3640
+ };
3641
+ devLog.info("Applying VTT caption hybrid transform", {
3642
+ fieldPath: context.fieldPath,
3643
+ count: captionsArray.length
3644
+ });
3645
+ const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
3646
+ const { text: _text, ...metadata } = caption;
3647
+ return {
3648
+ ...metadata,
3649
+ _originalIndex: index
3650
+ };
3651
+ });
3652
+ return devLog.info("VTT captions serialized:", {
3653
+ fieldPath: context.fieldPath,
3654
+ translationTextLength: translationText.length,
3655
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
3656
+ metadataCount: originalMetadata.length,
3657
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3658
+ }), {
3659
+ _type: "translatable.vtt.captions",
3660
+ translationText,
3661
+ _originalMetadata: originalMetadata
3662
+ };
3663
+ },
3664
+ fromTranslatable: (translatedValue, _originalValue, context) => {
3665
+ if (devLog.info("VTT captions fromTranslatable - input:", {
3666
+ fieldPath: context.fieldPath,
3667
+ valueType: typeof translatedValue,
3668
+ isCorrectType: translatedValue._type === "translatable.vtt.captions",
3669
+ hasText: !!translatedValue.translationText,
3670
+ hasMetadata: Array.isArray(translatedValue._originalMetadata)
3671
+ }), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
3672
+ return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
3673
+ fieldPath: context.fieldPath,
3674
+ valueType: typeof translatedValue,
3675
+ translatedValue
3676
+ }), [];
3677
+ devLog.info("Processing translated VTT captions", {
3678
+ fieldPath: context.fieldPath,
3679
+ metadataCount: translatedValue._originalMetadata.length
3680
+ });
3681
+ const { translationText, _originalMetadata } = translatedValue;
3682
+ devLog.info("VTT captions translationText:", {
3683
+ fieldPath: context.fieldPath,
3684
+ translationTextLength: translationText.length,
3685
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
3686
+ });
3687
+ const result = deserializeVTTCaptionText(translationText, _originalMetadata);
3688
+ return devLog.info("VTT captions final result:", {
3689
+ fieldPath: context.fieldPath,
3690
+ resultCount: result.length,
3691
+ resultExample: result.length > 0 ? result[0] : null
3692
+ }), result;
3693
+ }
3694
+ };
3695
+ function serializeVTTCaptionText(captions) {
3696
+ if (devLog.debug("serializeVTTCaptionText - input:", {
3697
+ captionsCount: captions.length,
3698
+ firstCaption: captions.length > 0 ? captions[0] : null
3699
+ }), !Array.isArray(captions) || captions.length === 0)
3700
+ return "";
3701
+ const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
3702
+ text: caption.text,
3703
+ hasText: typeof caption.text == "string"
3704
+ }), `[${index}] ${caption.text || ""}`)).join(`
3705
+ `);
3706
+ return devLog.debug("serializeVTTCaptionText - output:", {
3707
+ resultLength: result.length,
3708
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
3709
+ }), result;
3710
+ }
3711
+ function deserializeVTTCaptionText(translationText, originalMetadata) {
3712
+ if (devLog.debug("deserializeVTTCaptionText - input:", {
3713
+ translationTextLength: translationText.length,
3714
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
3715
+ metadataCount: originalMetadata.length,
3716
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3717
+ }), !translationText || translationText.trim() === "")
3718
+ return [];
3719
+ const result = [], lines = translationText.split(`
3720
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/;
3721
+ for (const line of lines) {
3722
+ const match = line.match(lineRegex);
3723
+ if (!match) {
3724
+ devLog.warn("Failed to parse caption line", { line });
3725
+ continue;
3726
+ }
3727
+ const index = Number.parseInt(match[1], 10), text = match[2];
3728
+ devLog.debug("Found caption:", {
3729
+ index,
3730
+ text
3731
+ });
3732
+ const metadata = originalMetadata.find((m) => m._originalIndex === index);
3733
+ if (metadata) {
3734
+ devLog.debug(`Found matching metadata for index ${index}`);
3735
+ const { _originalIndex, ...metadataWithoutIndex } = metadata;
3736
+ result.push({
3737
+ ...metadataWithoutIndex,
3738
+ text
3739
+ });
3740
+ } else
3741
+ devLog.warn("Missing metadata for caption", { index }), result.push({
3742
+ _type: "vtt.cue",
3743
+ _key: `caption-${index}`,
3744
+ text,
3745
+ startTime: 0,
3746
+ endTime: 0
3747
+ });
3748
+ }
3749
+ return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
3750
+ resultCount: result.length,
3751
+ resultExample: result.length > 0 ? result[0] : null
3752
+ }), result;
3753
+ }
3754
+ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
3755
+ globalTransformRegistry.register(transform);
3756
+ })), {
3759
3757
  name: "sanity-plugin-ai-translation",
3760
3758
  document: {
3761
3759
  actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
@@ -3766,6 +3764,8 @@ Then return the corrected translation document:`;
3766
3764
  })), translationHistorySchema = translationHistory;
3767
3765
  export {
3768
3766
  aiTranslationPlugin,
3769
- translationHistorySchema
3767
+ richTextTransform,
3768
+ translationHistorySchema,
3769
+ vttCaptionTransform
3770
3770
  };
3771
3771
  //# sourceMappingURL=index.js.map