@braze/sanity-plugin-ai-translation 0.2.1 → 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),
@@ -1922,70 +1674,214 @@ const fetchTranslationPrompts = async ({
1922
1674
  status
1923
1675
  })
1924
1676
  });
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
1962
- };
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
1975
- },
1976
- toLanguage: {
1977
- id: targetLanguage,
1978
- title: targetLanguage
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
+ }
1979
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'
1845
+ };
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}`
1856
+ },
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);
@@ -2178,10 +2083,27 @@ const fetchTranslationPrompts = async ({
2178
2083
  return;
2179
2084
  }
2180
2085
  const children = block.children;
2181
- Array.isArray(children) && blockNode.children.forEach((spanNode, idx) => {
2086
+ Array.isArray(children) && blockNode.children.some((c) => c.type === "span") ? blockNode.children.forEach((spanNode, idx) => {
2182
2087
  const childItem = children[idx];
2183
2088
  spanNode.type === "span" && childItem && applySpanChanges(childItem, spanNode);
2089
+ }) : blockNode.children.some((c) => c.type === "field") && blockNode.children.forEach((fieldNode) => {
2090
+ fieldNode.type === "field" && fieldNode.name && applyBlockFieldChanges(block, fieldNode.name, fieldNode);
2184
2091
  });
2092
+ }, applyBlockFieldChanges = (block, fieldName, fieldNode) => {
2093
+ const currentValue = block[fieldName];
2094
+ if (fieldNode.attributes.type === "array") {
2095
+ Array.isArray(currentValue) && fieldNode.children.forEach((childNode, idx) => {
2096
+ childNode.type === "block" && currentValue[idx] ? applyBlockChanges(currentValue[idx], childNode) : childNode.type === "field" && applyBlockFieldChanges(currentValue[idx], childNode.name || String(idx), childNode);
2097
+ });
2098
+ return;
2099
+ }
2100
+ if (fieldNode.attributes.type === "object") {
2101
+ currentValue && typeof currentValue == "object" && fieldNode.children.forEach((childNode) => {
2102
+ childNode.name && applyBlockFieldChanges(currentValue, childNode.name, childNode);
2103
+ });
2104
+ return;
2105
+ }
2106
+ fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text);
2185
2107
  }, applySpanChanges = (span, spanNode) => {
2186
2108
  if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
2187
2109
  devLog.warn("Span key mismatch, skipping", {
@@ -2198,7 +2120,7 @@ const fetchTranslationPrompts = async ({
2198
2120
  if (path.length === 0) {
2199
2121
  if (isPlainObject(value))
2200
2122
  for (const [key, val] of Object.entries(value))
2201
- !key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
2123
+ !key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
2202
2124
  return;
2203
2125
  }
2204
2126
  const fieldName = path[path.length - 1];
@@ -2211,6 +2133,14 @@ const fetchTranslationPrompts = async ({
2211
2133
  return;
2212
2134
  }
2213
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
+ }
2214
2144
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2215
2145
  for (const [key, val] of Object.entries(value))
2216
2146
  key.startsWith("_") || serialize(val, [...path, key], indent + 1);
@@ -2218,512 +2148,172 @@ const fetchTranslationPrompts = async ({
2218
2148
  return;
2219
2149
  }
2220
2150
  if (typeof value == "string") {
2221
- value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2222
- return;
2223
- }
2224
- (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
2225
- };
2226
- return serialize(doc), lines.join(`
2227
- `);
2228
- }, serializeBlock = (block, path, indent, lines) => {
2229
- const indentStr = " ".repeat(indent), metadata = {};
2230
- block._type && (metadata._type = block._type), block._key && (metadata._key = block._key), block.style && (metadata.style = block.style), block.listItem && (metadata.listItem = block.listItem), block.level && (metadata.level = block.level), block.markDefs && (metadata.markDefs = block.markDefs);
2231
- const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(typeof v == "string" ? v : JSON.stringify(v))}"`).join(" ");
2232
- lines.push(`${indentStr}<block ${metaStr}>`), Array.isArray(block.children) && block.children.forEach((child, idx) => {
2233
- serializeSpan(child, [...path, "children", idx], indent + 1, lines);
2234
- }), lines.push(`${indentStr}</block>`);
2235
- }, serializeSpan = (span, _path, indent, lines) => {
2236
- const indentStr = " ".repeat(indent);
2237
- if (!span || typeof span != "object") return;
2238
- const metadata = {};
2239
- span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
2240
- const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
2241
- metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
2242
- }, 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) => {
2243
- const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
2244
- for (const field of criticalFields)
2245
- original[field] !== modified[field] && errors.push({
2246
- path: [field],
2247
- expected: original[field],
2248
- actual: modified[field],
2249
- type: "metadata_changed"
2250
- });
2251
- return validateStructureRecursive(original, modified, [], errors), {
2252
- valid: errors.length === 0,
2253
- errors
2254
- };
2255
- }, validateStructureRecursive = (original, modified, path, errors) => {
2256
- if (typeof original != typeof modified) {
2257
- errors.push({
2258
- path,
2259
- expected: typeof original,
2260
- actual: typeof modified,
2261
- type: "type_mismatch"
2262
- });
2263
- return;
2264
- }
2265
- if (Array.isArray(original)) {
2266
- if (!Array.isArray(modified)) {
2267
- errors.push({
2268
- path,
2269
- expected: "array",
2270
- actual: typeof modified,
2271
- type: "type_mismatch"
2272
- });
2273
- return;
2274
- }
2275
- if (original.length !== modified.length) {
2276
- errors.push({
2277
- path,
2278
- expected: `length ${original.length}`,
2279
- actual: `length ${modified.length}`,
2280
- type: "type_mismatch"
2281
- });
2151
+ value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
2282
2152
  return;
2283
2153
  }
2284
- original.forEach((item, idx) => {
2285
- validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
2286
- });
2287
- return;
2288
- }
2289
- if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
2290
- const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
2291
- for (const key of origKeys)
2292
- key in modified || errors.push({
2293
- path: [...path, key],
2294
- expected: "present",
2295
- actual: "missing",
2296
- type: "missing"
2297
- });
2298
- for (const key of modKeys)
2299
- key in original || errors.push({
2300
- path: [...path, key],
2301
- expected: "absent",
2302
- actual: "present",
2303
- type: "extra"
2304
- });
2305
- const origObj = original, modObj = modified;
2306
- for (const key of origKeys)
2307
- key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
2308
- path: [...path, key],
2309
- expected: origObj[key],
2310
- actual: modObj[key],
2311
- type: "metadata_changed"
2312
- }) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
2313
- }
2314
- }, assembleTranslationPrompt = (context, components) => {
2315
- const sections = [
2316
- {
2317
- id: "agentDirectives",
2318
- content: buildAgentDirectivesSection(context),
2319
- required: !0,
2320
- priority: 0
2321
- },
2322
- {
2323
- id: "criticalRules",
2324
- content: buildCriticalRulesSection(context),
2325
- required: !0,
2326
- priority: 1
2327
- }
2328
- ];
2329
- components.glossaryTerms?.trim() && sections.push({
2330
- id: "glossaryTerms",
2331
- content: buildGlossarySection(components.glossaryTerms),
2332
- required: !1,
2333
- priority: 3
2334
- });
2335
- const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2336
- styleSection && sections.push({
2337
- id: "styleGuide",
2338
- content: styleSection,
2339
- required: !1,
2340
- priority: 4
2341
- }), sections.sort((a, b) => a.priority - b.priority);
2342
- let styleGuide = "";
2343
- const omittedSections = [], truncatedSections = [];
2344
- for (const section of sections) {
2345
- const prefix = styleGuide.length > 0 ? `
2346
-
2347
- ---
2348
-
2349
- ` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
2350
- if (availableCharacters <= 0) {
2351
- if (section.required) {
2352
- const truncatedContent = truncateSection(section.content, 0);
2353
- truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
2354
- } else
2355
- omittedSections.push(section.id);
2356
- continue;
2357
- }
2358
- if (section.content.length <= availableCharacters) {
2359
- styleGuide += prefix + section.content;
2360
- continue;
2361
- }
2362
- if (section.required) {
2363
- const truncatedContent = truncateSection(section.content, availableCharacters);
2364
- styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
2365
- } else
2366
- omittedSections.push(section.id);
2367
- }
2368
- return {
2369
- styleGuide,
2370
- omittedSections,
2371
- truncatedSections
2372
- };
2373
- }, truncateSection = (content, limit) => {
2374
- if (limit <= 0)
2375
- return "";
2376
- if (content.length <= limit)
2377
- return content;
2378
- if (limit <= 1)
2379
- return content.slice(0, limit);
2380
- const ellipsis = "\u2026";
2381
- if (limit <= ellipsis.length)
2382
- return content.slice(0, limit);
2383
- const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
2384
- return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
2385
- }, buildAgentDirectivesSection = (context) => {
2386
- const { targetLanguage, sourceLanguage } = context;
2387
- return `# AGENT WORKFLOW DIRECTIVES
2388
-
2389
- - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2390
- - Scan each field once, then move directly into translation work.
2391
- - Plan updates before editing so document structure remains intact.
2392
- - Default to medium reasoning effort and escalate only for ambiguous passages.
2393
- - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
2394
- }, buildCriticalRulesSection = (context) => {
2395
- const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2396
- return `# CRITICAL TRANSLATION RULES
2397
-
2398
- Follow these without exception:
2399
-
2400
- 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2401
- 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2402
- 3. Maintain all layout, markdown, rich-text marks, and data structure.
2403
- 4. Never invent metadata or field values that are absent in the source.
2404
- 5. Leave technical tokens (code, IDs, filenames) untouched.
2405
- 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2406
- }, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2407
-
2408
- Apply these mappings immediately after protected phrases and before style preferences.
2409
-
2410
- ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2411
- const components = [];
2412
- return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2413
- "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2414
- ), 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(`
2415
- `) : null;
2416
- }, translateEntireDocument = async ({
2417
- dispatch,
2418
- client,
2419
- schemaId,
2420
- sourceDocument,
2421
- targetLanguage,
2422
- languageFieldName,
2423
- translationTarget,
2424
- translationPrompts,
2425
- translationMode,
2426
- openaiConfig,
2427
- releaseId,
2428
- isNewTranslation
2429
- }) => {
2430
- const logContext = {
2431
- documentId: sourceDocument._id,
2432
- targetLanguage,
2433
- schemaId,
2434
- operation: translationTarget.operation,
2435
- targetId: translationTarget._id,
2436
- translationMode,
2437
- releaseId,
2438
- isNewTranslation
2439
- };
2440
- devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
2441
- try {
2442
- let versionedDocumentId, baseDocumentId;
2443
- if (isNewTranslation) {
2444
- const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
2445
- ...docWithoutMeta,
2446
- _type: sourceDocument._type,
2447
- [languageFieldName]: targetLanguage
2448
- };
2449
- devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
2450
- actionType: "sanity.action.document.version.create",
2451
- publishedId: baseDocumentId,
2452
- document: {
2453
- ...newTranslationDoc,
2454
- _type: sourceDocument._type,
2455
- _id: `versions.${releaseId}.${baseDocumentId}`
2456
- }
2457
- }), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
2458
- ...logContext,
2459
- versionedDocumentId,
2460
- baseDocumentId
2461
- });
2462
- } else {
2463
- const existingTranslationId = translationTarget._id;
2464
- if (!existingTranslationId)
2465
- throw new Error("Translation target ID not found for update operation");
2466
- devLog.info("Creating version of existing translation in release", {
2467
- ...logContext,
2468
- existingTranslationId
2469
- });
2470
- const existingDoc = await client.getDocument(existingTranslationId);
2471
- if (!existingDoc)
2472
- throw new Error(`Existing translation document not found: ${existingTranslationId}`);
2473
- await client.action({
2474
- actionType: "sanity.action.document.version.create",
2475
- publishedId: existingTranslationId,
2476
- document: {
2477
- ...existingDoc,
2478
- _id: `versions.${releaseId}.${existingTranslationId}`
2479
- }
2480
- }), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
2481
- ...logContext,
2482
- versionedDocumentId
2483
- });
2484
- const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
2485
- await client.createOrReplace({
2486
- ...sourceContent,
2487
- _id: versionedDocumentId,
2488
- [languageFieldName]: targetLanguage
2489
- }), devLog.info("Source content cloned onto version", {
2490
- ...logContext,
2491
- versionedDocumentId
2492
- });
2493
- }
2494
- let translatedDocument;
2495
- switch (translationMode) {
2496
- case "openai-only": {
2497
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2498
- throw new Error("OpenAI is not configured but openai-only mode was selected");
2499
- devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
2500
- const versionedDoc = await client.getDocument(versionedDocumentId);
2501
- if (!versionedDoc)
2502
- throw new Error(`Versioned document not found: ${versionedDocumentId}`);
2503
- const translatedDoc = await translateInPlaceWithOpenAI({
2504
- dispatch,
2505
- sourceDocument,
2506
- targetDocument: versionedDoc,
2507
- targetLanguage,
2508
- languageFieldName,
2509
- translationPrompts,
2510
- openaiConfig,
2511
- isReview: !1
2512
- });
2513
- translatedDoc && (translatedDocument = await client.createOrReplace({
2514
- ...translatedDoc,
2515
- _id: versionedDocumentId
2516
- }));
2517
- break;
2518
- }
2519
- case "sanity-with-review":
2520
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2521
- throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
2522
- if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
2523
- dispatch,
2524
- client,
2525
- schemaId,
2526
- sourceDocument,
2527
- targetLanguage,
2528
- languageFieldName,
2529
- versionedDocumentId,
2530
- translationPrompts
2531
- }), translatedDocument) {
2532
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
2533
- const reviewedDoc = await translateInPlaceWithOpenAI({
2534
- dispatch,
2535
- sourceDocument,
2536
- targetDocument: translatedDocument,
2537
- targetLanguage,
2538
- languageFieldName,
2539
- translationPrompts,
2540
- openaiConfig,
2541
- isReview: !0
2542
- });
2543
- reviewedDoc && (translatedDocument = await client.createOrReplace({
2544
- ...reviewedDoc,
2545
- _id: versionedDocumentId
2546
- }));
2547
- }
2548
- break;
2549
- default:
2550
- devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
2551
- dispatch,
2552
- client,
2553
- schemaId,
2554
- sourceDocument,
2555
- targetLanguage,
2556
- languageFieldName,
2557
- versionedDocumentId,
2558
- translationPrompts
2559
- });
2560
- break;
2561
- }
2562
- return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
2563
- ...translatedDocument,
2564
- _id: baseDocumentId
2565
- } : void 0;
2566
- } catch (error) {
2567
- const errorMessage = error instanceof Error ? error.message : String(error);
2568
- devLog.error(
2569
- "translateEntireDocument failed",
2570
- error instanceof Error ? error : new Error(String(error)),
2571
- logContext
2572
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2573
- errorMessage
2574
- }), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
2575
- return;
2576
- }
2577
- }, translateWithSanityAgent = async ({
2578
- dispatch,
2579
- client,
2580
- schemaId,
2581
- sourceDocument,
2582
- targetLanguage,
2583
- languageFieldName,
2584
- versionedDocumentId,
2585
- translationPrompts
2586
- }) => {
2587
- const logContext = {
2588
- documentId: sourceDocument._id,
2589
- targetLanguage,
2590
- versionedDocumentId
2154
+ (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
2591
2155
  };
2592
- try {
2593
- const { toLanguage, fromLanguage } = assembleLanguageSpecs({
2594
- sourceDocument,
2595
- languageFieldName,
2596
- targetLanguage
2597
- });
2598
- devLog.debug("Language specs assembled", {
2599
- ...logContext,
2600
- toLanguage,
2601
- fromLanguage
2156
+ return serialize(doc), lines.join(`
2157
+ `);
2158
+ }, serializeBlock = (block, path, indent, lines) => {
2159
+ const indentStr = " ".repeat(indent), metadata = {};
2160
+ block._type && (metadata._type = block._type), block._key && (metadata._key = block._key), block.style && (metadata.style = block.style), block.listItem && (metadata.listItem = block.listItem), block.level && (metadata.level = block.level), block.markDefs && (metadata.markDefs = block.markDefs);
2161
+ const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(typeof v == "string" ? v : JSON.stringify(v))}"`).join(" ");
2162
+ if (lines.push(`${indentStr}<block ${metaStr}>`), Array.isArray(block.children) && block._type === "block")
2163
+ block.children.forEach((child, idx) => {
2164
+ serializeSpan(child, [...path, "children", idx], indent + 1, lines);
2602
2165
  });
2603
- const documentAnalysis = analyzeDocumentContent(sourceDocument);
2604
- devLog.debug("Document analysis completed", {
2605
- ...logContext,
2606
- analysis: documentAnalysis
2607
- }), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
2608
- const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
2609
- {
2610
- documentAnalysis,
2611
- isUpdate: !1,
2612
- // Always fresh translation in a version
2613
- targetLanguage,
2614
- sourceLanguage: sourceDocument[languageFieldName]
2615
- },
2616
- {
2617
- globalPrompt: translationPrompts.globalPrompt,
2618
- localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2619
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2620
- protectedPhrases: translationPrompts.protectedPhrases
2621
- }
2622
- );
2623
- devLog.info("Structured style guide generated", {
2624
- ...logContext,
2625
- styleGuideLength: styleGuide.length,
2626
- omittedSections,
2627
- truncatedSections
2628
- }), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
2629
- ...logContext,
2630
- omittedSections,
2631
- truncatedSections
2166
+ else
2167
+ for (const [key, value] of Object.entries(block))
2168
+ key.startsWith("_") || key === "style" || key === "listItem" || key === "level" || key === "markDefs" || serializeBlockField(value, key, [...path, key], indent + 1, lines);
2169
+ lines.push(`${indentStr}</block>`);
2170
+ }, serializeBlockField = (value, fieldName, path, indent, lines) => {
2171
+ const indentStr = " ".repeat(indent);
2172
+ if (Array.isArray(value)) {
2173
+ value.some((item) => item?._type) ? (lines.push(`${indentStr}<field name="${fieldName}" type="array">`), value.forEach((item, idx) => {
2174
+ item?._type === "block" && item?.children ? serializeBlock(item, [...path, idx], indent + 1, lines) : item?._type ? serializeBlock(item, [...path, idx], indent + 1, lines) : serializeBlockField(item, String(idx), [...path, idx], indent + 1, lines);
2175
+ }), lines.push(`${indentStr}</field>`)) : (lines.push(`${indentStr}<field name="${fieldName}" type="array">`), value.forEach((item, idx) => {
2176
+ serializeBlockField(item, String(idx), [...path, idx], indent + 1, lines);
2177
+ }), lines.push(`${indentStr}</field>`));
2178
+ return;
2179
+ }
2180
+ if (isPlainObject(value)) {
2181
+ lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2182
+ for (const [key, val] of Object.entries(value))
2183
+ key.startsWith("_") || serializeBlockField(val, key, [...path, key], indent + 1, lines);
2184
+ lines.push(`${indentStr}</field>`);
2185
+ return;
2186
+ }
2187
+ if (typeof value == "string" && value.length > 0) {
2188
+ lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2189
+ return;
2190
+ }
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"
2632
2207
  });
2633
- const newDocument = await client.agent.action.translate({
2634
- schemaId,
2635
- toLanguage,
2636
- fromLanguage,
2637
- documentId: versionedDocumentId,
2638
- // Use versioned document ID
2639
- languageFieldPath: [languageFieldName],
2640
- styleGuide,
2641
- protectedPhrases: translationPrompts.protectedPhrases || [],
2642
- conditionalPaths: {
2643
- defaultHidden: !1,
2644
- defaultReadOnly: !1
2645
- }
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"
2646
2219
  });
2647
- return devLog.info("Sanity agent translation completed", {
2648
- ...logContext,
2649
- newDocumentId: newDocument._id,
2650
- newDocumentRev: newDocument._rev
2651
- }), newDocument;
2652
- } catch (error) {
2653
- const errorMessage = error instanceof Error ? error.message : String(error);
2654
- devLog.error(
2655
- "Sanity agent translation failed",
2656
- error instanceof Error ? error : new Error(String(error)),
2657
- logContext
2658
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2659
- 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);
2660
2243
  });
2661
2244
  return;
2662
2245
  }
2663
- }, 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 ({
2664
2272
  dispatch,
2665
2273
  sourceDocument,
2666
- targetDocument,
2667
- targetLanguage,
2668
2274
  languageFieldName,
2669
2275
  translationPrompts,
2670
- openaiConfig,
2671
- isReview
2276
+ targetLanguage,
2277
+ openaiConfig
2672
2278
  }) => {
2673
2279
  const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2674
2280
  sourceId: sourceDocument._id,
2675
- targetId: targetDocument._id,
2676
- targetLanguage,
2677
- sourceLanguage,
2678
- isReview
2281
+ sourceLanguage
2679
2282
  };
2680
2283
  try {
2681
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2682
- devLog.debug("Documents serialized for OpenAI", {
2284
+ const sourceText = serializeDocument(sourceDocument);
2285
+ devLog.info("Serialized document for OpenAI", {
2683
2286
  ...logContext,
2684
- sourceLength: sourceText.length,
2685
- targetLength: targetText.length
2287
+ characterCount: sourceText.length,
2288
+ estimatedTokens: Math.ceil(sourceText.length / 4),
2289
+ preview: sourceText.substring(0, 500)
2686
2290
  });
2687
- const prompt = isReview ? buildReviewPrompt({
2688
- sourceText,
2689
- translatedText: targetText,
2690
- sourceLanguage,
2691
- targetLanguage,
2692
- translationPrompts
2693
- }) : buildTranslationPrompt({
2291
+ const prompt = buildTranslationPrompt$1({
2694
2292
  sourceText,
2695
2293
  sourceLanguage,
2696
2294
  targetLanguage,
2697
2295
  translationPrompts
2698
- });
2699
- devLog.debug("Calling GPT-5", {
2700
- ...logContext,
2701
- promptLength: prompt.length
2702
- });
2703
- const response = await callGPT5(openaiConfig.apiKey, {
2296
+ }), response = await callGPT5(openaiConfig.apiKey, {
2704
2297
  messages: [
2705
2298
  {
2706
- role: "system",
2707
- 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."
2708
2301
  },
2709
2302
  { role: "user", content: prompt }
2710
2303
  ],
2711
2304
  model: openaiConfig.model,
2712
- reasoningEffort: openaiConfig.reasoningEffort,
2713
- verbosity: openaiConfig.verbosity,
2714
- onReasoningChunk: (reasoning) => {
2715
- const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
2716
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
2717
- reasoningProgress: reasoning
2718
- });
2719
- }
2305
+ reasoningEffort: openaiConfig.reasoningEffort
2720
2306
  });
2721
- devLog.info("OpenAI operation completed", {
2307
+ devLog.info("OpenAI translation response received", {
2722
2308
  ...logContext,
2723
- tokensUsed: response.tokensUsed.total,
2724
- cost: response.cost
2309
+ tokensUsed: response.tokensUsed,
2310
+ cost: response.cost,
2311
+ responseLength: response.content.length,
2312
+ responsePreview: response.content.substring(0, 500)
2725
2313
  });
2726
- 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);
2727
2317
  if (!validation.valid) {
2728
2318
  devLog.error("Structure validation failed", {
2729
2319
  ...logContext,
@@ -2737,17 +2327,14 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2737
2327
  `Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
2738
2328
  );
2739
2329
  }
2740
- devLog.info("Structure validation passed", logContext);
2741
- const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
2742
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
2330
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2743
2331
  tokensUsed: response.tokensUsed.total,
2744
- estimatedCost: response.cost,
2745
- reasoningSummary: response.reasoningSummary
2332
+ estimatedCost: response.cost
2746
2333
  }), result;
2747
2334
  } catch (error) {
2748
2335
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2749
2336
  }
2750
- }, buildTranslationPrompt = ({
2337
+ }, buildTranslationPrompt$1 = ({
2751
2338
  sourceText,
2752
2339
  sourceLanguage,
2753
2340
  targetLanguage,
@@ -2794,73 +2381,193 @@ Brief summary of key translation decisions and reasoning
2794
2381
  </reasoning>
2795
2382
 
2796
2383
  Then return the translated document:`;
2797
- }, buildReviewPrompt = ({
2798
- sourceText,
2799
- 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 = ({
2800
2403
  sourceLanguage,
2801
2404
  targetLanguage,
2802
- translationPrompts
2405
+ components,
2406
+ isUpdate = !1
2803
2407
  }) => {
2804
- const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2805
- return `# TRANSLATION QUALITY REVIEW
2806
-
2807
- Review this ${targetLanguage} translation and return a corrected version.
2808
-
2809
- ## 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(`
2810
2412
 
2811
- 1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
2812
- 2. **Terminology**: Are glossary terms used correctly?
2813
- 3. **Protected Phrases**: Are protected phrases preserved?
2814
- 4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
2815
- 5. **Style**: Does it follow the style guide?
2816
- 6. **Completeness**: Is all content translated?
2413
+ ---
2817
2414
 
2818
- ## Guidelines
2415
+ `);
2416
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2819
2417
 
2820
- ${globalPrompt ? `### General Guidelines
2821
- ${globalPrompt}
2822
- ` : ""}
2823
- ${localePrompt ? `### ${targetLanguage} Guidelines
2824
- ${localePrompt}
2825
- ` : ""}
2826
- ${glossaryTerms ? `### Required Terminology
2827
- ${glossaryTerms}
2828
- ` : ""}
2829
- ${protectedPhrases?.length ? `### Protected Phrases (must preserve)
2830
- ${protectedPhrases.join(", ")}
2831
- ` : ""}
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
2832
2425
 
2833
- ## Source Document (${sourceLanguage})
2426
+ Follow these without exception:
2834
2427
 
2835
- \`\`\`
2836
- ${sourceText}
2837
- \`\`\`
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
2838
2435
 
2839
- ## Current Translation (${targetLanguage})
2436
+ This document already has a translation; only adjust fields that changed.
2840
2437
 
2841
- \`\`\`
2842
- ${translatedText}
2843
- \`\`\`
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
2844
2441
 
2845
- ## Instructions
2442
+ Apply these mappings immediately after protected phrases and before style preferences.
2846
2443
 
2847
- 1. Review the translation for quality issues
2848
- 2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
2849
- 3. Keep good translations as-is - only fix what needs fixing
2850
- 4. Return the corrected translation in THE EXACT SAME FORMAT
2851
- 5. **CRITICAL**: Only modify text content between tags
2852
- 6. Never add, remove, or change any tags, attributes, or structure
2444
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2853
2445
 
2854
- If you made corrections or have insights about the translation quality, include a brief summary:
2855
- <reasoning>
2856
- Brief summary of review findings and corrections made
2857
- </reasoning>
2446
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2858
2447
 
2859
- 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;
2860
2566
  }, translateDocument = async ({
2861
2567
  dispatch,
2862
2568
  client,
2863
2569
  sourceDocument,
2570
+ sourceLanguage,
2864
2571
  targetLanguage,
2865
2572
  languageFieldName,
2866
2573
  shouldOverwriteFreshTranslations,
@@ -2879,62 +2586,37 @@ Then return the corrected translation document:`;
2879
2586
  };
2880
2587
  devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2881
2588
  try {
2882
- const sourceLanguage = sourceDocument[languageFieldName];
2883
- if (devLog.debug("Checking source document language", {
2884
- ...logContext,
2885
- sourceLanguage,
2886
- languageFieldName
2887
- }), !sourceLanguage) {
2888
- const error = `Source document ${sourceDocument._id} does not have a language`;
2889
- throw devLog.error(error, void 0, logContext), new Error(error);
2890
- }
2891
- devLog.debug("Getting translation metadata", logContext);
2892
2589
  const translationMetadata = await getTranslationMetadata({
2893
2590
  client,
2894
2591
  sourceDocument,
2895
2592
  sourceLanguage
2896
2593
  }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
2897
2594
  (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
2898
- ) ?? !1;
2899
- if (devLog.debug("Checking if translation is already fresh", {
2900
- ...logContext,
2901
- isAlreadyFreshTranslation,
2902
- currentRev: sourceDocument._rev
2903
- }), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2904
- devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2595
+ );
2596
+ if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2597
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2905
2598
  targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
2906
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2907
- return;
2908
- }
2909
- const translationTarget = determineTranslationTarget({
2910
- translationMetadata,
2911
- targetLanguage
2912
- }), isNewTranslation = translationTarget.operation === "create";
2913
- devLog.debug("Translation target determined", {
2914
- ...logContext,
2915
- operation: translationTarget.operation,
2916
- isNewTranslation
2917
- });
2918
- const newDocument = await translateEntireDocument({
2599
+ });
2600
+ return;
2601
+ }
2602
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2919
2603
  dispatch,
2920
2604
  client,
2921
2605
  schemaId,
2922
2606
  sourceDocument,
2923
2607
  targetLanguage,
2608
+ existingTranslationId,
2924
2609
  languageFieldName,
2925
- translationTarget,
2926
2610
  translationPrompts,
2927
2611
  translationMode,
2928
2612
  openaiConfig,
2929
- releaseId,
2930
- isNewTranslation
2613
+ releaseId
2931
2614
  });
2932
- if (!newDocument)
2933
- 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");
2934
- return devLog.debug("Translation document created", {
2935
- ...logContext,
2936
- newDocumentId: newDocument._id
2937
- }), 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", {
2938
2620
  targetId: newDocument._id
2939
2621
  }), await updateTranslationMetadata({
2940
2622
  client,
@@ -2943,8 +2625,10 @@ Then return the corrected translation document:`;
2943
2625
  newDocument,
2944
2626
  targetLanguage,
2945
2627
  translationMetadata,
2946
- isNewTranslation
2947
- }), 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", {
2948
2632
  ...logContext,
2949
2633
  newDocumentId: newDocument._id
2950
2634
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
@@ -2970,77 +2654,61 @@ Then return the corrected translation document:`;
2970
2654
  }) => {
2971
2655
  const logContext = {
2972
2656
  documentId: sourceDocument._id,
2657
+ documentRev: sourceDocument._rev,
2973
2658
  targetLanguages,
2974
2659
  referencesCount: references.length,
2975
2660
  maxConcurrentJobs: maxConcurrentTranslationJobs,
2976
2661
  translationMode,
2977
2662
  shouldOverwriteFreshTranslations
2978
2663
  };
2979
- devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
2980
2664
  let releaseId;
2981
2665
  try {
2982
2666
  const sourceLanguage = sourceDocument[languageFieldName];
2983
2667
  if (!sourceLanguage)
2984
2668
  throw devLog.error("Source language not found"), new Error("Source language not found");
2985
- maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
2986
- ...logContext,
2987
- newPermits: maxConcurrentTranslationJobs
2988
- }), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
2669
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2989
2670
  type: "SET_OVERALL_STATUS",
2990
2671
  payload: "translating"
2991
2672
  });
2992
- 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()}`;
2993
- devLog.info("Creating Content Release for translation batch", {
2994
- ...logContext,
2995
- releaseTitle
2996
- });
2997
- 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({
2998
2674
  metadata: {
2999
2675
  title: releaseTitle,
3000
2676
  description: releaseDescription,
3001
- releaseType: "asap"
2677
+ releaseType: "undecided"
3002
2678
  }
3003
2679
  });
3004
- releaseId = newReleaseId, devLog.info("Content Release created", {
2680
+ devLog.info("Content Release created", {
3005
2681
  ...logContext,
3006
- releaseId
2682
+ releaseId: releaseId2
3007
2683
  });
3008
2684
  const sourceDocuments = [
3009
2685
  {
3010
2686
  _id: sourceDocument._id,
3011
2687
  _type: sourceDocument._type,
3012
- // TODO: Handle this properly
2688
+ // TODO: Extract document title more robustly across schema types
3013
2689
  title: sourceDocument.name || sourceDocument.title
3014
2690
  },
3015
2691
  ...references
3016
- ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), targetDocumentIds = [];
3017
- devLog.debug("Prepared document set for translation", {
3018
- ...logContext,
3019
- sourceDocumentIds: sourceDocumentIds.length,
3020
- totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
3021
- });
3022
- const translationPrompts = await fetchTranslationPrompts({
2692
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
3023
2693
  client,
3024
2694
  sourceLanguage,
3025
2695
  targetLanguages,
3026
2696
  sourceDocument
3027
2697
  });
3028
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2698
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
3029
2699
  const translationTasks = sourceDocumentIds.flatMap(
3030
2700
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3031
2701
  try {
3032
2702
  await translationSemaphore.withPermit(async () => {
3033
2703
  const document = await client.withConfig({
3034
- // This enables us to detect documents that might be stale due to unpublished changes
3035
- perspective: "raw"
3036
- }).fetch("*[_id == $id][0]", {
3037
- id: documentId
3038
- });
3039
- if (document && releaseId) {
3040
- 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({
3041
2708
  dispatch,
3042
2709
  client,
3043
2710
  sourceDocument: document,
2711
+ sourceLanguage,
3044
2712
  targetLanguage,
3045
2713
  languageFieldName,
3046
2714
  shouldOverwriteFreshTranslations,
@@ -3048,9 +2716,9 @@ Then return the corrected translation document:`;
3048
2716
  translationPrompts,
3049
2717
  translationMode,
3050
2718
  openaiConfig,
3051
- releaseId
2719
+ releaseId: releaseId2
3052
2720
  });
3053
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
2721
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
3054
2722
  }
3055
2723
  });
3056
2724
  } catch (error) {
@@ -3066,25 +2734,21 @@ Then return the corrected translation document:`;
3066
2734
  }
3067
2735
  })
3068
2736
  ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
3069
- devLog.info("Translation tasks summary", {
2737
+ devLog.info("Translation tasks completed", {
3070
2738
  ...logContext,
3071
2739
  total: results.length,
3072
2740
  succeeded,
3073
- failed
3074
- }), devLog.info("All translation tasks completed", {
3075
- ...logContext,
3076
- targetDocumentIds: targetDocumentIds.length
2741
+ failed,
2742
+ targetDocuments: versionedTargetDocumentIds.length
3077
2743
  }), dispatch({
3078
2744
  type: "SET_OVERALL_STATUS",
3079
2745
  payload: "linking_references"
3080
- }), devLog.debug("Starting reference localization", {
3081
- ...logContext,
3082
- documentsToLocalize: targetDocumentIds.length
3083
2746
  }), await Promise.all(
3084
- targetDocumentIds.map(async (documentId) => {
2747
+ versionedTargetDocumentIds.map(async (documentId) => {
3085
2748
  await localizeReferences({
3086
2749
  client,
3087
2750
  documentId,
2751
+ sourceLanguage,
3088
2752
  languageFieldName
3089
2753
  });
3090
2754
  })
@@ -3094,26 +2758,13 @@ Then return the corrected translation document:`;
3094
2758
  ...logContext,
3095
2759
  succeeded,
3096
2760
  failed
3097
- }), 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();
3098
2762
  } catch (error) {
3099
- if (devLog.error(
2763
+ throw devLog.error(
3100
2764
  "translateDocumentSet failed with unexpected error",
3101
2765
  error instanceof Error ? error : new Error(String(error)),
3102
2766
  logContext
3103
- ), releaseId)
3104
- try {
3105
- await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
3106
- ...logContext,
3107
- releaseId
3108
- });
3109
- } catch (cleanupError) {
3110
- devLog.error(
3111
- "Failed to cleanup release",
3112
- cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
3113
- logContext
3114
- );
3115
- }
3116
- 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;
3117
2768
  }
3118
2769
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
3119
2770
  const {
@@ -3534,24 +3185,12 @@ Then return the corrected translation document:`;
3534
3185
  " \u2014 ",
3535
3186
  job._id.slice(-6)
3536
3187
  ] }),
3537
- !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3538
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3539
- job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3540
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
3541
- /* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
3542
- ] }) })
3543
- ] }),
3544
- job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3545
- /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3546
- job.tokensUsed.toLocaleString(),
3547
- " tokens (~$",
3548
- job.estimatedCost?.toFixed(4),
3549
- ")"
3550
- ] }),
3551
- job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3552
- "\u{1F4A1} ",
3553
- job.reasoningSummary
3554
- ] }) })
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
+ ")"
3555
3194
  ] }) : null,
3556
3195
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3557
3196
  "Error: ",
@@ -3665,26 +3304,9 @@ Then return the corrected translation document:`;
3665
3304
  "completed"
3666
3305
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3667
3306
  const { overallStatus, document } = useTranslationContext();
3668
- return devLog.debug("TranslationDialog rendered", {
3669
- documentId: document._id,
3670
- overallStatus,
3671
- component: "TranslationDialog"
3672
- }), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
3673
- documentId: document._id,
3674
- overallStatus,
3675
- component: "TranslationDialog"
3676
- }), /* @__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", {
3677
3308
  documentId: document._id,
3678
- overallStatus,
3679
- component: "TranslationDialog"
3680
- }), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
3681
- documentId: document._id,
3682
- overallStatus,
3683
- component: "TranslationDialog"
3684
- }), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
3685
- documentId: document._id,
3686
- overallStatus,
3687
- component: "TranslationDialog"
3309
+ overallStatus
3688
3310
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3689
3311
  }, getTranslateAction = (config) => ({ draft, published }) => {
3690
3312
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
@@ -3711,7 +3333,427 @@ Then return the corrected translation document:`;
3711
3333
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3712
3334
  }
3713
3335
  } : (devLog.error("Missing config in TranslateAction"), null);
3714
- }, 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
+ })), {
3715
3757
  name: "sanity-plugin-ai-translation",
3716
3758
  document: {
3717
3759
  actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
@@ -3722,6 +3764,8 @@ Then return the corrected translation document:`;
3722
3764
  })), translationHistorySchema = translationHistory;
3723
3765
  export {
3724
3766
  aiTranslationPlugin,
3725
- translationHistorySchema
3767
+ richTextTransform,
3768
+ translationHistorySchema,
3769
+ vttCaptionTransform
3726
3770
  };
3727
3771
  //# sourceMappingURL=index.js.map