@braze/sanity-plugin-ai-translation 0.2.23 → 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, getPublishedId, 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";
@@ -192,12 +192,7 @@ async function analyzeFeedback({
192
192
  currentDocument,
193
193
  onProgress
194
194
  }) {
195
- devLog.info("Starting feedback analysis", {
196
- sourceLanguage,
197
- targetLanguage,
198
- hasUserFeedback: !!userFeedback,
199
- shouldIncludeEdits
200
- }), onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
195
+ onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
201
196
  const analysisPrompt = buildAnalysisPrompt({
202
197
  sourceLanguage,
203
198
  targetLanguage,
@@ -206,10 +201,8 @@ async function analyzeFeedback({
206
201
  sourceSnapshot,
207
202
  targetSnapshot,
208
203
  currentDocument
209
- }), analysis = await analyzeWithAI(client, analysisPrompt);
210
- devLog.info("AI analysis complete", { analysis });
211
- const suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
212
- 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..." });
213
206
  const validatedSuggestions = await Promise.all(
214
207
  suggestions.map(async (suggestion) => {
215
208
  const validation = await validateSuggestion(client, suggestion);
@@ -222,7 +215,10 @@ async function analyzeFeedback({
222
215
  };
223
216
  })
224
217
  );
225
- return devLog.info("Suggestions validated", {
218
+ return devLog.info("Feedback analysis complete", {
219
+ sourceLanguage,
220
+ targetLanguage,
221
+ total: validatedSuggestions.length,
226
222
  valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
227
223
  warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
228
224
  invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
@@ -236,24 +232,18 @@ async function applySuggestions({
236
232
  suggestions,
237
233
  onProgress
238
234
  }) {
239
- if (devLog.info("Applying suggestions", {
240
- sourceLanguage,
241
- targetLanguage,
242
- targetLanguageType: typeof targetLanguage,
243
- targetLanguageValue: targetLanguage,
244
- count: suggestions.filter((s) => s.approved).length
245
- }), !targetLanguage)
235
+ if (!targetLanguage)
246
236
  throw devLog.error("targetLanguage is undefined in applySuggestions!", {
247
237
  sourceLanguage,
248
238
  targetLanguage,
249
239
  suggestionsCount: suggestions.length
250
240
  }), new Error("targetLanguage is required but was undefined");
251
241
  const approvedSuggestions = suggestions.filter((s) => s.approved);
252
- if (approvedSuggestions.length === 0) {
253
- devLog.info("No approved suggestions to apply");
254
- return;
255
- }
256
- 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
+ }));
257
247
  }
258
248
  function buildAnalysisPrompt({
259
249
  sourceLanguage,
@@ -471,49 +461,21 @@ function parseAIResponse(responseText) {
471
461
  }
472
462
  }
473
463
  async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
474
- devLog.info("Applying improvements from suggestions", {
475
- count: suggestions.length,
476
- targetLanguage,
477
- suggestions: suggestions.map((s) => ({
478
- type: s.type,
479
- approved: s.approved,
480
- sourceLocale: s.sourceLocale,
481
- targetLocale: s.targetLocale,
482
- sourceValue: s.sourceValue,
483
- targetValue: s.targetValue
484
- }))
485
- });
486
464
  const settingsId = "translation-settings";
487
465
  await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
488
466
  _id: settingsId,
489
467
  _type: "translationSettings"
490
468
  });
491
- 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");
492
- devLog.info("Glossary terms before filtering", {
493
- count: glossaryTermsBeforeFilter.length,
494
- terms: glossaryTermsBeforeFilter.map((s) => ({
495
- sourceLocale: s.sourceLocale,
496
- sourceValue: s.sourceValue,
497
- targetLocale: s.targetLocale,
498
- targetValue: s.targetValue,
499
- approved: s.approved
500
- }))
501
- });
502
- 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(
503
470
  (s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
504
471
  );
505
- if (devLog.info("Filtered suggestions by type", {
506
- globalPrompts: globalPrompts.length,
507
- localePrompts: localePrompts.length,
508
- protectedPhrases: protectedPhrases.length,
509
- glossaryTerms: glossaryTerms.length
510
- }), (globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
472
+ if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
511
473
  stage: "applying",
512
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...`
513
475
  }), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
514
476
  onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
515
477
  const phrases = protectedPhrases.map((s) => s.description);
516
- devLog.debug("Applying protected phrases", { phrases }), updates.push(addProtectedPhrases(client, settingsId, phrases));
478
+ updates.push(addProtectedPhrases(client, settingsId, phrases));
517
479
  }
518
480
  if (glossaryTerms.length > 0) {
519
481
  onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
@@ -525,22 +487,17 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
525
487
  targetValue: s.targetValue
526
488
  // biome-ignore-end lint/style/noNonNullAssertion: safely handled
527
489
  }));
528
- devLog.debug("Applying glossary terms", { terms }), updates.push(addGlossaryTerms(client, terms));
490
+ updates.push(addGlossaryTerms(client, terms));
529
491
  }
530
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(
531
493
  client,
532
494
  settingsId,
533
495
  targetLanguage,
534
496
  suggestions.filter((s) => s.approved)
535
- ), devLog.info("All improvements applied successfully");
497
+ );
536
498
  }
537
499
  async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
538
- if (devLog.info("Updating prompts with deterministic template", {
539
- settingsId,
540
- targetLanguage,
541
- hasGlobalUpdates: globalPrompts.length > 0,
542
- hasLocaleUpdates: localePrompts.length > 0
543
- }), !targetLanguage)
500
+ if (!targetLanguage)
544
501
  throw devLog.error("targetLanguage is undefined or empty!", {
545
502
  targetLanguage,
546
503
  globalPrompts: globalPrompts.length,
@@ -556,11 +513,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
556
513
  ...updates.map((update) => `- ${update}`)
557
514
  ].filter(Boolean).join(`
558
515
  `);
559
- await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit(), devLog.info("Global prompt updated", {
560
- previousLength: currentGlobalPrompt.length,
561
- newLength: newGlobalPrompt.length,
562
- addedUpdates: updates.length
563
- });
516
+ await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
564
517
  }
565
518
  if (localePrompts.length > 0) {
566
519
  const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
@@ -573,12 +526,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
573
526
  ...updates.map((update) => `- ${update}`)
574
527
  ].filter(Boolean).join(`
575
528
  `), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
576
- await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit(), devLog.info("Locale prompt updated", {
577
- targetLanguage,
578
- previousLength: currentValue.length,
579
- newLength: newValue.length,
580
- addedUpdates: updates.length
581
- });
529
+ await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
582
530
  } else {
583
531
  const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
584
532
  `);
@@ -588,22 +536,17 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
588
536
  _key: targetLanguage,
589
537
  value: newValue
590
538
  }
591
- ]).commit(), devLog.info("Locale prompt created", {
592
- targetLanguage,
593
- length: newValue.length,
594
- addedUpdates: updates.length
595
- });
539
+ ]).commit();
596
540
  }
597
541
  }
598
542
  devLog.info("Prompts updated successfully", {
543
+ targetLanguage,
599
544
  globalUpdated: globalPrompts.length > 0,
600
545
  localeUpdated: localePrompts.length > 0
601
546
  });
602
547
  }
603
548
  async function addProtectedPhrases(client, settingsId, phrases) {
604
- devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
605
549
  const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
606
- devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
607
550
  let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
608
551
  const conflictingPhrases = [];
609
552
  for (const phrase of newPhrases)
@@ -611,12 +554,7 @@ async function addProtectedPhrases(client, settingsId, phrases) {
611
554
  'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
612
555
  { phrase }
613
556
  ) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
614
- if (newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length === 0) {
615
- devLog.info("No new protected phrases to add (all already exist)");
616
- return;
617
- }
618
- const result = await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit();
619
- 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 }));
620
558
  }
621
559
  async function addGlossaryTerms(client, terms) {
622
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)", {
@@ -644,29 +582,13 @@ async function addGlossaryTerms(client, terms) {
644
582
  );
645
583
  if (existingTerm) {
646
584
  const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
647
- existingTargetIndex === -1 ? (await client.patch(existingTerm._id).append("translations", [
585
+ existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
648
586
  {
649
587
  _type: "internationalizedArrayStringValue",
650
588
  _key: targetLocale,
651
589
  value: targetValue
652
590
  }
653
- ]).commit(), devLog.info("Added target language to existing glossary term", {
654
- sourceLocale,
655
- sourceValue,
656
- targetLocale,
657
- targetValue
658
- })) : existingTargetValue !== targetValue ? (await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit(), devLog.info("Updated existing glossary term translation", {
659
- sourceLocale,
660
- sourceValue,
661
- targetLocale,
662
- oldValue: existingTargetValue,
663
- newValue: targetValue
664
- })) : devLog.info("Glossary term already has this exact translation", {
665
- sourceLocale,
666
- sourceValue,
667
- targetLocale,
668
- targetValue
669
- });
591
+ ]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
670
592
  } else
671
593
  await client.create({
672
594
  _type: "translationGlossaryTerm",
@@ -682,11 +604,10 @@ async function addGlossaryTerms(client, terms) {
682
604
  value: targetValue
683
605
  }
684
606
  ]
685
- }), devLog.info("Created new glossary term", { sourceLocale, sourceValue, targetLocale, targetValue });
607
+ });
686
608
  }
687
609
  devLog.info("Glossary terms processed", {
688
- totalRequested: terms.length,
689
- validAdded: validTerms.length,
610
+ processed: validTerms.length,
690
611
  skipped: terms.length - validTerms.length
691
612
  });
692
613
  }
@@ -699,10 +620,7 @@ async function recordChangeHistory(client, settingsId, targetLanguage, suggestio
699
620
  description: suggestion.description,
700
621
  locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
701
622
  }));
702
- historyEntries.length !== 0 && (await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit(), devLog.info("Change history recorded", {
703
- count: historyEntries.length,
704
- timestamp
705
- }));
623
+ historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
706
624
  }
707
625
  const ICON_SIZE = 64, progressIconMap = {
708
626
  in_progress: /* @__PURE__ */ jsx(Spinner, {}),
@@ -1756,7 +1674,12 @@ const fetchTranslationPrompts = async ({
1756
1674
  }
1757
1675
  };
1758
1676
  return recursiveSearch(obj, []), results;
1759
- }, localizeReferences = async ({ client, documentId, languageFieldName }) => {
1677
+ }, localizeReferences = async ({
1678
+ client,
1679
+ documentId,
1680
+ sourceLanguage,
1681
+ languageFieldName
1682
+ }) => {
1760
1683
  try {
1761
1684
  const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
1762
1685
  if (!doc) {
@@ -1778,25 +1701,25 @@ const fetchTranslationPrompts = async ({
1778
1701
  const refs = findKeyValues(doc, "_ref");
1779
1702
  if (!refs?.length)
1780
1703
  return;
1781
- await updateReferences(documentId, client, refs, targetLanguage);
1704
+ await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
1782
1705
  } catch (error) {
1783
1706
  devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
1784
1707
  documentId,
1785
1708
  component: "localizeReferences"
1786
1709
  });
1787
1710
  }
1788
- }, updateReferences = async (documentId, client, refs, targetLanguage) => {
1711
+ }, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
1789
1712
  if (!refs?.length)
1790
1713
  return [];
1791
- const refIds = refs.map((r) => undraftId(r.value)), translations = await client.fetch(
1792
- `*[_type == $translationMetadataSchemaType && count(translations[value._ref in $refIds]) > 0]
1793
- .translations[value._ref in $refIds]{
1794
- "sourceId": value._ref,
1795
- "targetId": ^.translations[_key == $targetLanguage][0].value._ref
1796
- }[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
+ }`,
1797
1719
  {
1798
1720
  translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1799
1721
  refIds,
1722
+ sourceLanguage,
1800
1723
  targetLanguage
1801
1724
  }
1802
1725
  ), translationMap = /* @__PURE__ */ new Map();
@@ -1805,13 +1728,8 @@ const fetchTranslationPrompts = async ({
1805
1728
  const patch = client.patch(documentId);
1806
1729
  let hasChanges = !1;
1807
1730
  for (const ref of refs) {
1808
- const sourceId = undraftId(ref.value), newId = translationMap.get(sourceId);
1809
- newId && (patch.set({ [pathToString(ref.path)]: undraftId(newId) }), patch.set({ [`${pathToString(ref.path.slice(0, -1))}._weak`]: !0 }), patch.set({
1810
- [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1811
- _type: "reference",
1812
- _weak: !1
1813
- }
1814
- }), hasChanges = !0);
1731
+ const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
1732
+ newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), hasChanges = !0);
1815
1733
  }
1816
1734
  hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
1817
1735
  documentId,
@@ -2294,7 +2212,7 @@ const fetchTranslationPrompts = async ({
2294
2212
  sourceLanguage
2295
2213
  };
2296
2214
  try {
2297
- const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt({
2215
+ const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt$1({
2298
2216
  sourceText,
2299
2217
  sourceLanguage,
2300
2218
  targetLanguage,
@@ -2330,7 +2248,7 @@ const fetchTranslationPrompts = async ({
2330
2248
  } catch (error) {
2331
2249
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2332
2250
  }
2333
- }, buildTranslationPrompt = ({
2251
+ }, buildTranslationPrompt$1 = ({
2334
2252
  sourceText,
2335
2253
  sourceLanguage,
2336
2254
  targetLanguage,
@@ -2377,111 +2295,46 @@ Brief summary of key translation decisions and reasoning
2377
2295
  </reasoning>
2378
2296
 
2379
2297
  Then return the translated document:`;
2380
- }, analyzeDocumentContent = (document) => {
2381
- let hasCodeBlocks = !1, hasInlineCode = !1, codeBlockCount = 0, inlineCodeCount = 0;
2382
- const hasMarkdown = !1;
2383
- let hasPortableText = !1, estimatedFieldCount = 0;
2384
- const traverse = (value, depth = 0) => {
2385
- value != null && (depth > 0 && typeof value == "object" && estimatedFieldCount++, Array.isArray(value) ? value.forEach((item) => {
2386
- 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) => {
2387
- child && typeof child == "object" && "marks" in child && Array.isArray(child.marks) && child.marks.includes("code") && (hasInlineCode = !0, inlineCodeCount++);
2388
- })), traverse(item, depth + 1);
2389
- }) : typeof value == "object" && Object.values(value).forEach((val) => traverse(val, depth + 1)));
2390
- };
2391
- traverse(document);
2392
- let complexity = "simple";
2393
- return estimatedFieldCount > 50 ? complexity = "complex" : estimatedFieldCount > 20 && (complexity = "moderate"), {
2394
- hasCodeBlocks,
2395
- hasInlineCode,
2396
- codeBlockCount,
2397
- inlineCodeCount,
2398
- hasMarkdown,
2399
- hasPortableText,
2400
- complexity,
2401
- estimatedFieldCount
2402
- };
2403
- }, assembleTranslationPrompt = (context, components) => {
2404
- const sections = [
2405
- {
2406
- id: "agentDirectives",
2407
- content: buildAgentDirectivesSection(context),
2408
- required: !0,
2409
- priority: 0
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
2410
2310
  },
2411
- {
2412
- id: "criticalRules",
2413
- content: buildCriticalRulesSection(context),
2414
- required: !0,
2415
- priority: 1
2311
+ toLanguage: {
2312
+ id: targetLanguage,
2313
+ title: targetLanguage
2416
2314
  }
2417
- ];
2418
- components.glossaryTerms?.trim() && sections.push({
2419
- id: "glossaryTerms",
2420
- content: buildGlossarySection(components.glossaryTerms),
2421
- required: !1,
2422
- priority: 3
2423
- });
2315
+ };
2316
+ }, buildTranslationPrompt = ({
2317
+ sourceLanguage,
2318
+ targetLanguage,
2319
+ components,
2320
+ isUpdate = !1
2321
+ }) => {
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));
2424
2324
  const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
2425
- styleSection && sections.push({
2426
- id: "styleGuide",
2427
- content: styleSection,
2428
- required: !1,
2429
- priority: 4
2430
- }), sections.sort((a, b) => a.priority - b.priority);
2431
- let styleGuide = "";
2432
- const omittedSections = [], truncatedSections = [];
2433
- for (const section of sections) {
2434
- const prefix = styleGuide.length > 0 ? `
2325
+ return styleSection && sections.push(styleSection), sections.join(`
2435
2326
 
2436
2327
  ---
2437
2328
 
2438
- ` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
2439
- if (availableCharacters <= 0) {
2440
- if (section.required) {
2441
- const truncatedContent = truncateSection(section.content, 0);
2442
- truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
2443
- } else
2444
- omittedSections.push(section.id);
2445
- continue;
2446
- }
2447
- if (section.content.length <= availableCharacters) {
2448
- styleGuide += prefix + section.content;
2449
- continue;
2450
- }
2451
- if (section.required) {
2452
- const truncatedContent = truncateSection(section.content, availableCharacters);
2453
- styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
2454
- } else
2455
- omittedSections.push(section.id);
2456
- }
2457
- return {
2458
- styleGuide,
2459
- omittedSections,
2460
- truncatedSections
2461
- };
2462
- }, truncateSection = (content, limit) => {
2463
- if (limit <= 0)
2464
- return "";
2465
- if (content.length <= limit)
2466
- return content;
2467
- if (limit <= 1)
2468
- return content.slice(0, limit);
2469
- const ellipsis = "\u2026";
2470
- if (limit <= ellipsis.length)
2471
- return content.slice(0, limit);
2472
- const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
2473
- return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
2474
- }, buildAgentDirectivesSection = (context) => {
2475
- const { targetLanguage, sourceLanguage } = context;
2476
- return `# AGENT WORKFLOW DIRECTIVES
2329
+ `);
2330
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2477
2331
 
2478
2332
  - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2479
2333
  - Scan each field once, then move directly into translation work.
2480
2334
  - Plan updates before editing so document structure remains intact.
2481
2335
  - Default to medium reasoning effort and escalate only for ambiguous passages.
2482
- - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
2483
- }, buildCriticalRulesSection = (context) => {
2484
- const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
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";
2485
2338
  return `# CRITICAL TRANSLATION RULES
2486
2339
 
2487
2340
  Follow these without exception:
@@ -2492,34 +2345,26 @@ Follow these without exception:
2492
2345
  4. Never invent metadata or field values that are absent in the source.
2493
2346
  5. Leave technical tokens (code, IDs, filenames) untouched.
2494
2347
  6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2495
- }, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2348
+ }, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
2349
+
2350
+ This document already has a translation; only adjust fields that changed.
2351
+
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
2496
2355
 
2497
2356
  Apply these mappings immediately after protected phrases and before style preferences.
2498
2357
 
2499
- ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2358
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2359
+
2360
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2361
+
2362
+ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2500
2363
  const components = [];
2501
2364
  return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2502
2365
  "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2503
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(`
2504
2367
  `) : null;
2505
- }, assembleLanguageSpecs = ({
2506
- sourceDocument,
2507
- languageFieldName,
2508
- targetLanguage
2509
- }) => {
2510
- const sourceLanugageId = sourceDocument[languageFieldName];
2511
- if (!sourceLanugageId)
2512
- throw new Error("[assembleLanguages] Source language ID is missing");
2513
- return {
2514
- fromLanguage: {
2515
- id: sourceLanugageId,
2516
- title: sourceLanugageId
2517
- },
2518
- toLanguage: {
2519
- id: targetLanguage,
2520
- title: targetLanguage
2521
- }
2522
- };
2523
2368
  }, getSanityAgentTranslation = async ({
2524
2369
  sourceDocument,
2525
2370
  targetLanguage,
@@ -2528,19 +2373,18 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2528
2373
  client,
2529
2374
  translationPrompts
2530
2375
  }) => {
2531
- analyzeDocumentContent(sourceDocument);
2532
- const { styleGuide } = assembleTranslationPrompt(
2533
- {
2534
- // Always fresh translation in a version
2535
- targetLanguage,
2536
- sourceLanguage: sourceDocument[languageFieldName]
2537
- },
2538
- {
2376
+ const styleGuide = buildTranslationPrompt({
2377
+ sourceLanguage: sourceDocument[languageFieldName],
2378
+ targetLanguage,
2379
+ isUpdate: !1,
2380
+ // Always fresh translation in a version
2381
+ components: {
2539
2382
  globalPrompt: translationPrompts.globalPrompt,
2540
2383
  localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2541
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms
2384
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2385
+ protectedPhrases: translationPrompts.protectedPhrases
2542
2386
  }
2543
- ), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2387
+ }), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2544
2388
  sourceDocument,
2545
2389
  languageFieldName,
2546
2390
  targetLanguage
@@ -2610,13 +2454,11 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2610
2454
  ...translatedDoc,
2611
2455
  _id: targetDocumentVersionId
2612
2456
  };
2613
- console.log("NEW DOC", newDoc);
2614
- const createdDoc = await client.action({
2457
+ return await client.action({
2615
2458
  actionType: "sanity.action.document.version.create",
2616
2459
  publishedId: targetDocumentPublishedId,
2617
2460
  document: newDoc
2618
- });
2619
- return console.log("CREATED DOC", createdDoc), newDoc;
2461
+ }), newDoc;
2620
2462
  }, translateDocument = async ({
2621
2463
  dispatch,
2622
2464
  client,
@@ -2720,29 +2562,20 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2720
2562
  const sourceLanguage = sourceDocument[languageFieldName];
2721
2563
  if (!sourceLanguage)
2722
2564
  throw devLog.error("Source language not found"), new Error("Source language not found");
2723
- maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
2724
- ...logContext,
2725
- newPermits: maxConcurrentTranslationJobs
2726
- }), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), dispatch({
2565
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2727
2566
  type: "SET_OVERALL_STATUS",
2728
2567
  payload: "translating"
2729
2568
  });
2730
- 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()}`;
2731
- devLog.info("Creating new Content Release for translation batch", {
2732
- ...logContext,
2733
- releaseTitle
2734
- });
2735
- const { releaseId: releaseId2 } = 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({
2736
2570
  metadata: {
2737
2571
  title: releaseTitle,
2738
2572
  description: releaseDescription,
2739
2573
  releaseType: "undecided"
2740
2574
  }
2741
2575
  });
2742
- devLog.info("\u2705 NEW CONTENT RELEASE CREATED", {
2576
+ devLog.info("Content Release created", {
2743
2577
  ...logContext,
2744
- releaseId: releaseId2,
2745
- releaseTitle
2578
+ releaseId: releaseId2
2746
2579
  });
2747
2580
  const sourceDocuments = [
2748
2581
  {
@@ -2752,19 +2585,13 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2752
2585
  title: sourceDocument.name || sourceDocument.title
2753
2586
  },
2754
2587
  ...references
2755
- ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), targetDocumentIds = [];
2756
- devLog.debug("Prepared document set for translation", {
2757
- ...logContext,
2758
- sourceDocumentIds: sourceDocumentIds.length,
2759
- totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
2760
- });
2761
- const translationPrompts = await fetchTranslationPrompts({
2588
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
2762
2589
  client,
2763
2590
  sourceLanguage,
2764
2591
  targetLanguages,
2765
2592
  sourceDocument
2766
2593
  });
2767
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2594
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2768
2595
  const translationTasks = sourceDocumentIds.flatMap(
2769
2596
  (documentId) => targetLanguages.map(async (targetLanguage) => {
2770
2597
  try {
@@ -2773,7 +2600,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2773
2600
  perspective: "drafts"
2774
2601
  }).getDocument(documentId);
2775
2602
  if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2776
- const targetDocumentId = await translateDocument({
2603
+ const versionedTargetDocumentId = await translateDocument({
2777
2604
  dispatch,
2778
2605
  client,
2779
2606
  sourceDocument: document,
@@ -2787,7 +2614,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2787
2614
  openaiConfig,
2788
2615
  releaseId: releaseId2
2789
2616
  });
2790
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
2617
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
2791
2618
  }
2792
2619
  });
2793
2620
  } catch (error) {
@@ -2803,25 +2630,21 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2803
2630
  }
2804
2631
  })
2805
2632
  ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
2806
- devLog.info("Translation tasks summary", {
2633
+ devLog.info("Translation tasks completed", {
2807
2634
  ...logContext,
2808
2635
  total: results.length,
2809
2636
  succeeded,
2810
- failed
2811
- }), devLog.info("All translation tasks completed", {
2812
- ...logContext,
2813
- targetDocumentIds: targetDocumentIds.length
2637
+ failed,
2638
+ targetDocuments: versionedTargetDocumentIds.length
2814
2639
  }), dispatch({
2815
2640
  type: "SET_OVERALL_STATUS",
2816
2641
  payload: "linking_references"
2817
- }), devLog.debug("Starting reference localization", {
2818
- ...logContext,
2819
- documentsToLocalize: targetDocumentIds.length
2820
2642
  }), await Promise.all(
2821
- targetDocumentIds.map(async (documentId) => {
2643
+ versionedTargetDocumentIds.map(async (documentId) => {
2822
2644
  await localizeReferences({
2823
2645
  client,
2824
2646
  documentId,
2647
+ sourceLanguage,
2825
2648
  languageFieldName
2826
2649
  });
2827
2650
  })
@@ -2831,7 +2654,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2831
2654
  ...logContext,
2832
2655
  succeeded,
2833
2656
  failed
2834
- }), 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();
2835
2658
  } catch (error) {
2836
2659
  throw devLog.error(
2837
2660
  "translateDocumentSet failed with unexpected error",
@@ -3377,26 +3200,9 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
3377
3200
  "completed"
3378
3201
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3379
3202
  const { overallStatus, document } = useTranslationContext();
3380
- return devLog.debug("TranslationDialog rendered", {
3381
- documentId: document._id,
3382
- overallStatus,
3383
- component: "TranslationDialog"
3384
- }), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
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", {
3385
3204
  documentId: document._id,
3386
- overallStatus,
3387
- component: "TranslationDialog"
3388
- }), /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen })) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? (devLog.debug("Showing translation progress panel", {
3389
- documentId: document._id,
3390
- overallStatus,
3391
- component: "TranslationDialog"
3392
- }), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
3393
- documentId: document._id,
3394
- overallStatus,
3395
- component: "TranslationDialog"
3396
- }), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
3397
- documentId: document._id,
3398
- overallStatus,
3399
- component: "TranslationDialog"
3205
+ overallStatus
3400
3206
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3401
3207
  }, getTranslateAction = (config) => ({ draft, published }) => {
3402
3208
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);