@braze/sanity-plugin-ai-translation 0.2.23 → 0.2.25

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,26 @@ 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
+ "targetType": translations[_key == $sourceLanguage][0].value -> _type
1719
+ }`,
1797
1720
  {
1798
1721
  translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1799
1722
  refIds,
1723
+ sourceLanguage,
1800
1724
  targetLanguage
1801
1725
  }
1802
1726
  ), translationMap = /* @__PURE__ */ new Map();
@@ -1805,11 +1729,10 @@ const fetchTranslationPrompts = async ({
1805
1729
  const patch = client.patch(documentId);
1806
1730
  let hasChanges = !1;
1807
1731
  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({
1732
+ const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
1733
+ newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
1810
1734
  [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1811
- _type: "reference",
1812
- _weak: !1
1735
+ type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
1813
1736
  }
1814
1737
  }), hasChanges = !0);
1815
1738
  }
@@ -2294,7 +2217,7 @@ const fetchTranslationPrompts = async ({
2294
2217
  sourceLanguage
2295
2218
  };
2296
2219
  try {
2297
- const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt({
2220
+ const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt$1({
2298
2221
  sourceText,
2299
2222
  sourceLanguage,
2300
2223
  targetLanguage,
@@ -2330,7 +2253,7 @@ const fetchTranslationPrompts = async ({
2330
2253
  } catch (error) {
2331
2254
  throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2332
2255
  }
2333
- }, buildTranslationPrompt = ({
2256
+ }, buildTranslationPrompt$1 = ({
2334
2257
  sourceText,
2335
2258
  sourceLanguage,
2336
2259
  targetLanguage,
@@ -2377,111 +2300,46 @@ Brief summary of key translation decisions and reasoning
2377
2300
  </reasoning>
2378
2301
 
2379
2302
  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
2303
+ }, assembleLanguageSpecs = ({
2304
+ sourceDocument,
2305
+ languageFieldName,
2306
+ targetLanguage
2307
+ }) => {
2308
+ const sourceLanugageId = sourceDocument[languageFieldName];
2309
+ if (!sourceLanugageId)
2310
+ throw new Error("[assembleLanguages] Source language ID is missing");
2311
+ return {
2312
+ fromLanguage: {
2313
+ id: sourceLanugageId,
2314
+ title: sourceLanugageId
2410
2315
  },
2411
- {
2412
- id: "criticalRules",
2413
- content: buildCriticalRulesSection(context),
2414
- required: !0,
2415
- priority: 1
2316
+ toLanguage: {
2317
+ id: targetLanguage,
2318
+ title: targetLanguage
2416
2319
  }
2417
- ];
2418
- components.glossaryTerms?.trim() && sections.push({
2419
- id: "glossaryTerms",
2420
- content: buildGlossarySection(components.glossaryTerms),
2421
- required: !1,
2422
- priority: 3
2423
- });
2320
+ };
2321
+ }, buildTranslationPrompt = ({
2322
+ sourceLanguage,
2323
+ targetLanguage,
2324
+ components,
2325
+ isUpdate = !1
2326
+ }) => {
2327
+ const sections = [];
2328
+ 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
2329
  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 ? `
2330
+ return styleSection && sections.push(styleSection), sections.join(`
2435
2331
 
2436
2332
  ---
2437
2333
 
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
2334
+ `);
2335
+ }, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
2477
2336
 
2478
2337
  - Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
2479
2338
  - Scan each field once, then move directly into translation work.
2480
2339
  - Plan updates before editing so document structure remains intact.
2481
2340
  - 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";
2341
+ - Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
2342
+ const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
2485
2343
  return `# CRITICAL TRANSLATION RULES
2486
2344
 
2487
2345
  Follow these without exception:
@@ -2492,34 +2350,26 @@ Follow these without exception:
2492
2350
  4. Never invent metadata or field values that are absent in the source.
2493
2351
  5. Leave technical tokens (code, IDs, filenames) untouched.
2494
2352
  6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
2495
- }, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2353
+ }, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
2354
+
2355
+ This document already has a translation; only adjust fields that changed.
2356
+
2357
+ - Compare the latest source to the existing target text.
2358
+ - Keep prior terminology and style unless the source demands a change.
2359
+ - Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
2496
2360
 
2497
2361
  Apply these mappings immediately after protected phrases and before style preferences.
2498
2362
 
2499
- ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2363
+ ${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
2364
+
2365
+ These phrases must NEVER be translated. Keep them exactly as they appear:
2366
+
2367
+ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2500
2368
  const components = [];
2501
2369
  return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
2502
2370
  "Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
2503
2371
  ), 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
2372
  `) : 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
2373
  }, getSanityAgentTranslation = async ({
2524
2374
  sourceDocument,
2525
2375
  targetLanguage,
@@ -2528,19 +2378,18 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2528
2378
  client,
2529
2379
  translationPrompts
2530
2380
  }) => {
2531
- analyzeDocumentContent(sourceDocument);
2532
- const { styleGuide } = assembleTranslationPrompt(
2533
- {
2534
- // Always fresh translation in a version
2535
- targetLanguage,
2536
- sourceLanguage: sourceDocument[languageFieldName]
2537
- },
2538
- {
2381
+ const styleGuide = buildTranslationPrompt({
2382
+ sourceLanguage: sourceDocument[languageFieldName],
2383
+ targetLanguage,
2384
+ isUpdate: !1,
2385
+ // Always fresh translation in a version
2386
+ components: {
2539
2387
  globalPrompt: translationPrompts.globalPrompt,
2540
2388
  localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2541
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms
2389
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2390
+ protectedPhrases: translationPrompts.protectedPhrases
2542
2391
  }
2543
- ), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2392
+ }), { fromLanguage, toLanguage } = assembleLanguageSpecs({
2544
2393
  sourceDocument,
2545
2394
  languageFieldName,
2546
2395
  targetLanguage
@@ -2610,13 +2459,11 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2610
2459
  ...translatedDoc,
2611
2460
  _id: targetDocumentVersionId
2612
2461
  };
2613
- console.log("NEW DOC", newDoc);
2614
- const createdDoc = await client.action({
2462
+ return await client.action({
2615
2463
  actionType: "sanity.action.document.version.create",
2616
2464
  publishedId: targetDocumentPublishedId,
2617
2465
  document: newDoc
2618
- });
2619
- return console.log("CREATED DOC", createdDoc), newDoc;
2466
+ }), newDoc;
2620
2467
  }, translateDocument = async ({
2621
2468
  dispatch,
2622
2469
  client,
@@ -2720,29 +2567,20 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2720
2567
  const sourceLanguage = sourceDocument[languageFieldName];
2721
2568
  if (!sourceLanguage)
2722
2569
  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({
2570
+ maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2727
2571
  type: "SET_OVERALL_STATUS",
2728
2572
  payload: "translating"
2729
2573
  });
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({
2574
+ 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
2575
  metadata: {
2737
2576
  title: releaseTitle,
2738
2577
  description: releaseDescription,
2739
2578
  releaseType: "undecided"
2740
2579
  }
2741
2580
  });
2742
- devLog.info("\u2705 NEW CONTENT RELEASE CREATED", {
2581
+ devLog.info("Content Release created", {
2743
2582
  ...logContext,
2744
- releaseId: releaseId2,
2745
- releaseTitle
2583
+ releaseId: releaseId2
2746
2584
  });
2747
2585
  const sourceDocuments = [
2748
2586
  {
@@ -2752,19 +2590,13 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2752
2590
  title: sourceDocument.name || sourceDocument.title
2753
2591
  },
2754
2592
  ...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({
2593
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
2762
2594
  client,
2763
2595
  sourceLanguage,
2764
2596
  targetLanguages,
2765
2597
  sourceDocument
2766
2598
  });
2767
- devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2599
+ setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
2768
2600
  const translationTasks = sourceDocumentIds.flatMap(
2769
2601
  (documentId) => targetLanguages.map(async (targetLanguage) => {
2770
2602
  try {
@@ -2773,7 +2605,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2773
2605
  perspective: "drafts"
2774
2606
  }).getDocument(documentId);
2775
2607
  if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
2776
- const targetDocumentId = await translateDocument({
2608
+ const versionedTargetDocumentId = await translateDocument({
2777
2609
  dispatch,
2778
2610
  client,
2779
2611
  sourceDocument: document,
@@ -2787,7 +2619,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2787
2619
  openaiConfig,
2788
2620
  releaseId: releaseId2
2789
2621
  });
2790
- targetDocumentId && targetDocumentIds.push(targetDocumentId);
2622
+ versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
2791
2623
  }
2792
2624
  });
2793
2625
  } catch (error) {
@@ -2803,25 +2635,21 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2803
2635
  }
2804
2636
  })
2805
2637
  ), 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", {
2638
+ devLog.info("Translation tasks completed", {
2807
2639
  ...logContext,
2808
2640
  total: results.length,
2809
2641
  succeeded,
2810
- failed
2811
- }), devLog.info("All translation tasks completed", {
2812
- ...logContext,
2813
- targetDocumentIds: targetDocumentIds.length
2642
+ failed,
2643
+ targetDocuments: versionedTargetDocumentIds.length
2814
2644
  }), dispatch({
2815
2645
  type: "SET_OVERALL_STATUS",
2816
2646
  payload: "linking_references"
2817
- }), devLog.debug("Starting reference localization", {
2818
- ...logContext,
2819
- documentsToLocalize: targetDocumentIds.length
2820
2647
  }), await Promise.all(
2821
- targetDocumentIds.map(async (documentId) => {
2648
+ versionedTargetDocumentIds.map(async (documentId) => {
2822
2649
  await localizeReferences({
2823
2650
  client,
2824
2651
  documentId,
2652
+ sourceLanguage,
2825
2653
  languageFieldName
2826
2654
  });
2827
2655
  })
@@ -2831,7 +2659,7 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
2831
2659
  ...logContext,
2832
2660
  succeeded,
2833
2661
  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();
2662
+ }), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
2835
2663
  } catch (error) {
2836
2664
  throw devLog.error(
2837
2665
  "translateDocumentSet failed with unexpected error",
@@ -3377,26 +3205,9 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
3377
3205
  "completed"
3378
3206
  ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
3379
3207
  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", {
3208
+ 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
3209
  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"
3210
+ overallStatus
3400
3211
  }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
3401
3212
  }, getTranslateAction = (config) => ({ draft, published }) => {
3402
3213
  const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);