@braze/sanity-plugin-ai-translation 0.2.22 → 0.2.24

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,4 +1,4 @@
1
- import { useClient, defineType, defineField, pathToString, definePlugin } from "sanity";
1
+ import { useClient, defineType, defineField, getPublishedId, pathToString, 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
4
  import { createContext, useContext, useEffect, useState, useReducer, useCallback, useMemo } from "react";
@@ -6,6 +6,7 @@ import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Selec
6
6
  import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
7
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";
@@ -31,7 +32,7 @@ const TranslationContext = createContext({
31
32
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
32
33
  );
33
34
  return context;
34
- }, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), draftId = (id) => `drafts.${undraftId(id)}`, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
35
+ }, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
35
36
  const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
36
37
  params: {
37
38
  documentId: undraftId(documentId),
@@ -191,12 +192,7 @@ async function analyzeFeedback({
191
192
  currentDocument,
192
193
  onProgress
193
194
  }) {
194
- devLog.info("Starting feedback analysis", {
195
- sourceLanguage,
196
- targetLanguage,
197
- hasUserFeedback: !!userFeedback,
198
- shouldIncludeEdits
199
- }), onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
195
+ onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
200
196
  const analysisPrompt = buildAnalysisPrompt({
201
197
  sourceLanguage,
202
198
  targetLanguage,
@@ -205,10 +201,8 @@ async function analyzeFeedback({
205
201
  sourceSnapshot,
206
202
  targetSnapshot,
207
203
  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..." });
204
+ }), analysis = await analyzeWithAI(client, analysisPrompt), suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
205
+ onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
212
206
  const validatedSuggestions = await Promise.all(
213
207
  suggestions.map(async (suggestion) => {
214
208
  const validation = await validateSuggestion(client, suggestion);
@@ -221,7 +215,10 @@ async function analyzeFeedback({
221
215
  };
222
216
  })
223
217
  );
224
- return devLog.info("Suggestions validated", {
218
+ return devLog.info("Feedback analysis complete", {
219
+ sourceLanguage,
220
+ targetLanguage,
221
+ total: validatedSuggestions.length,
225
222
  valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
226
223
  warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
227
224
  invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
@@ -235,24 +232,18 @@ async function applySuggestions({
235
232
  suggestions,
236
233
  onProgress
237
234
  }) {
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)
235
+ if (!targetLanguage)
245
236
  throw devLog.error("targetLanguage is undefined in applySuggestions!", {
246
237
  sourceLanguage,
247
238
  targetLanguage,
248
239
  suggestionsCount: suggestions.length
249
240
  }), new Error("targetLanguage is required but was undefined");
250
241
  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");
242
+ approvedSuggestions.length !== 0 && (onProgress?.({ stage: "applying", message: "Applying updates to translation settings..." }), await applyImprovementsFromSuggestions(client, schemaId, targetLanguage, approvedSuggestions, onProgress), devLog.info("Suggestions applied successfully", {
243
+ sourceLanguage,
244
+ targetLanguage,
245
+ count: approvedSuggestions.length
246
+ }));
256
247
  }
257
248
  function buildAnalysisPrompt({
258
249
  sourceLanguage,
@@ -470,49 +461,21 @@ function parseAIResponse(responseText) {
470
461
  }
471
462
  }
472
463
  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
464
  const settingsId = "translation-settings";
486
465
  await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
487
466
  _id: settingsId,
488
467
  _type: "translationSettings"
489
468
  });
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(
469
+ 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
470
  (s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
503
471
  );
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?.({
472
+ if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
510
473
  stage: "applying",
511
474
  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
475
  }), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
513
476
  onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
514
477
  const phrases = protectedPhrases.map((s) => s.description);
515
- devLog.debug("Applying protected phrases", { phrases }), updates.push(addProtectedPhrases(client, settingsId, phrases));
478
+ updates.push(addProtectedPhrases(client, settingsId, phrases));
516
479
  }
517
480
  if (glossaryTerms.length > 0) {
518
481
  onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
@@ -524,22 +487,17 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
524
487
  targetValue: s.targetValue
525
488
  // biome-ignore-end lint/style/noNonNullAssertion: safely handled
526
489
  }));
527
- devLog.debug("Applying glossary terms", { terms }), updates.push(addGlossaryTerms(client, terms));
490
+ updates.push(addGlossaryTerms(client, terms));
528
491
  }
529
492
  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
493
  client,
531
494
  settingsId,
532
495
  targetLanguage,
533
496
  suggestions.filter((s) => s.approved)
534
- ), devLog.info("All improvements applied successfully");
497
+ );
535
498
  }
536
499
  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)
500
+ if (!targetLanguage)
543
501
  throw devLog.error("targetLanguage is undefined or empty!", {
544
502
  targetLanguage,
545
503
  globalPrompts: globalPrompts.length,
@@ -555,11 +513,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
555
513
  ...updates.map((update) => `- ${update}`)
556
514
  ].filter(Boolean).join(`
557
515
  `);
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
- });
516
+ await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
563
517
  }
564
518
  if (localePrompts.length > 0) {
565
519
  const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
@@ -572,12 +526,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
572
526
  ...updates.map((update) => `- ${update}`)
573
527
  ].filter(Boolean).join(`
574
528
  `), 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
- });
529
+ await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
581
530
  } else {
582
531
  const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
583
532
  `);
@@ -587,22 +536,17 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
587
536
  _key: targetLanguage,
588
537
  value: newValue
589
538
  }
590
- ]).commit(), devLog.info("Locale prompt created", {
591
- targetLanguage,
592
- length: newValue.length,
593
- addedUpdates: updates.length
594
- });
539
+ ]).commit();
595
540
  }
596
541
  }
597
542
  devLog.info("Prompts updated successfully", {
543
+ targetLanguage,
598
544
  globalUpdated: globalPrompts.length > 0,
599
545
  localeUpdated: localePrompts.length > 0
600
546
  });
601
547
  }
602
548
  async function addProtectedPhrases(client, settingsId, phrases) {
603
- devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
604
549
  const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
605
- devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
606
550
  let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
607
551
  const conflictingPhrases = [];
608
552
  for (const phrase of newPhrases)
@@ -610,12 +554,7 @@ async function addProtectedPhrases(client, settingsId, phrases) {
610
554
  'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
611
555
  { phrase }
612
556
  ) && (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 });
557
+ 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
558
  }
620
559
  async function addGlossaryTerms(client, terms) {
621
560
  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 +582,13 @@ async function addGlossaryTerms(client, terms) {
643
582
  );
644
583
  if (existingTerm) {
645
584
  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", [
585
+ existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
647
586
  {
648
587
  _type: "internationalizedArrayStringValue",
649
588
  _key: targetLocale,
650
589
  value: targetValue
651
590
  }
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
- });
591
+ ]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
669
592
  } else
670
593
  await client.create({
671
594
  _type: "translationGlossaryTerm",
@@ -681,11 +604,10 @@ async function addGlossaryTerms(client, terms) {
681
604
  value: targetValue
682
605
  }
683
606
  ]
684
- }), devLog.info("Created new glossary term", { sourceLocale, sourceValue, targetLocale, targetValue });
607
+ });
685
608
  }
686
609
  devLog.info("Glossary terms processed", {
687
- totalRequested: terms.length,
688
- validAdded: validTerms.length,
610
+ processed: validTerms.length,
689
611
  skipped: terms.length - validTerms.length
690
612
  });
691
613
  }
@@ -698,10 +620,7 @@ async function recordChangeHistory(client, settingsId, targetLanguage, suggestio
698
620
  description: suggestion.description,
699
621
  locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
700
622
  }));
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
- }));
623
+ historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
705
624
  }
706
625
  const ICON_SIZE = 64, progressIconMap = {
707
626
  in_progress: /* @__PURE__ */ jsx(Spinner, {}),
@@ -1227,8 +1146,8 @@ const ICON_SIZE = 64, progressIconMap = {
1227
1146
  references: action.payload
1228
1147
  };
1229
1148
  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))), {
1149
+ const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
1150
+ return newTranslationJobs.set(jobKey, mergedJob), {
1232
1151
  ...context,
1233
1152
  translationJobs: newTranslationJobs
1234
1153
  };
@@ -1755,7 +1674,12 @@ const fetchTranslationPrompts = async ({
1755
1674
  }
1756
1675
  };
1757
1676
  return recursiveSearch(obj, []), results;
1758
- }, localizeReferences = async ({ client, documentId, languageFieldName }) => {
1677
+ }, localizeReferences = async ({
1678
+ client,
1679
+ documentId,
1680
+ sourceLanguage,
1681
+ languageFieldName
1682
+ }) => {
1759
1683
  try {
1760
1684
  const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
1761
1685
  if (!doc) {
@@ -1777,25 +1701,25 @@ const fetchTranslationPrompts = async ({
1777
1701
  const refs = findKeyValues(doc, "_ref");
1778
1702
  if (!refs?.length)
1779
1703
  return;
1780
- await updateReferences(documentId, client, refs, targetLanguage);
1704
+ await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
1781
1705
  } catch (error) {
1782
1706
  devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
1783
1707
  documentId,
1784
1708
  component: "localizeReferences"
1785
1709
  });
1786
1710
  }
1787
- }, updateReferences = async (documentId, client, refs, targetLanguage) => {
1711
+ }, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
1788
1712
  if (!refs?.length)
1789
1713
  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)]`,
1714
+ const refIds = refs.map((r) => getPublishedId(r.value)), translations = await client.fetch(
1715
+ `*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
1716
+ "sourceId": translations[_key == $sourceLanguage][0].value._ref,
1717
+ "targetId": translations[_key == $targetLanguage][0].value._ref
1718
+ }`,
1796
1719
  {
1797
1720
  translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1798
1721
  refIds,
1722
+ sourceLanguage,
1799
1723
  targetLanguage
1800
1724
  }
1801
1725
  ), translationMap = /* @__PURE__ */ new Map();
@@ -1804,13 +1728,8 @@ const fetchTranslationPrompts = async ({
1804
1728
  const patch = client.patch(documentId);
1805
1729
  let hasChanges = !1;
1806
1730
  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({
1809
- [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1810
- _type: "reference",
1811
- _weak: !1
1812
- }
1813
- }), hasChanges = !0);
1731
+ const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
1732
+ newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), hasChanges = !0);
1814
1733
  }
1815
1734
  hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
1816
1735
  documentId,
@@ -1833,7 +1752,9 @@ const fetchTranslationPrompts = async ({
1833
1752
  payload
1834
1753
  });
1835
1754
  }
1836
- }, getTranslationMetadata = async ({
1755
+ }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1756
+ (translation) => translation._key === targetLanguage
1757
+ )?.value._ref || null, getTranslationMetadata = async ({
1837
1758
  client,
1838
1759
  sourceDocument,
1839
1760
  sourceLanguage
@@ -1882,7 +1803,7 @@ const fetchTranslationPrompts = async ({
1882
1803
  _type: "internationalizedArrayReferenceValue",
1883
1804
  value: {
1884
1805
  _type: "reference",
1885
- _ref: undraftId(newDocument._id),
1806
+ _ref: getPublishedId(newDocument._id),
1886
1807
  _weak: !0,
1887
1808
  _strengthenOnPublish: {
1888
1809
  _type: "reference",
@@ -1892,7 +1813,7 @@ const fetchTranslationPrompts = async ({
1892
1813
  }, newTranslationHistory = {
1893
1814
  _key: targetLanguage,
1894
1815
  language: targetLanguage,
1895
- sourceId: undraftId(sourceDocument._id),
1816
+ sourceId: getPublishedId(sourceDocument._id),
1896
1817
  sourceRev: sourceDocument._rev,
1897
1818
  sourceLanguage,
1898
1819
  sourceSnapshot: JSON.stringify(sourceDocument),
@@ -1922,70 +1843,51 @@ const fetchTranslationPrompts = async ({
1922
1843
  status
1923
1844
  })
1924
1845
  });
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
1846
+ }, callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1847
+ const model = request.model || "gpt-5", requestBody = {
1848
+ model,
1849
+ input: request.messages
1850
+ // Uses 'input' instead of 'messages'
1962
1851
  };
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
1852
+ request.reasoningEffort && (requestBody.reasoning = {
1853
+ effort: request.reasoningEffort,
1854
+ summary: "auto"
1855
+ // Request reasoning summaries
1856
+ });
1857
+ const response = await fetch("https://api.openai.com/v1/responses", {
1858
+ method: "POST",
1859
+ headers: {
1860
+ "Content-Type": "application/json",
1861
+ Authorization: `Bearer ${apiKey}`
1975
1862
  },
1976
- toLanguage: {
1977
- id: targetLanguage,
1978
- title: targetLanguage
1979
- }
1863
+ body: JSON.stringify(requestBody)
1864
+ });
1865
+ if (!response.ok) {
1866
+ const error = await response.json();
1867
+ throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
1868
+ }
1869
+ const result = await response.json(), tokensUsed = {
1870
+ prompt: result.usage?.input_tokens || 0,
1871
+ completion: result.usage?.output_tokens || 0,
1872
+ reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
1873
+ total: result.usage?.total_tokens || 0
1874
+ }, cost = calculateCost(tokensUsed, model);
1875
+ let content = "";
1876
+ if (result.output && Array.isArray(result.output)) {
1877
+ const messageItem = result.output.find((item) => item.type === "message");
1878
+ messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
1879
+ }
1880
+ return content || (content = result.output_text || result.text || ""), {
1881
+ content,
1882
+ tokensUsed,
1883
+ cost
1980
1884
  };
1981
- }, callGPT5 = async (apiKey, request) => {
1982
- const model = request.model || "gpt-5", isGPT5 = model === "gpt-5", useStreaming = !!request.onReasoningChunk, requestBody = {
1885
+ }, callChatCompletionsAPI = async (apiKey, request) => {
1886
+ const model = request.model || "gpt-4o", requestBody = {
1983
1887
  model,
1984
1888
  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", {
1889
+ temperature: request.temperature ?? 0.3
1890
+ }, response = await fetch("https://api.openai.com/v1/chat/completions", {
1989
1891
  method: "POST",
1990
1892
  headers: {
1991
1893
  "Content-Type": "application/json",
@@ -1995,88 +1897,30 @@ const fetchTranslationPrompts = async ({
1995
1897
  });
1996
1898
  if (!response.ok) {
1997
1899
  const error = await response.json();
1998
- throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
1900
+ throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
1999
1901
  }
2000
- if (useStreaming && response.body && request.onReasoningChunk)
2001
- return await handleStreamingResponse(response, request.onReasoningChunk, model);
2002
1902
  const result = await response.json(), usage = result.usage || {}, tokensUsed = {
2003
1903
  prompt: usage.prompt_tokens || 0,
2004
1904
  completion: usage.completion_tokens || 0,
2005
1905
  reasoning: usage.reasoning_tokens || 0,
2006
- // New in GPT-5
2007
1906
  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);
1907
+ }, cost = calculateCost(tokensUsed, model);
2059
1908
  return {
2060
- content: contentBuffer || fullContent,
1909
+ content: result.choices[0].message.content || "",
2061
1910
  tokensUsed,
2062
- cost,
2063
- reasoningSummary: reasoningBuffer || void 0
1911
+ cost
2064
1912
  };
2065
- }, calculateGPT5Cost = (tokens, model) => {
1913
+ }, calculateCost = (tokens, model) => {
2066
1914
  const pricing = {
2067
1915
  "gpt-5": {
2068
1916
  input: 2e-6,
2069
- // $0.002 per 1k input tokens
2070
1917
  output: 1e-5,
2071
- // $0.010 per 1k output tokens
2072
1918
  reasoning: 5e-6
2073
- // $0.005 per 1k reasoning tokens
2074
1919
  },
2075
1920
  "gpt-4o": {
2076
1921
  input: 5e-6,
2077
1922
  output: 14999999999999999e-21,
2078
1923
  reasoning: 0
2079
- // No reasoning tokens in GPT-4
2080
1924
  },
2081
1925
  "gpt-4-turbo": {
2082
1926
  input: 1e-5,
@@ -2355,434 +2199,35 @@ const fetchTranslationPrompts = async ({
2355
2199
  type: "metadata_changed"
2356
2200
  }) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
2357
2201
  }
2358
- }, assembleTranslationPrompt = (context, components) => {
2359
- const sections = [
2360
- {
2361
- id: "agentDirectives",
2362
- content: buildAgentDirectivesSection(context),
2363
- required: !0,
2364
- priority: 0
2365
- },
2366
- {
2367
- id: "criticalRules",
2368
- content: buildCriticalRulesSection(context),
2369
- required: !0,
2370
- priority: 1
2371
- }
2372
- ];
2373
- components.glossaryTerms?.trim() && sections.push({
2374
- id: "glossaryTerms",
2375
- content: buildGlossarySection(components.glossaryTerms),
2376
- required: !1,
2377
- priority: 3
2378
- });
2379
- const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2380
- styleSection && sections.push({
2381
- id: "styleGuide",
2382
- content: styleSection,
2383
- required: !1,
2384
- priority: 4
2385
- }), sections.sort((a, b) => a.priority - b.priority);
2386
- let styleGuide = "";
2387
- const omittedSections = [], truncatedSections = [];
2388
- for (const section of sections) {
2389
- const prefix = styleGuide.length > 0 ? `
2390
-
2391
- ---
2392
-
2393
- ` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
2394
- if (availableCharacters <= 0) {
2395
- if (section.required) {
2396
- const truncatedContent = truncateSection(section.content, 0);
2397
- truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
2398
- } else
2399
- omittedSections.push(section.id);
2400
- continue;
2401
- }
2402
- if (section.content.length <= availableCharacters) {
2403
- styleGuide += prefix + section.content;
2404
- continue;
2405
- }
2406
- if (section.required) {
2407
- const truncatedContent = truncateSection(section.content, availableCharacters);
2408
- styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
2409
- } else
2410
- omittedSections.push(section.id);
2411
- }
2412
- return {
2413
- styleGuide,
2414
- omittedSections,
2415
- truncatedSections
2416
- };
2417
- }, truncateSection = (content, limit) => {
2418
- if (limit <= 0)
2419
- return "";
2420
- if (content.length <= limit)
2421
- return content;
2422
- if (limit <= 1)
2423
- return content.slice(0, limit);
2424
- const ellipsis = "\u2026";
2425
- if (limit <= ellipsis.length)
2426
- return content.slice(0, limit);
2427
- const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
2428
- return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
2429
- }, buildAgentDirectivesSection = (context) => {
2430
- const { targetLanguage, sourceLanguage } = context;
2431
- return `# AGENT WORKFLOW DIRECTIVES
2432
-
2433
- - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2434
- - Scan each field once, then move directly into translation work.
2435
- - Plan updates before editing so document structure remains intact.
2436
- - Default to medium reasoning effort and escalate only for ambiguous passages.
2437
- - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
2438
- }, buildCriticalRulesSection = (context) => {
2439
- const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2440
- return `# CRITICAL TRANSLATION RULES
2441
-
2442
- Follow these without exception:
2443
-
2444
- 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2445
- 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2446
- 3. Maintain all layout, markdown, rich-text marks, and data structure.
2447
- 4. Never invent metadata or field values that are absent in the source.
2448
- 5. Leave technical tokens (code, IDs, filenames) untouched.
2449
- 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2450
- }, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2451
-
2452
- Apply these mappings immediately after protected phrases and before style preferences.
2453
-
2454
- ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2455
- const components = [];
2456
- return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2457
- "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2458
- ), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
2459
- `) : null;
2460
- }, translateEntireDocument = async ({
2202
+ }, getOpenAITranslation = async ({
2461
2203
  dispatch,
2462
- client,
2463
- schemaId,
2464
2204
  sourceDocument,
2465
- targetLanguage,
2466
2205
  languageFieldName,
2467
- translationTarget,
2468
2206
  translationPrompts,
2469
- translationMode,
2470
- openaiConfig,
2471
- releaseId,
2472
- isNewTranslation
2473
- }) => {
2474
- const logContext = {
2475
- documentId: sourceDocument._id,
2476
- targetLanguage,
2477
- schemaId,
2478
- operation: translationTarget.operation,
2479
- targetId: translationTarget._id,
2480
- translationMode,
2481
- releaseId,
2482
- isNewTranslation
2483
- };
2484
- devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
2485
- try {
2486
- let versionedDocumentId, baseDocumentId;
2487
- if (isNewTranslation) {
2488
- const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
2489
- ...docWithoutMeta,
2490
- _type: sourceDocument._type,
2491
- [languageFieldName]: targetLanguage
2492
- };
2493
- devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
2494
- actionType: "sanity.action.document.version.create",
2495
- publishedId: baseDocumentId,
2496
- document: {
2497
- ...newTranslationDoc,
2498
- _type: sourceDocument._type,
2499
- _id: `versions.${releaseId}.${baseDocumentId}`
2500
- }
2501
- }), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
2502
- ...logContext,
2503
- versionedDocumentId,
2504
- baseDocumentId
2505
- });
2506
- } else {
2507
- const existingTranslationId = translationTarget._id;
2508
- if (!existingTranslationId)
2509
- throw new Error("Translation target ID not found for update operation");
2510
- devLog.info("Creating version of existing translation in release", {
2511
- ...logContext,
2512
- existingTranslationId
2513
- });
2514
- let existingDoc = await client.getDocument(existingTranslationId), actualDocumentId = existingTranslationId;
2515
- if (!existingDoc) {
2516
- const alternateId = existingTranslationId.startsWith("drafts.") ? undraftId(existingTranslationId) : draftId(existingTranslationId);
2517
- devLog.info("Document not found with stored ID, trying alternate ID", {
2518
- ...logContext,
2519
- storedId: existingTranslationId,
2520
- alternateId
2521
- }), existingDoc = await client.getDocument(alternateId), existingDoc && (actualDocumentId = alternateId, devLog.info("Found document with alternate ID", {
2522
- ...logContext,
2523
- actualDocumentId
2524
- }));
2525
- }
2526
- if (!existingDoc)
2527
- throw new Error(
2528
- `Existing translation document not found: ${existingTranslationId} (also tried ${existingTranslationId.startsWith("drafts.") ? undraftId(existingTranslationId) : draftId(existingTranslationId)})`
2529
- );
2530
- const publishedId = undraftId(actualDocumentId);
2531
- await client.action({
2532
- actionType: "sanity.action.document.version.create",
2533
- publishedId,
2534
- document: {
2535
- ...existingDoc,
2536
- _id: `versions.${releaseId}.${publishedId}`
2537
- }
2538
- }), versionedDocumentId = `versions.${releaseId}.${publishedId}`, baseDocumentId = publishedId, devLog.info("Existing translation version created", {
2539
- ...logContext,
2540
- versionedDocumentId,
2541
- baseDocumentId
2542
- });
2543
- const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
2544
- await client.createOrReplace({
2545
- ...sourceContent,
2546
- _id: versionedDocumentId,
2547
- [languageFieldName]: targetLanguage
2548
- }), devLog.info("Source content cloned onto version", {
2549
- ...logContext,
2550
- versionedDocumentId
2551
- });
2552
- }
2553
- let translatedDocument;
2554
- switch (translationMode) {
2555
- case "openai-only": {
2556
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2557
- throw new Error("OpenAI is not configured but openai-only mode was selected");
2558
- devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
2559
- const versionedDoc = await client.getDocument(versionedDocumentId);
2560
- if (!versionedDoc)
2561
- throw new Error(`Versioned document not found: ${versionedDocumentId}`);
2562
- const translatedDoc = await translateInPlaceWithOpenAI({
2563
- dispatch,
2564
- sourceDocument,
2565
- targetDocument: versionedDoc,
2566
- targetLanguage,
2567
- languageFieldName,
2568
- translationPrompts,
2569
- openaiConfig,
2570
- isReview: !1
2571
- });
2572
- translatedDoc && (translatedDocument = await client.createOrReplace({
2573
- ...translatedDoc,
2574
- _id: versionedDocumentId
2575
- }));
2576
- break;
2577
- }
2578
- case "sanity-with-review":
2579
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2580
- throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
2581
- if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
2582
- dispatch,
2583
- client,
2584
- schemaId,
2585
- sourceDocument,
2586
- targetLanguage,
2587
- languageFieldName,
2588
- versionedDocumentId,
2589
- translationPrompts
2590
- }), translatedDocument) {
2591
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
2592
- const reviewedDoc = await translateInPlaceWithOpenAI({
2593
- dispatch,
2594
- sourceDocument,
2595
- targetDocument: translatedDocument,
2596
- targetLanguage,
2597
- languageFieldName,
2598
- translationPrompts,
2599
- openaiConfig,
2600
- isReview: !0
2601
- });
2602
- reviewedDoc && (translatedDocument = await client.createOrReplace({
2603
- ...reviewedDoc,
2604
- _id: versionedDocumentId
2605
- }));
2606
- }
2607
- break;
2608
- default:
2609
- devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
2610
- dispatch,
2611
- client,
2612
- schemaId,
2613
- sourceDocument,
2614
- targetLanguage,
2615
- languageFieldName,
2616
- versionedDocumentId,
2617
- translationPrompts
2618
- });
2619
- break;
2620
- }
2621
- return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
2622
- ...translatedDocument,
2623
- _id: baseDocumentId
2624
- } : void 0;
2625
- } catch (error) {
2626
- const errorMessage = error instanceof Error ? error.message : String(error);
2627
- devLog.error(
2628
- "translateEntireDocument failed",
2629
- error instanceof Error ? error : new Error(String(error)),
2630
- logContext
2631
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2632
- errorMessage
2633
- }), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
2634
- return;
2635
- }
2636
- }, translateWithSanityAgent = async ({
2637
- dispatch,
2638
- client,
2639
- schemaId,
2640
- sourceDocument,
2641
2207
  targetLanguage,
2642
- languageFieldName,
2643
- versionedDocumentId,
2644
- translationPrompts
2645
- }) => {
2646
- const logContext = {
2647
- documentId: sourceDocument._id,
2648
- targetLanguage,
2649
- versionedDocumentId
2650
- };
2651
- try {
2652
- const { toLanguage, fromLanguage } = assembleLanguageSpecs({
2653
- sourceDocument,
2654
- languageFieldName,
2655
- targetLanguage
2656
- });
2657
- devLog.debug("Language specs assembled", {
2658
- ...logContext,
2659
- toLanguage,
2660
- fromLanguage
2661
- });
2662
- const documentAnalysis = analyzeDocumentContent(sourceDocument);
2663
- devLog.debug("Document analysis completed", {
2664
- ...logContext,
2665
- analysis: documentAnalysis
2666
- }), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
2667
- const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
2668
- {
2669
- documentAnalysis,
2670
- isUpdate: !1,
2671
- // Always fresh translation in a version
2672
- targetLanguage,
2673
- sourceLanguage: sourceDocument[languageFieldName]
2674
- },
2675
- {
2676
- globalPrompt: translationPrompts.globalPrompt,
2677
- localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2678
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2679
- protectedPhrases: translationPrompts.protectedPhrases
2680
- }
2681
- );
2682
- devLog.info("Structured style guide generated", {
2683
- ...logContext,
2684
- styleGuideLength: styleGuide.length,
2685
- omittedSections,
2686
- truncatedSections
2687
- }), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
2688
- ...logContext,
2689
- omittedSections,
2690
- truncatedSections
2691
- });
2692
- const newDocument = await client.agent.action.translate({
2693
- schemaId,
2694
- toLanguage,
2695
- fromLanguage,
2696
- documentId: versionedDocumentId,
2697
- // Use versioned document ID
2698
- languageFieldPath: [languageFieldName],
2699
- styleGuide,
2700
- protectedPhrases: translationPrompts.protectedPhrases || [],
2701
- conditionalPaths: {
2702
- defaultHidden: !1,
2703
- defaultReadOnly: !1
2704
- }
2705
- });
2706
- return devLog.info("Sanity agent translation completed", {
2707
- ...logContext,
2708
- newDocumentId: newDocument._id,
2709
- newDocumentRev: newDocument._rev
2710
- }), newDocument;
2711
- } catch (error) {
2712
- const errorMessage = error instanceof Error ? error.message : String(error);
2713
- devLog.error(
2714
- "Sanity agent translation failed",
2715
- error instanceof Error ? error : new Error(String(error)),
2716
- logContext
2717
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2718
- errorMessage
2719
- });
2720
- return;
2721
- }
2722
- }, translateInPlaceWithOpenAI = async ({
2723
- dispatch,
2724
- sourceDocument,
2725
- targetDocument,
2726
- targetLanguage,
2727
- languageFieldName,
2728
- translationPrompts,
2729
- openaiConfig,
2730
- isReview
2208
+ openaiConfig
2731
2209
  }) => {
2732
2210
  const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2733
2211
  sourceId: sourceDocument._id,
2734
- targetId: targetDocument._id,
2735
- targetLanguage,
2736
- sourceLanguage,
2737
- isReview
2212
+ sourceLanguage
2738
2213
  };
2739
2214
  try {
2740
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2741
- devLog.debug("Documents serialized for OpenAI", {
2742
- ...logContext,
2743
- sourceLength: sourceText.length,
2744
- targetLength: targetText.length
2745
- });
2746
- const prompt = isReview ? buildReviewPrompt({
2215
+ const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt$1({
2747
2216
  sourceText,
2748
- translatedText: targetText,
2749
2217
  sourceLanguage,
2750
2218
  targetLanguage,
2751
2219
  translationPrompts
2752
- }) : buildTranslationPrompt({
2753
- sourceText,
2754
- sourceLanguage,
2755
- targetLanguage,
2756
- translationPrompts
2757
- });
2758
- devLog.debug("Calling GPT-5", {
2759
- ...logContext,
2760
- promptLength: prompt.length
2761
- });
2762
- const response = await callGPT5(openaiConfig.apiKey, {
2220
+ }), response = await callGPT5(openaiConfig.apiKey, {
2763
2221
  messages: [
2764
2222
  {
2765
- role: "system",
2766
- 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."
2223
+ role: "developer",
2224
+ content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
2767
2225
  },
2768
2226
  { role: "user", content: prompt }
2769
2227
  ],
2770
2228
  model: openaiConfig.model,
2771
- reasoningEffort: openaiConfig.reasoningEffort,
2772
- verbosity: openaiConfig.verbosity,
2773
- onReasoningChunk: (reasoning) => {
2774
- const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
2775
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
2776
- reasoningProgress: reasoning
2777
- });
2778
- }
2779
- });
2780
- devLog.info("OpenAI operation completed", {
2781
- ...logContext,
2782
- tokensUsed: response.tokensUsed.total,
2783
- cost: response.cost
2784
- });
2785
- const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
2229
+ reasoningEffort: openaiConfig.reasoningEffort
2230
+ }), result = deserializeDocument(sourceDocument, response.content), validation = validateStructureIntegrity(sourceDocument, result);
2786
2231
  if (!validation.valid) {
2787
2232
  devLog.error("Structure validation failed", {
2788
2233
  ...logContext,
@@ -2796,17 +2241,14 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2796
2241
  `Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
2797
2242
  );
2798
2243
  }
2799
- devLog.info("Structure validation passed", logContext);
2800
- const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
2801
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
2244
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2802
2245
  tokensUsed: response.tokensUsed.total,
2803
- estimatedCost: response.cost,
2804
- reasoningSummary: response.reasoningSummary
2246
+ estimatedCost: response.cost
2805
2247
  }), result;
2806
2248
  } catch (error) {
2807
2249
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2808
2250
  }
2809
- }, buildTranslationPrompt = ({
2251
+ }, buildTranslationPrompt$1 = ({
2810
2252
  sourceText,
2811
2253
  sourceLanguage,
2812
2254
  targetLanguage,
@@ -2853,73 +2295,175 @@ Brief summary of key translation decisions and reasoning
2853
2295
  </reasoning>
2854
2296
 
2855
2297
  Then return the translated document:`;
2856
- }, buildReviewPrompt = ({
2857
- sourceText,
2858
- translatedText,
2298
+ }, assembleLanguageSpecs = ({
2299
+ sourceDocument,
2300
+ languageFieldName,
2301
+ targetLanguage
2302
+ }) => {
2303
+ const sourceLanugageId = sourceDocument[languageFieldName];
2304
+ if (!sourceLanugageId)
2305
+ throw new Error("[assembleLanguages] Source language ID is missing");
2306
+ return {
2307
+ fromLanguage: {
2308
+ id: sourceLanugageId,
2309
+ title: sourceLanugageId
2310
+ },
2311
+ toLanguage: {
2312
+ id: targetLanguage,
2313
+ title: targetLanguage
2314
+ }
2315
+ };
2316
+ }, buildTranslationPrompt = ({
2859
2317
  sourceLanguage,
2860
2318
  targetLanguage,
2861
- translationPrompts
2319
+ components,
2320
+ isUpdate = !1
2862
2321
  }) => {
2863
- const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2864
- return `# TRANSLATION QUALITY REVIEW
2865
-
2866
- Review this ${targetLanguage} translation and return a corrected version.
2322
+ const sections = [];
2323
+ 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));
2324
+ const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2325
+ return styleSection && sections.push(styleSection), sections.join(`
2867
2326
 
2868
- ## Review Criteria
2327
+ ---
2869
2328
 
2870
- 1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
2871
- 2. **Terminology**: Are glossary terms used correctly?
2872
- 3. **Protected Phrases**: Are protected phrases preserved?
2873
- 4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
2874
- 5. **Style**: Does it follow the style guide?
2875
- 6. **Completeness**: Is all content translated?
2329
+ `);
2330
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2876
2331
 
2877
- ## Guidelines
2332
+ - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2333
+ - Scan each field once, then move directly into translation work.
2334
+ - Plan updates before editing so document structure remains intact.
2335
+ - Default to medium reasoning effort and escalate only for ambiguous passages.
2336
+ - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
2337
+ const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2338
+ return `# CRITICAL TRANSLATION RULES
2878
2339
 
2879
- ${globalPrompt ? `### General Guidelines
2880
- ${globalPrompt}
2881
- ` : ""}
2882
- ${localePrompt ? `### ${targetLanguage} Guidelines
2883
- ${localePrompt}
2884
- ` : ""}
2885
- ${glossaryTerms ? `### Required Terminology
2886
- ${glossaryTerms}
2887
- ` : ""}
2888
- ${protectedPhrases?.length ? `### Protected Phrases (must preserve)
2889
- ${protectedPhrases.join(", ")}
2890
- ` : ""}
2340
+ Follow these without exception:
2891
2341
 
2892
- ## Source Document (${sourceLanguage})
2342
+ 1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
2343
+ 2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
2344
+ 3. Maintain all layout, markdown, rich-text marks, and data structure.
2345
+ 4. Never invent metadata or field values that are absent in the source.
2346
+ 5. Leave technical tokens (code, IDs, filenames) untouched.
2347
+ 6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2348
+ }, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
2893
2349
 
2894
- \`\`\`
2895
- ${sourceText}
2896
- \`\`\`
2350
+ This document already has a translation; only adjust fields that changed.
2897
2351
 
2898
- ## Current Translation (${targetLanguage})
2352
+ - Compare the latest source to the existing target text.
2353
+ - Keep prior terminology and style unless the source demands a change.
2354
+ - Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2899
2355
 
2900
- \`\`\`
2901
- ${translatedText}
2902
- \`\`\`
2356
+ Apply these mappings immediately after protected phrases and before style preferences.
2903
2357
 
2904
- ## Instructions
2358
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2905
2359
 
2906
- 1. Review the translation for quality issues
2907
- 2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
2908
- 3. Keep good translations as-is - only fix what needs fixing
2909
- 4. Return the corrected translation in THE EXACT SAME FORMAT
2910
- 5. **CRITICAL**: Only modify text content between tags
2911
- 6. Never add, remove, or change any tags, attributes, or structure
2912
-
2913
- If you made corrections or have insights about the translation quality, include a brief summary:
2914
- <reasoning>
2915
- Brief summary of review findings and corrections made
2916
- </reasoning>
2360
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2917
2361
 
2918
- Then return the corrected translation document:`;
2362
+ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2363
+ const components = [];
2364
+ return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2365
+ "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2366
+ ), 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(`
2367
+ `) : null;
2368
+ }, getSanityAgentTranslation = async ({
2369
+ sourceDocument,
2370
+ targetLanguage,
2371
+ languageFieldName,
2372
+ schemaId,
2373
+ client,
2374
+ translationPrompts
2375
+ }) => {
2376
+ const styleGuide = buildTranslationPrompt({
2377
+ sourceLanguage: sourceDocument[languageFieldName],
2378
+ targetLanguage,
2379
+ isUpdate: !1,
2380
+ // Always fresh translation in a version
2381
+ components: {
2382
+ globalPrompt: translationPrompts.globalPrompt,
2383
+ localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2384
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2385
+ protectedPhrases: translationPrompts.protectedPhrases
2386
+ }
2387
+ }), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2388
+ sourceDocument,
2389
+ languageFieldName,
2390
+ targetLanguage
2391
+ });
2392
+ return await client.agent.action.translate({
2393
+ schemaId,
2394
+ toLanguage,
2395
+ fromLanguage,
2396
+ documentId: sourceDocument._id,
2397
+ noWrite: !0,
2398
+ // Do not write to the document, just return the translated content
2399
+ languageFieldPath: [languageFieldName],
2400
+ styleGuide,
2401
+ protectedPhrases: translationPrompts.protectedPhrases,
2402
+ conditionalPaths: {
2403
+ defaultHidden: !1,
2404
+ defaultReadOnly: !1
2405
+ }
2406
+ });
2407
+ }, handleTranslation = async ({
2408
+ dispatch,
2409
+ client,
2410
+ schemaId,
2411
+ sourceDocument,
2412
+ targetLanguage,
2413
+ existingTranslationId,
2414
+ languageFieldName,
2415
+ translationPrompts,
2416
+ translationMode,
2417
+ openaiConfig,
2418
+ releaseId
2419
+ }) => {
2420
+ let translatedDoc;
2421
+ switch (translationMode) {
2422
+ case "sanity-only": {
2423
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2424
+ sourceDocument,
2425
+ targetLanguage,
2426
+ languageFieldName,
2427
+ schemaId,
2428
+ client,
2429
+ translationPrompts
2430
+ });
2431
+ break;
2432
+ }
2433
+ case "openai-only": {
2434
+ if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2435
+ throw new Error("OpenAI is not configured but openai-only mode was selected");
2436
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2437
+ dispatch,
2438
+ sourceDocument,
2439
+ targetLanguage,
2440
+ languageFieldName,
2441
+ translationPrompts,
2442
+ openaiConfig
2443
+ });
2444
+ break;
2445
+ }
2446
+ case "sanity-with-review":
2447
+ throw new Error("Sanity with review translation method is not implemented");
2448
+ default:
2449
+ throw new Error(`Invalid translation mode: ${translationMode}`);
2450
+ }
2451
+ if (!translatedDoc)
2452
+ return null;
2453
+ const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2454
+ ...translatedDoc,
2455
+ _id: targetDocumentVersionId
2456
+ };
2457
+ return await client.action({
2458
+ actionType: "sanity.action.document.version.create",
2459
+ publishedId: targetDocumentPublishedId,
2460
+ document: newDoc
2461
+ }), newDoc;
2919
2462
  }, translateDocument = async ({
2920
2463
  dispatch,
2921
2464
  client,
2922
2465
  sourceDocument,
2466
+ sourceLanguage,
2923
2467
  targetLanguage,
2924
2468
  languageFieldName,
2925
2469
  shouldOverwriteFreshTranslations,
@@ -2938,62 +2482,37 @@ Then return the corrected translation document:`;
2938
2482
  };
2939
2483
  devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
2940
2484
  try {
2941
- const sourceLanguage = sourceDocument[languageFieldName];
2942
- if (devLog.debug("Checking source document language", {
2943
- ...logContext,
2944
- sourceLanguage,
2945
- languageFieldName
2946
- }), !sourceLanguage) {
2947
- const error = `Source document ${sourceDocument._id} does not have a language`;
2948
- throw devLog.error(error, void 0, logContext), new Error(error);
2949
- }
2950
- devLog.debug("Getting translation metadata", logContext);
2951
2485
  const translationMetadata = await getTranslationMetadata({
2952
2486
  client,
2953
2487
  sourceDocument,
2954
2488
  sourceLanguage
2955
2489
  }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
2956
2490
  (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
2957
- ) ?? !1;
2958
- if (devLog.debug("Checking if translation is already fresh", {
2959
- ...logContext,
2960
- isAlreadyFreshTranslation,
2961
- currentRev: sourceDocument._rev
2962
- }), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2963
- devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2491
+ );
2492
+ if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
2493
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
2964
2494
  targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
2965
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2495
+ });
2966
2496
  return;
2967
2497
  }
2968
- const translationTarget = determineTranslationTarget({
2969
- translationMetadata,
2970
- targetLanguage
2971
- }), isNewTranslation = translationTarget.operation === "create";
2972
- devLog.debug("Translation target determined", {
2973
- ...logContext,
2974
- operation: translationTarget.operation,
2975
- isNewTranslation
2976
- });
2977
- const newDocument = await translateEntireDocument({
2498
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2978
2499
  dispatch,
2979
2500
  client,
2980
2501
  schemaId,
2981
2502
  sourceDocument,
2982
2503
  targetLanguage,
2504
+ existingTranslationId,
2983
2505
  languageFieldName,
2984
- translationTarget,
2985
2506
  translationPrompts,
2986
2507
  translationMode,
2987
2508
  openaiConfig,
2988
- releaseId,
2989
- isNewTranslation
2509
+ releaseId
2990
2510
  });
2991
- if (!newDocument)
2992
- 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");
2993
- return devLog.debug("Translation document created", {
2994
- ...logContext,
2995
- newDocumentId: newDocument._id
2996
- }), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2511
+ if (!newDocument) {
2512
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2513
+ return;
2514
+ }
2515
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2997
2516
  targetId: newDocument._id
2998
2517
  }), await updateTranslationMetadata({
2999
2518
  client,
@@ -3002,8 +2521,10 @@ Then return the corrected translation document:`;
3002
2521
  newDocument,
3003
2522
  targetLanguage,
3004
2523
  translationMetadata,
3005
- isNewTranslation
3006
- }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"), devLog.info("Translation completed successfully", {
2524
+ isNewTranslation: !existingTranslationId
2525
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
2526
+ targetId: newDocument._id
2527
+ }), devLog.info("Translation completed successfully", {
3007
2528
  ...logContext,
3008
2529
  newDocumentId: newDocument._id
3009
2530
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
@@ -3029,40 +2550,32 @@ Then return the corrected translation document:`;
3029
2550
  }) => {
3030
2551
  const logContext = {
3031
2552
  documentId: sourceDocument._id,
2553
+ documentRev: sourceDocument._rev,
3032
2554
  targetLanguages,
3033
2555
  referencesCount: references.length,
3034
2556
  maxConcurrentJobs: maxConcurrentTranslationJobs,
3035
2557
  translationMode,
3036
2558
  shouldOverwriteFreshTranslations
3037
2559
  };
3038
- devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
3039
2560
  let releaseId;
3040
2561
  try {
3041
2562
  const sourceLanguage = sourceDocument[languageFieldName];
3042
2563
  if (!sourceLanguage)
3043
2564
  throw devLog.error("Source language not found"), new Error("Source language not found");
3044
- maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
3045
- ...logContext,
3046
- newPermits: maxConcurrentTranslationJobs
3047
- }), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
2565
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
3048
2566
  type: "SET_OVERALL_STATUS",
3049
2567
  payload: "translating"
3050
2568
  });
3051
- 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()}`;
3052
- devLog.info("Creating Content Release for translation batch", {
3053
- ...logContext,
3054
- releaseTitle
3055
- });
3056
- const { releaseId: newReleaseId } = await client.releases.create({
2569
+ 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({
3057
2570
  metadata: {
3058
2571
  title: releaseTitle,
3059
2572
  description: releaseDescription,
3060
- releaseType: "asap"
2573
+ releaseType: "undecided"
3061
2574
  }
3062
2575
  });
3063
- releaseId = newReleaseId, devLog.info("Content Release created", {
2576
+ devLog.info("Content Release created", {
3064
2577
  ...logContext,
3065
- releaseId
2578
+ releaseId: releaseId2
3066
2579
  });
3067
2580
  const sourceDocuments = [
3068
2581
  {
@@ -3072,34 +2585,26 @@ Then return the corrected translation document:`;
3072
2585
  title: sourceDocument.name || sourceDocument.title
3073
2586
  },
3074
2587
  ...references
3075
- ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), targetDocumentIds = [];
3076
- devLog.debug("Prepared document set for translation", {
3077
- ...logContext,
3078
- sourceDocumentIds: sourceDocumentIds.length,
3079
- totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
3080
- });
3081
- const translationPrompts = await fetchTranslationPrompts({
2588
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
3082
2589
  client,
3083
2590
  sourceLanguage,
3084
2591
  targetLanguages,
3085
2592
  sourceDocument
3086
2593
  });
3087
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2594
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
3088
2595
  const translationTasks = sourceDocumentIds.flatMap(
3089
2596
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3090
2597
  try {
3091
2598
  await translationSemaphore.withPermit(async () => {
3092
2599
  const document = await client.withConfig({
3093
- // This enables us to detect documents that might be stale due to unpublished changes
3094
- perspective: "raw"
3095
- }).fetch("*[_id == $id][0]", {
3096
- id: documentId
3097
- });
3098
- if (document && releaseId) {
3099
- const targetDocumentId = await translateDocument({
2600
+ perspective: "drafts"
2601
+ }).getDocument(documentId);
2602
+ if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2603
+ const versionedTargetDocumentId = await translateDocument({
3100
2604
  dispatch,
3101
2605
  client,
3102
2606
  sourceDocument: document,
2607
+ sourceLanguage,
3103
2608
  targetLanguage,
3104
2609
  languageFieldName,
3105
2610
  shouldOverwriteFreshTranslations,
@@ -3107,9 +2612,9 @@ Then return the corrected translation document:`;
3107
2612
  translationPrompts,
3108
2613
  translationMode,
3109
2614
  openaiConfig,
3110
- releaseId
2615
+ releaseId: releaseId2
3111
2616
  });
3112
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
2617
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
3113
2618
  }
3114
2619
  });
3115
2620
  } catch (error) {
@@ -3125,25 +2630,21 @@ Then return the corrected translation document:`;
3125
2630
  }
3126
2631
  })
3127
2632
  ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
3128
- devLog.info("Translation tasks summary", {
2633
+ devLog.info("Translation tasks completed", {
3129
2634
  ...logContext,
3130
2635
  total: results.length,
3131
2636
  succeeded,
3132
- failed
3133
- }), devLog.info("All translation tasks completed", {
3134
- ...logContext,
3135
- targetDocumentIds: targetDocumentIds.length
2637
+ failed,
2638
+ targetDocuments: versionedTargetDocumentIds.length
3136
2639
  }), dispatch({
3137
2640
  type: "SET_OVERALL_STATUS",
3138
2641
  payload: "linking_references"
3139
- }), devLog.debug("Starting reference localization", {
3140
- ...logContext,
3141
- documentsToLocalize: targetDocumentIds.length
3142
2642
  }), await Promise.all(
3143
- targetDocumentIds.map(async (documentId) => {
2643
+ versionedTargetDocumentIds.map(async (documentId) => {
3144
2644
  await localizeReferences({
3145
2645
  client,
3146
2646
  documentId,
2647
+ sourceLanguage,
3147
2648
  languageFieldName
3148
2649
  });
3149
2650
  })
@@ -3153,26 +2654,13 @@ Then return the corrected translation document:`;
3153
2654
  ...logContext,
3154
2655
  succeeded,
3155
2656
  failed
3156
- }), 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();
2657
+ }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
3157
2658
  } catch (error) {
3158
- if (devLog.error(
2659
+ throw devLog.error(
3159
2660
  "translateDocumentSet failed with unexpected error",
3160
2661
  error instanceof Error ? error : new Error(String(error)),
3161
2662
  logContext
3162
- ), releaseId)
3163
- try {
3164
- await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
3165
- ...logContext,
3166
- releaseId
3167
- });
3168
- } catch (cleanupError) {
3169
- devLog.error(
3170
- "Failed to cleanup release",
3171
- cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
3172
- logContext
3173
- );
3174
- }
3175
- throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
2663
+ ), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
3176
2664
  }
3177
2665
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
3178
2666
  const {
@@ -3593,24 +3081,12 @@ Then return the corrected translation document:`;
3593
3081
  " \u2014 ",
3594
3082
  job._id.slice(-6)
3595
3083
  ] }),
3596
- !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3597
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3598
- job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3599
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
3600
- /* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
3601
- ] }) })
3602
- ] }),
3603
- job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3604
- /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3605
- job.tokensUsed.toLocaleString(),
3606
- " tokens (~$",
3607
- job.estimatedCost?.toFixed(4),
3608
- ")"
3609
- ] }),
3610
- job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3611
- "\u{1F4A1} ",
3612
- job.reasoningSummary
3613
- ] }) })
3084
+ !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3085
+ job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3086
+ job.tokensUsed.toLocaleString(),
3087
+ " tokens (~$",
3088
+ job.estimatedCost?.toFixed(4),
3089
+ ")"
3614
3090
  ] }) : null,
3615
3091
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3616
3092
  "Error: ",
@@ -3724,26 +3200,9 @@ Then return the corrected translation document:`;
3724
3200
  "completed"
3725
3201
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3726
3202
  const { overallStatus, document } = useTranslationContext();
3727
- return devLog.debug("TranslationDialog rendered", {
3728
- documentId: document._id,
3729
- overallStatus,
3730
- component: "TranslationDialog"
3731
- }), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
3732
- documentId: document._id,
3733
- overallStatus,
3734
- component: "TranslationDialog"
3735
- }), /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen })) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? (devLog.debug("Showing translation progress panel", {
3736
- documentId: document._id,
3737
- overallStatus,
3738
- component: "TranslationDialog"
3739
- }), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
3203
+ 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", {
3740
3204
  documentId: document._id,
3741
- overallStatus,
3742
- component: "TranslationDialog"
3743
- }), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
3744
- documentId: document._id,
3745
- overallStatus,
3746
- component: "TranslationDialog"
3205
+ overallStatus
3747
3206
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3748
3207
  }, getTranslateAction = (config) => ({ draft, published }) => {
3749
3208
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);