@braze/sanity-plugin-ai-translation 0.1.0

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 ADDED
@@ -0,0 +1,2225 @@
1
+ import { useClient, defineType, defineField, pathToString, definePlugin } from "sanity";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, ArrowLeftIcon, EditIcon, InfoOutlineIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
4
+ import { createContext, useContext, useEffect, useState, useReducer, useMemo } from "react";
5
+ import { Card, Flex, Stack, Text, Spinner, Button, Box, Checkbox, Select, TextInput, TextArea, Heading, Inline, Tooltip, Badge, Grid, Label, Code } from "@sanity/ui";
6
+ import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
7
+ import defaults from "lodash.defaults";
8
+ import { diffPatch } from "@sanity/diff-patch";
9
+ const TranslationContext = createContext({
10
+ document: null,
11
+ config: null,
12
+ overallStatus: "idle",
13
+ selectedTargetLanguages: [],
14
+ translationJobs: /* @__PURE__ */ new Map(),
15
+ shouldTranslateDiffs: !1,
16
+ shouldOverwriteFreshTranslations: !1,
17
+ shouldTranslateReferences: !1,
18
+ maxReferenceDepth: 1,
19
+ references: [],
20
+ translationMetadata: null,
21
+ dispatch: () => {
22
+ }
23
+ }), useTranslationContext = () => {
24
+ const context = useContext(TranslationContext);
25
+ if (!context.document || !context.config)
26
+ throw new Error(
27
+ "useTranslationContext must be used within a TranslationContextProvider with document and config props"
28
+ );
29
+ return context;
30
+ }, TRANSLATION_METADATA_TYPE = "translation.metadata", UPDATE_EXISTING_TRANSLATION_PROMPT = "***IMPORTANT:** This document has an existing, but out-of-date translation. Please review the existing translation before processing and change as little as possible. Existing translation: $extantTranslation", undraftId = (id) => id.replace(/^drafts\./, ""), draftId = (id) => `drafts.${undraftId(id)}`, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
31
+ const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
32
+ params: {
33
+ documentId: undraftId(documentId),
34
+ translationMetadataType: TRANSLATION_METADATA_TYPE
35
+ },
36
+ initialValue: null
37
+ });
38
+ return useEffect(() => {
39
+ if (loading) {
40
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
41
+ return;
42
+ }
43
+ if (error) {
44
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
45
+ return;
46
+ }
47
+ if (data) {
48
+ dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
49
+ return;
50
+ }
51
+ dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
52
+ }, [data, loading, error, dispatch]), {
53
+ data,
54
+ loading,
55
+ error
56
+ };
57
+ }, isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
58
+ info: (message, context) => {
59
+ isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
60
+ },
61
+ warn: (message, context) => {
62
+ isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
63
+ },
64
+ error: (message, error, context) => {
65
+ isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
66
+ },
67
+ debug: (message, data, context) => {
68
+ isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
69
+ },
70
+ group: (label, context) => {
71
+ isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
72
+ },
73
+ groupEnd: () => {
74
+ isLoggingEnabled() && console.groupEnd();
75
+ },
76
+ time: (label) => {
77
+ isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
78
+ },
79
+ timeEnd: (label) => {
80
+ isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
81
+ }
82
+ };
83
+ async function analyzeFeedback({
84
+ client,
85
+ sourceLanguage,
86
+ targetLanguage,
87
+ userFeedback,
88
+ shouldIncludeEdits,
89
+ sourceSnapshot,
90
+ targetSnapshot,
91
+ currentDocument,
92
+ onProgress
93
+ }) {
94
+ devLog.info("Starting feedback analysis", {
95
+ sourceLanguage,
96
+ targetLanguage,
97
+ hasUserFeedback: !!userFeedback,
98
+ shouldIncludeEdits
99
+ }), onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
100
+ const analysisPrompt = buildAnalysisPrompt({
101
+ sourceLanguage,
102
+ targetLanguage,
103
+ userFeedback,
104
+ shouldIncludeEdits,
105
+ sourceSnapshot,
106
+ targetSnapshot,
107
+ currentDocument
108
+ }), analysis = await analyzeWithAI(client, analysisPrompt);
109
+ devLog.info("AI analysis complete", { analysis });
110
+ const suggestions = convertAnalysisToSuggestions(analysis);
111
+ return devLog.info("Generated suggestions", { count: suggestions.length }), suggestions;
112
+ }
113
+ async function applySuggestions({
114
+ client,
115
+ schemaId,
116
+ sourceLanguage,
117
+ targetLanguage,
118
+ suggestions,
119
+ onProgress
120
+ }) {
121
+ devLog.info("Applying suggestions", {
122
+ sourceLanguage,
123
+ targetLanguage,
124
+ count: suggestions.filter((s) => s.approved).length
125
+ });
126
+ const approvedSuggestions = suggestions.filter((s) => s.approved);
127
+ if (approvedSuggestions.length === 0) {
128
+ devLog.info("No approved suggestions to apply");
129
+ return;
130
+ }
131
+ onProgress?.({ stage: "applying", message: "Applying updates to translation settings..." }), await applyImprovementsFromSuggestions(client, schemaId, sourceLanguage, approvedSuggestions, onProgress), devLog.info("Suggestions applied successfully");
132
+ }
133
+ function buildAnalysisPrompt({
134
+ sourceLanguage,
135
+ targetLanguage,
136
+ userFeedback,
137
+ shouldIncludeEdits,
138
+ sourceSnapshot,
139
+ targetSnapshot,
140
+ currentDocument
141
+ }) {
142
+ const parts = [
143
+ "You are analyzing feedback about AI translation quality to improve future translations.",
144
+ `Source language: ${sourceLanguage}`,
145
+ `Target language: ${targetLanguage}`,
146
+ "",
147
+ "Your task is to analyze the feedback and infer specific improvements to:",
148
+ "1. Global translation prompt (general guidance for all translations)",
149
+ "2. Locale-specific prompt (guidance specific to this target language)",
150
+ "3. Protected phrases (terms that should never be translated)",
151
+ "4. Glossary terms (specific term-to-term translation mappings)",
152
+ ""
153
+ ];
154
+ return userFeedback && parts.push("USER FEEDBACK:", userFeedback, ""), shouldIncludeEdits && sourceSnapshot && targetSnapshot && currentDocument && parts.push(
155
+ "EDIT ANALYSIS:",
156
+ "Compare the original translation with the edited version to understand what the user changed.",
157
+ "",
158
+ "Original source document:",
159
+ sourceSnapshot,
160
+ "",
161
+ "Original AI translation:",
162
+ targetSnapshot,
163
+ "",
164
+ "Current edited document:",
165
+ JSON.stringify(currentDocument, null, 2),
166
+ ""
167
+ ), parts.push(
168
+ "INSTRUCTIONS:",
169
+ "Analyze the feedback and/or edits to identify patterns and improvements.",
170
+ "",
171
+ "For edits, look for:",
172
+ "- Terminology changes that suggest glossary additions",
173
+ "- Phrases that were protected/unprotected incorrectly",
174
+ "- Style, tone, or formatting issues that suggest prompt improvements",
175
+ "- Consistent patterns across multiple changes",
176
+ "",
177
+ "IMPORTANT RULES:",
178
+ "1. DO NOT create redundant updates. Choose ONE mechanism for each issue:",
179
+ " - Protected phrases = terms that should NEVER be translated (language-agnostic)",
180
+ ' - Glossary terms = specific translation mappings (e.g., "chat" \u2192 "cat" in French)',
181
+ " - Locale prompts = guidance specific to this source\u2192target language pair",
182
+ " - Global prompts = guidance that applies to ALL translations",
183
+ "",
184
+ "2. Be locale-specific:",
185
+ ` - This feedback is specifically about ${sourceLanguage} \u2192 ${targetLanguage} translation`,
186
+ " - DO NOT assume rules apply to other language pairs unless explicitly stated",
187
+ " - Prefer locale-specific prompts over global prompts unless universally applicable",
188
+ "",
189
+ "3. Protected phrases vs. Locale prompts:",
190
+ " - Use protected phrases ONLY for brand names, product names, proper nouns, technical terms that should NEVER be translated in ANY language",
191
+ ' - Use locale prompts for language-pair-specific guidance (e.g., "Keep technical terms in English when translating to German")',
192
+ " - NEVER mention specific terms in both protected phrases AND prompts",
193
+ "",
194
+ "4. Glossary vs. Protected phrases:",
195
+ ' - Glossary = "Always translate X as Y" (provides the correct translation)',
196
+ ' - Protected = "Never translate X" (keeps it as-is)',
197
+ " - CRITICAL: If sourceValue === targetValue, use PROTECTED PHRASES, NOT glossary",
198
+ " - Only use glossary when the term actually changes between languages",
199
+ " - Use protected when the term should remain unchanged across all languages",
200
+ "",
201
+ "Output a JSON object with this structure:",
202
+ "{",
203
+ ' "globalPromptUpdates": "string | null - ONLY if this applies to ALL language pairs",',
204
+ ` "localePromptUpdates": "string | null - ONLY if this is specific to ${sourceLanguage}\u2192${targetLanguage}",`,
205
+ ' "protectedPhrasesToAdd": ["ONLY brand names, products, proper nouns that should NEVER translate"],',
206
+ ' "glossaryTermsToAdd": [{"sourceValue": "term", "targetValue": "translation"} - ONLY specific term mappings],',
207
+ ' "reasoning": "string - Explain your analysis and which mechanism you chose for each issue"',
208
+ "}",
209
+ "",
210
+ "Be conservative: only suggest changes when there is clear evidence.",
211
+ "If in doubt, prefer locale-specific updates over global updates.",
212
+ "Avoid redundancy - if you add a protected phrase, do NOT mention it in prompts."
213
+ ), parts.join(`
214
+ `);
215
+ }
216
+ function convertAnalysisToSuggestions(analysis) {
217
+ const suggestions = [];
218
+ if (analysis.globalPromptUpdates && suggestions.push({
219
+ id: crypto.randomUUID(),
220
+ type: "globalPrompt",
221
+ description: analysis.globalPromptUpdates,
222
+ approved: !0
223
+ }), analysis.localePromptUpdates && suggestions.push({
224
+ id: crypto.randomUUID(),
225
+ type: "localePrompt",
226
+ description: analysis.localePromptUpdates,
227
+ approved: !0
228
+ }), analysis.protectedPhrasesToAdd)
229
+ for (const phrase of analysis.protectedPhrasesToAdd)
230
+ suggestions.push({
231
+ id: crypto.randomUUID(),
232
+ type: "protectedPhrase",
233
+ description: phrase,
234
+ approved: !0
235
+ });
236
+ if (analysis.glossaryTermsToAdd)
237
+ for (const term of analysis.glossaryTermsToAdd)
238
+ suggestions.push({
239
+ id: crypto.randomUUID(),
240
+ type: "glossaryTerm",
241
+ description: `${term.sourceValue} \u2192 ${term.targetValue}`,
242
+ approved: !0,
243
+ sourceValue: term.sourceValue,
244
+ targetValue: term.targetValue
245
+ });
246
+ return suggestions;
247
+ }
248
+ async function analyzeWithAI(client, prompt) {
249
+ try {
250
+ const response = await client.agent.action.prompt({
251
+ instruction: prompt,
252
+ format: "json"
253
+ });
254
+ devLog.debug("AI response received", { responseType: typeof response });
255
+ const responseText = typeof response == "string" ? response : response && typeof response == "object" ? JSON.stringify(response) : (() => {
256
+ throw new Error(`Unexpected response type: ${typeof response}`);
257
+ })();
258
+ return parseAIResponse(responseText);
259
+ } catch (error) {
260
+ throw devLog.error("Error during AI analysis", { error }), new Error(`Failed to analyze feedback: ${error instanceof Error ? error.message : "Unknown error"}`);
261
+ }
262
+ }
263
+ function parseAIResponse(responseText) {
264
+ try {
265
+ let parsed;
266
+ try {
267
+ parsed = JSON.parse(responseText);
268
+ } catch {
269
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
270
+ if (!jsonMatch) throw new Error("No JSON found in AI response");
271
+ parsed = JSON.parse(jsonMatch[0]);
272
+ }
273
+ if (!parsed || typeof parsed != "object")
274
+ throw new Error("Response is not a valid object");
275
+ const analysis = parsed;
276
+ if (typeof analysis.reasoning != "string")
277
+ throw new Error("Invalid response structure: missing reasoning");
278
+ return {
279
+ globalPromptUpdates: analysis.globalPromptUpdates || void 0,
280
+ localePromptUpdates: analysis.localePromptUpdates || void 0,
281
+ protectedPhrasesToAdd: Array.isArray(analysis.protectedPhrasesToAdd) ? analysis.protectedPhrasesToAdd : [],
282
+ glossaryTermsToAdd: Array.isArray(analysis.glossaryTermsToAdd) ? analysis.glossaryTermsToAdd : [],
283
+ reasoning: analysis.reasoning
284
+ };
285
+ } catch (error) {
286
+ throw devLog.error("Error parsing AI response", { error, responseText }), new Error("Failed to parse AI response as JSON");
287
+ }
288
+ }
289
+ async function applyImprovementsFromSuggestions(client, sourceLanguage, targetLanguage, suggestions, onProgress) {
290
+ devLog.info("Applying improvements from suggestions", { count: suggestions.length });
291
+ const settingsId = "translation-settings";
292
+ await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
293
+ _id: settingsId,
294
+ _type: "translationSettings"
295
+ });
296
+ 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(
297
+ (s) => s.type === "glossaryTerm" && s.sourceValue?.trim() && s.targetValue?.trim()
298
+ );
299
+ if (devLog.info("Filtered suggestions by type", {
300
+ globalPrompts: globalPrompts.length,
301
+ localePrompts: localePrompts.length,
302
+ protectedPhrases: protectedPhrases.length,
303
+ glossaryTerms: glossaryTerms.length
304
+ }), globalPrompts.length > 0) {
305
+ onProgress?.({ stage: "applying", message: "Updating global translation prompt..." });
306
+ const combinedUpdate = globalPrompts.map((s) => s.description).join(`
307
+
308
+ `);
309
+ devLog.debug("Applying global prompt update", { combinedUpdate }), updates.push(updateGlobalPrompt(client, settingsId, combinedUpdate));
310
+ }
311
+ if (localePrompts.length > 0) {
312
+ onProgress?.({ stage: "applying", message: `Updating ${targetLanguage} translation prompt...` });
313
+ const combinedUpdate = localePrompts.map((s) => s.description).join(`
314
+
315
+ `);
316
+ devLog.debug("Applying locale prompt update", { targetLanguage, combinedUpdate }), updates.push(updateLocalePrompt(client, settingsId, targetLanguage, combinedUpdate));
317
+ }
318
+ if (protectedPhrases.length > 0) {
319
+ onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
320
+ const phrases = protectedPhrases.map((s) => s.description);
321
+ devLog.debug("Applying protected phrases", { phrases }), updates.push(addProtectedPhrases(client, settingsId, phrases));
322
+ }
323
+ if (glossaryTerms.length > 0) {
324
+ onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
325
+ const terms = glossaryTerms.map((s) => ({
326
+ // biome-ignore-start lint/style/noNonNullAssertion: safely handled
327
+ sourceValue: s.sourceValue,
328
+ targetValue: s.targetValue
329
+ // biome-ignore-end lint/style/noNonNullAssertion: safely handled
330
+ }));
331
+ devLog.debug("Applying glossary terms", { terms }), updates.push(addGlossaryTerms(client, terms, sourceLanguage, targetLanguage));
332
+ }
333
+ updates.length === 0 && devLog.warn("No valid updates to apply - all suggestions may have been empty"), await Promise.all(updates), devLog.info("All improvements applied successfully");
334
+ }
335
+ async function mergePromptWithAI(client, currentPrompt, newGuidance, context) {
336
+ const instruction = `
337
+ You are updating a ${context === "global" ? "translation" : `locale-specific translation for ${context}`} prompt.
338
+ Your task is to merge new guidance into an existing prompt.
339
+
340
+ CURRENT PROMPT:
341
+ ${currentPrompt}
342
+
343
+ NEW GUIDANCE TO ADD:
344
+ ${newGuidance}
345
+
346
+ INSTRUCTIONS:
347
+ - Read the current prompt carefully
348
+ - Integrate the new guidance into the prompt in a natural, coherent way
349
+ - If the new guidance conflicts with existing content, update or replace the conflicting parts
350
+ - If the new guidance is already covered, do not add redundant text
351
+ - Maintain the structure and tone of the prompt
352
+ - Output ONLY the updated prompt text, with no preamble or explanation
353
+
354
+ OUTPUT THE UPDATED PROMPT:`.trim(), mergedPrompt = await client.agent.action.prompt({
355
+ instruction,
356
+ format: "string"
357
+ });
358
+ return devLog.debug("AI merged prompt", {
359
+ context,
360
+ originalLength: currentPrompt.length,
361
+ newLength: mergedPrompt.length
362
+ }), mergedPrompt;
363
+ }
364
+ async function updateGlobalPrompt(client, settingsId, updates) {
365
+ devLog.info("Updating global prompt", { settingsId, updatesLength: updates.length });
366
+ const currentPrompt = await client.fetch("*[_id == $settingsId][0].globalPrompt", { settingsId });
367
+ devLog.debug("Fetched current global prompt", {
368
+ hasCurrentPrompt: !!currentPrompt,
369
+ currentLength: currentPrompt?.length
370
+ });
371
+ const newPrompt = currentPrompt ? await mergePromptWithAI(client, currentPrompt, updates, "global") : updates, result = await client.patch(settingsId).set({ globalPrompt: newPrompt.trim() }).commit();
372
+ devLog.info("Global prompt updated", { resultId: result._id, newPromptLength: newPrompt.length });
373
+ }
374
+ async function updateLocalePrompt(client, settingsId, locale, updates) {
375
+ devLog.info("Updating locale prompt", { settingsId, locale, updatesLength: updates.length });
376
+ const currentLocalePrompts = await client.fetch(
377
+ "*[_id == $settingsId][0].localePrompts",
378
+ { settingsId }
379
+ );
380
+ devLog.debug("Fetched current locale prompts", { count: currentLocalePrompts?.length });
381
+ const existingPrompt = currentLocalePrompts?.find((lp) => lp.locale === locale);
382
+ if (devLog.debug("Found existing prompt for locale", { locale, hasExisting: !!existingPrompt }), existingPrompt?.prompt) {
383
+ const newPrompt = await mergePromptWithAI(client, existingPrompt.prompt, updates, locale), updatedPrompts = currentLocalePrompts.map(
384
+ (lp) => lp.locale === locale ? { ...lp, prompt: newPrompt.trim() } : lp
385
+ ), result = await client.patch(settingsId).set({ localePrompts: updatedPrompts }).commit();
386
+ devLog.info("Locale prompt updated (merged)", { locale, resultId: result._id });
387
+ } else {
388
+ const result = await client.patch(settingsId).setIfMissing({ localePrompts: [] }).append("localePrompts", [
389
+ {
390
+ _type: "localePrompt",
391
+ _key: locale,
392
+ locale,
393
+ prompt: updates.trim()
394
+ }
395
+ ]).commit();
396
+ devLog.info("Locale prompt created", { locale, resultId: result._id });
397
+ }
398
+ }
399
+ async function addProtectedPhrases(client, settingsId, phrases) {
400
+ devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
401
+ const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
402
+ devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
403
+ const newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
404
+ if (newPhrases.length === 0) {
405
+ devLog.info("No new protected phrases to add (all already exist)");
406
+ return;
407
+ }
408
+ const result = await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit();
409
+ devLog.info("Protected phrases added", { count: newPhrases.length, phrases: newPhrases, resultId: result._id });
410
+ }
411
+ async function addGlossaryTerms(client, terms, sourceLocale, targetLocale) {
412
+ const validTerms = terms.filter(({ sourceValue, targetValue }) => sourceValue === targetValue ? (devLog.warn("Skipping glossary term where source === target (should be a protected phrase)", {
413
+ sourceValue,
414
+ targetValue
415
+ }), !1) : !0);
416
+ if (validTerms.length === 0) {
417
+ devLog.info("No valid glossary terms to add");
418
+ return;
419
+ }
420
+ for (const { sourceValue, targetValue } of validTerms) {
421
+ const existingTerm = await client.fetch(
422
+ `*[_type == "translationGlossaryTerm"
423
+ && count(translations[locale == $sourceLocale && value == $sourceValue]) > 0
424
+ ][0] {
425
+ _id,
426
+ translations
427
+ }`,
428
+ { sourceLocale, sourceValue }
429
+ );
430
+ existingTerm ? existingTerm.translations.some(
431
+ (t) => t.locale === targetLocale && t.value === targetValue
432
+ ) ? devLog.info("Glossary term already has this translation", { sourceValue, targetValue, targetLocale }) : (await client.patch(existingTerm._id).append("translations", [
433
+ {
434
+ _type: "translation",
435
+ _key: targetLocale,
436
+ locale: targetLocale,
437
+ value: targetValue
438
+ }
439
+ ]).commit(), devLog.info("Updated existing glossary term", { sourceValue, targetValue, targetLocale })) : (await client.create({
440
+ _type: "translationGlossaryTerm",
441
+ translations: [
442
+ {
443
+ _type: "translation",
444
+ _key: sourceLocale,
445
+ locale: sourceLocale,
446
+ value: sourceValue
447
+ },
448
+ {
449
+ _type: "translation",
450
+ _key: targetLocale,
451
+ locale: targetLocale,
452
+ value: targetValue
453
+ }
454
+ ]
455
+ }), devLog.info("Created new glossary term", { sourceLocale, sourceValue, targetLocale, targetValue }));
456
+ }
457
+ devLog.info("Glossary terms processed", {
458
+ totalRequested: terms.length,
459
+ validAdded: validTerms.length,
460
+ skipped: terms.length - validTerms.length
461
+ });
462
+ }
463
+ const ICON_SIZE = 64, progressIconMap = {
464
+ in_progress: /* @__PURE__ */ jsx(Spinner, {}),
465
+ completed: /* @__PURE__ */ jsx(CheckmarkCircleIcon, { fontSize: ICON_SIZE }),
466
+ error: /* @__PURE__ */ jsx(ErrorOutlineIcon, { fontSize: ICON_SIZE })
467
+ }, ProgressPanel = ({
468
+ title,
469
+ description,
470
+ tone,
471
+ status = "in_progress",
472
+ icon,
473
+ ...props
474
+ }) => /* @__PURE__ */ jsx(Card, { tone, padding: 4, ...props, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, align: "center", justify: "center", children: [
475
+ icon || progressIconMap[status],
476
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
477
+ title && /* @__PURE__ */ jsx(Text, { size: 2, weight: "bold", children: title }),
478
+ description && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: description })
479
+ ] })
480
+ ] }) }), IMPROVEMENT_TYPE_LABELS = {
481
+ globalPrompt: "Global Prompt",
482
+ localePrompt: "Locale Prompt",
483
+ protectedPhrase: "Protected Phrase",
484
+ glossaryTerm: "Glossary Term"
485
+ }, createNewSuggestion = (type = "protectedPhrase") => {
486
+ const baseSuggestion = {
487
+ id: crypto.randomUUID(),
488
+ type,
489
+ description: "",
490
+ approved: !0
491
+ };
492
+ return type === "glossaryTerm" ? {
493
+ ...baseSuggestion,
494
+ sourceValue: "",
495
+ targetValue: ""
496
+ } : baseSuggestion;
497
+ }, SuggestionReviewList = ({ suggestions, onSuggestionsChange }) => {
498
+ const [editingId, setEditingId] = useState(null), handleToggleApproval = (id) => {
499
+ const updated = suggestions.map((s) => s.id === id ? { ...s, approved: !s.approved } : s);
500
+ onSuggestionsChange(updated);
501
+ }, handleTypeChange = (id, newType) => {
502
+ const updated = suggestions.map((s) => {
503
+ if (s.id !== id)
504
+ return s;
505
+ if (newType === "glossaryTerm" && s.type !== "glossaryTerm") {
506
+ const match = s.description.match(/^(.+?)\s*→\s*(.+)$/);
507
+ return {
508
+ ...s,
509
+ type: newType,
510
+ sourceValue: match ? match[1].trim() : "",
511
+ targetValue: match ? match[2].trim() : ""
512
+ };
513
+ }
514
+ return newType !== "glossaryTerm" && s.type === "glossaryTerm" ? {
515
+ ...s,
516
+ type: newType,
517
+ description: s.sourceValue && s.targetValue ? `${s.sourceValue} \u2192 ${s.targetValue}` : s.description,
518
+ sourceValue: void 0,
519
+ targetValue: void 0
520
+ } : { ...s, type: newType };
521
+ });
522
+ onSuggestionsChange(updated);
523
+ }, handleDescriptionChange = (id, newDescription) => {
524
+ const updated = suggestions.map((s) => s.id === id ? { ...s, description: newDescription } : s);
525
+ onSuggestionsChange(updated);
526
+ }, handleGlossarySourceChange = (id, newSource) => {
527
+ const updated = suggestions.map(
528
+ (s) => s.id === id ? {
529
+ ...s,
530
+ sourceValue: newSource,
531
+ description: `${newSource} \u2192 ${s.targetValue || ""}`
532
+ } : s
533
+ );
534
+ onSuggestionsChange(updated);
535
+ }, handleGlossaryTargetChange = (id, newTarget) => {
536
+ const updated = suggestions.map(
537
+ (s) => s.id === id ? {
538
+ ...s,
539
+ targetValue: newTarget,
540
+ description: `${s.sourceValue || ""} \u2192 ${newTarget}`
541
+ } : s
542
+ );
543
+ onSuggestionsChange(updated);
544
+ }, handleRemove = (id) => {
545
+ const updated = suggestions.filter((s) => s.id !== id);
546
+ onSuggestionsChange(updated);
547
+ }, handleAddSuggestion = (type) => {
548
+ const newSuggestion = createNewSuggestion(type);
549
+ onSuggestionsChange([...suggestions, newSuggestion]), setEditingId(newSuggestion.id);
550
+ };
551
+ return suggestions.length === 0 ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
552
+ /* @__PURE__ */ jsx(
553
+ ProgressPanel,
554
+ {
555
+ status: "error",
556
+ tone: "transparent",
557
+ title: "No suggestions generated",
558
+ description: 'Try providing more specific feedback or enabling "Include Edits".',
559
+ icon: /* @__PURE__ */ jsx(EmptyIcon, { fontSize: 24 })
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
563
+ Button,
564
+ {
565
+ mode: "ghost",
566
+ tone: "primary",
567
+ icon: AddIcon,
568
+ text: "Add Suggestion",
569
+ fontSize: 1,
570
+ onClick: () => handleAddSuggestion("protectedPhrase")
571
+ }
572
+ ) })
573
+ ] }) : /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
574
+ suggestions.map((suggestion) => {
575
+ const isEditing = editingId === suggestion.id, isGlossaryTerm = suggestion.type === "glossaryTerm", isPrompt = suggestion.type === "globalPrompt" || suggestion.type === "localePrompt";
576
+ return /* @__PURE__ */ jsx(
577
+ Card,
578
+ {
579
+ padding: 4,
580
+ border: !0,
581
+ tone: suggestion.approved ? "default" : "transparent",
582
+ style: { opacity: suggestion.approved ? 1 : 0.5 },
583
+ children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
584
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Checkbox, { checked: suggestion.approved, onChange: () => handleToggleApproval(suggestion.id) }) }),
585
+ /* @__PURE__ */ jsxs(Stack, { space: 3, flex: 1, children: [
586
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", wrap: "wrap", children: [
587
+ /* @__PURE__ */ jsx(Box, { flex: 1, style: { minWidth: "200px" }, children: /* @__PURE__ */ jsx(
588
+ Select,
589
+ {
590
+ fontSize: 1,
591
+ value: suggestion.type,
592
+ onChange: (event) => handleTypeChange(suggestion.id, event.currentTarget.value),
593
+ children: Object.entries(IMPROVEMENT_TYPE_LABELS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
594
+ }
595
+ ) }),
596
+ /* @__PURE__ */ jsx(
597
+ Button,
598
+ {
599
+ mode: "bleed",
600
+ tone: "critical",
601
+ icon: TrashIcon,
602
+ fontSize: 1,
603
+ padding: 2,
604
+ onClick: () => handleRemove(suggestion.id)
605
+ }
606
+ )
607
+ ] }),
608
+ isGlossaryTerm ? /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
609
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
610
+ TextInput,
611
+ {
612
+ fontSize: 1,
613
+ placeholder: "Source term",
614
+ value: suggestion.sourceValue || "",
615
+ onChange: (event) => handleGlossarySourceChange(suggestion.id, event.currentTarget.value)
616
+ }
617
+ ) }),
618
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "\u2192" }),
619
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
620
+ TextInput,
621
+ {
622
+ fontSize: 1,
623
+ placeholder: "Target term",
624
+ value: suggestion.targetValue || "",
625
+ onChange: (event) => handleGlossaryTargetChange(suggestion.id, event.currentTarget.value)
626
+ }
627
+ ) })
628
+ ] }) }) : isPrompt ? /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(
629
+ TextArea,
630
+ {
631
+ fontSize: 1,
632
+ rows: isEditing ? 6 : 3,
633
+ value: suggestion.description,
634
+ onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
635
+ onFocus: () => setEditingId(suggestion.id),
636
+ onBlur: () => setEditingId(null),
637
+ placeholder: "Enter prompt guidance..."
638
+ }
639
+ ) }) : /* @__PURE__ */ jsx(
640
+ TextInput,
641
+ {
642
+ fontSize: 1,
643
+ value: suggestion.description,
644
+ onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
645
+ placeholder: "Enter protected phrase..."
646
+ }
647
+ )
648
+ ] })
649
+ ] }) })
650
+ },
651
+ suggestion.id
652
+ );
653
+ }),
654
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
655
+ Button,
656
+ {
657
+ mode: "ghost",
658
+ tone: "neutral",
659
+ icon: AddIcon,
660
+ text: "Add Suggestion",
661
+ fontSize: 1,
662
+ onClick: () => handleAddSuggestion("protectedPhrase")
663
+ }
664
+ ) })
665
+ ] });
666
+ }, GiveFeedbackDialog = ({ setIsDialogOpen }) => {
667
+ const client = useClient({ apiVersion: "vX" }), { document, config, translationMetadata } = useTranslationContext(), { loading } = useTranslationMetadata({
668
+ documentId: document._id
669
+ }), [feedback, setFeedback] = useState(""), [shouldIncludeEdits, setShouldIncludeEdits] = useState(!1), [processingState, setProcessingState] = useState("idle"), [suggestions, setSuggestions] = useState([]), [changeSummary, setChangeSummary] = useState(""), [error, setError] = useState(""), handleShouldIncludeEditsChange = () => {
670
+ setShouldIncludeEdits(!shouldIncludeEdits);
671
+ }, handleCancel = () => {
672
+ setIsDialogOpen(!1);
673
+ }, handleAnalyze = async () => {
674
+ if (!feedback && !shouldIncludeEdits) {
675
+ setError('Please provide feedback or enable "Include Edits"');
676
+ return;
677
+ }
678
+ if (!config) {
679
+ setError("Missing plugin configuration");
680
+ return;
681
+ }
682
+ setError(""), setProcessingState("analyzing"), setChangeSummary("");
683
+ try {
684
+ const translationHistory2 = translationMetadata?.translationHistory?.find(
685
+ (history) => history.language === document[config.languageFieldName]
686
+ );
687
+ if (!translationHistory2)
688
+ throw new Error(
689
+ "No translation history found for this document. Please ensure this document was translated using the AI Translation plugin."
690
+ );
691
+ const { sourceSnapshot, targetSnapshot, sourceLanguage } = translationHistory2;
692
+ if (!sourceSnapshot || !targetSnapshot)
693
+ throw new Error("Translation snapshots are missing from history.");
694
+ const generatedSuggestions = await analyzeFeedback({
695
+ client,
696
+ schemaId: config.schemaId,
697
+ sourceLanguage: sourceLanguage || "en-us",
698
+ targetLanguage: document[config.languageFieldName],
699
+ userFeedback: feedback || void 0,
700
+ shouldIncludeEdits,
701
+ sourceSnapshot,
702
+ targetSnapshot,
703
+ currentDocument: shouldIncludeEdits ? document : void 0,
704
+ onProgress: ({ stage, message }) => {
705
+ setProcessingState(stage), setChangeSummary(message);
706
+ }
707
+ });
708
+ setSuggestions(generatedSuggestions), setProcessingState("reviewing");
709
+ } catch (err) {
710
+ console.error("Error analyzing feedback:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
711
+ }
712
+ }, handleApplySuggestions = async () => {
713
+ if (!config) {
714
+ setError("Missing plugin configuration");
715
+ return;
716
+ }
717
+ if (suggestions.filter((s) => s.approved).length === 0) {
718
+ setError("Please approve at least one suggestion to apply");
719
+ return;
720
+ }
721
+ setError(""), setProcessingState("applying");
722
+ try {
723
+ const translationHistory2 = translationMetadata?.translationHistory?.find(
724
+ (history) => history.language === document[config.languageFieldName]
725
+ );
726
+ if (!translationHistory2)
727
+ throw new Error("Translation history not found");
728
+ await applySuggestions({
729
+ client,
730
+ schemaId: config.schemaId,
731
+ sourceLanguage: translationHistory2.sourceLanguage || "en-us",
732
+ targetLanguage: document[config.languageFieldName],
733
+ suggestions,
734
+ onProgress: ({ stage, message }) => {
735
+ setProcessingState(stage), setChangeSummary(message);
736
+ }
737
+ }), setProcessingState("completed");
738
+ } catch (err) {
739
+ console.error("Error applying suggestions:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
740
+ }
741
+ };
742
+ if (loading)
743
+ return /* @__PURE__ */ jsx(ProgressPanel, { title: "Loading translation metadata" });
744
+ if (processingState === "analyzing")
745
+ return /* @__PURE__ */ jsx(ProgressPanel, { title: "Analyzing feedback with AI...", description: changeSummary });
746
+ if (processingState === "reviewing")
747
+ return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
748
+ /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
749
+ /* @__PURE__ */ jsx(Heading, { children: "Review Suggestions" }),
750
+ /* @__PURE__ */ jsx(Text, { muted: !0, children: "The AI has analyzed your feedback and generated the following suggestions. Review and edit them before applying." })
751
+ ] }),
752
+ /* @__PURE__ */ jsx(SuggestionReviewList, { suggestions, onSuggestionsChange: setSuggestions }),
753
+ error && /* @__PURE__ */ jsx(Card, { padding: 3, tone: "critical", border: !0, children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
754
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, paddingTop: 2, children: [
755
+ /* @__PURE__ */ jsx(Flex, { gap: 2, children: /* @__PURE__ */ jsx(
756
+ Button,
757
+ {
758
+ type: "button",
759
+ mode: "bleed",
760
+ tone: "neutral",
761
+ onClick: () => setProcessingState("idle"),
762
+ icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
763
+ text: "Back"
764
+ }
765
+ ) }),
766
+ /* @__PURE__ */ jsxs(
767
+ Button,
768
+ {
769
+ type: "button",
770
+ mode: "ghost",
771
+ tone: "positive",
772
+ size: 1,
773
+ onClick: handleApplySuggestions,
774
+ disabled: suggestions.filter((s) => s.approved).length === 0,
775
+ children: [
776
+ "Apply ",
777
+ suggestions.filter((s) => s.approved).length,
778
+ " Suggestion",
779
+ suggestions.filter((s) => s.approved).length !== 1 ? "s" : ""
780
+ ]
781
+ }
782
+ )
783
+ ] })
784
+ ] });
785
+ if (processingState === "applying")
786
+ return /* @__PURE__ */ jsx(ProgressPanel, { title: "Updating translation settings...", description: changeSummary, icon: /* @__PURE__ */ jsx(EditIcon, {}) });
787
+ if (processingState === "completed") {
788
+ const appliedCount = suggestions.filter((s) => s.approved).length, description = `${appliedCount} suggestion${appliedCount !== 1 ? "s" : ""} applied. Translation prompts and glossary havebeen updated based on your feedback.`;
789
+ return /* @__PURE__ */ jsxs(Box, { padding: 4, children: [
790
+ /* @__PURE__ */ jsx(ProgressPanel, { title: "Translation settings updated", description, status: "completed" }),
791
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-end", paddingTop: 3, children: /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", tone: "primary", size: 1, onClick: handleCancel, children: "Close" }) })
792
+ ] });
793
+ }
794
+ return processingState === "error" ? /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
795
+ /* @__PURE__ */ jsx(ProgressPanel, { status: "error", title: "Error processing feedback", description: error, tone: "critical" }),
796
+ /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
797
+ /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", onClick: handleCancel, children: "Close" }),
798
+ /* @__PURE__ */ jsx(Button, { type: "reset", mode: "ghost", tone: "primary", onClick: () => setProcessingState("idle"), children: "Reset" })
799
+ ] })
800
+ ] }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
801
+ /* @__PURE__ */ jsx(Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
802
+ /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
803
+ /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
804
+ /* @__PURE__ */ jsx(Text, { size: 2, muted: !0, children: "Enter your feedback for the translation AI agent." }),
805
+ /* @__PURE__ */ jsx(
806
+ Tooltip,
807
+ {
808
+ portal: !0,
809
+ content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "The AI agent will use your feedback to improve translation prompts, update protected phrases, and modify the glossary. Be specific about what should be changed." }) }),
810
+ children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(InfoOutlineIcon, {}) })
811
+ }
812
+ )
813
+ ] }),
814
+ /* @__PURE__ */ jsx(
815
+ TextArea,
816
+ {
817
+ rows: 8,
818
+ fontSize: 2,
819
+ onChange: (event) => setFeedback(event.currentTarget.value),
820
+ padding: 3,
821
+ placeholder: 'Example: "Braze" is a branded term and should not be translated.',
822
+ value: feedback
823
+ }
824
+ )
825
+ ] }) }),
826
+ /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsx(
827
+ Button,
828
+ {
829
+ type: "button",
830
+ mode: shouldIncludeEdits ? "ghost" : "bleed",
831
+ size: 2,
832
+ onClick: handleShouldIncludeEditsChange,
833
+ tone: shouldIncludeEdits ? "primary" : "default",
834
+ children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
835
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Checkbox, { checked: shouldIncludeEdits, readOnly: !0, style: { pointerEvents: "none" } }) }),
836
+ /* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
837
+ /* @__PURE__ */ jsx(Text, { size: 2, weight: "medium", children: "Include Edits in Feedback" }),
838
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, style: { textAlign: "left" }, children: "The AI will analyze edits you made to the translation." })
839
+ ] })
840
+ ] })
841
+ }
842
+ ) }) })
843
+ ] }) }),
844
+ /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, paddingTop: 2, children: [
845
+ /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", size: 1, onClick: handleCancel, children: "Cancel" }),
846
+ /* @__PURE__ */ jsx(
847
+ Button,
848
+ {
849
+ type: "submit",
850
+ mode: "ghost",
851
+ tone: "positive",
852
+ size: 1,
853
+ onClick: handleAnalyze,
854
+ disabled: !feedback.trim() && !shouldIncludeEdits,
855
+ children: "Analyze Feedback"
856
+ }
857
+ )
858
+ ] })
859
+ ] });
860
+ }, translationReducer = (context, action) => {
861
+ const logContext = {
862
+ documentId: context.document?._id,
863
+ actionType: action.type,
864
+ component: "translationReducer"
865
+ };
866
+ switch (devLog.debug("Translation action dispatched", {
867
+ ...logContext,
868
+ payload: action.payload
869
+ }), action.type) {
870
+ case "SET_OVERALL_STATUS":
871
+ return { ...context, overallStatus: action.payload };
872
+ case "SET_TRANSLATION_METADATA":
873
+ return { ...context, translationMetadata: action.payload };
874
+ case "SET_SELECTED_TARGET_LANGUAGES":
875
+ return {
876
+ ...context,
877
+ selectedTargetLanguages: action.payload
878
+ };
879
+ case "SET_SHOULD_TRANSLATE_DIFFS":
880
+ return {
881
+ ...context,
882
+ shouldTranslateDiffs: action.payload
883
+ };
884
+ case "SET_SHOULD_TRANSLATE_REFERENCES":
885
+ return {
886
+ ...context,
887
+ shouldTranslateReferences: action.payload
888
+ };
889
+ case "SET_MAX_REFERENCE_DEPTH":
890
+ return {
891
+ ...context,
892
+ maxReferenceDepth: action.payload
893
+ };
894
+ case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
895
+ return {
896
+ ...context,
897
+ shouldOverwriteFreshTranslations: action.payload
898
+ };
899
+ case "SET_REFERENCES":
900
+ return {
901
+ ...context,
902
+ references: action.payload
903
+ };
904
+ case "SET_TRANSLATION_JOB_STATUS": {
905
+ const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs);
906
+ return newTranslationJobs.set(jobKey, defaults(action.payload, newTranslationJobs.get(jobKey))), {
907
+ ...context,
908
+ translationJobs: newTranslationJobs
909
+ };
910
+ }
911
+ default:
912
+ return context;
913
+ }
914
+ }, TranslationContextProvider = ({ document, config, children }) => {
915
+ devLog.debug("TranslationContextProvider initialized", {
916
+ documentId: document._id,
917
+ documentType: document._type,
918
+ supportedLanguages: config.supportedLanguages?.map((l) => l.id),
919
+ component: "TranslationContextProvider"
920
+ });
921
+ const initialContext = {
922
+ document,
923
+ config,
924
+ overallStatus: "idle",
925
+ translationMetadata: null,
926
+ selectedTargetLanguages: [],
927
+ shouldTranslateDiffs: !1,
928
+ shouldOverwriteFreshTranslations: !1,
929
+ shouldTranslateReferences: !1,
930
+ maxReferenceDepth: 1,
931
+ references: [],
932
+ translationJobs: /* @__PURE__ */ new Map()
933
+ }, [context, dispatch] = useReducer(translationReducer, initialContext);
934
+ return devLog.debug("Translation context state initialized", {
935
+ documentId: document._id,
936
+ overallStatus: context.overallStatus,
937
+ selectedLanguages: context.selectedTargetLanguages,
938
+ shouldTranslateDiffs: context.shouldTranslateDiffs,
939
+ component: "TranslationContextProvider"
940
+ }), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
941
+ }, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
942
+ const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
943
+ return devLog.debug("ImproveTranslationPromptsAction initialized", {
944
+ documentId: document?._id,
945
+ documentType: document?._type
946
+ }), !document || !config.translatableSchemaTypes?.includes(document._type) ? (devLog.warn("Document not in translatable schema types", {
947
+ documentType: document?._type,
948
+ translatableTypes: config.translatableSchemaTypes
949
+ }), null) : config ? {
950
+ label: "Give Translation Feedback",
951
+ icon: TranslateIcon,
952
+ onHandle: () => {
953
+ devLog.info("Give Translation Feedback dialog opened", {
954
+ documentId: document._id,
955
+ documentType: document._type,
956
+ component: "GiveTranslationFeedbackAction"
957
+ }), setIsDialogOpen(!0);
958
+ },
959
+ dialog: isDialogOpen && {
960
+ type: "dialog",
961
+ onClose: () => setIsDialogOpen(!1),
962
+ header: "Give Translation Feedback",
963
+ content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
964
+ }
965
+ } : (devLog.error("Missing config in TranslateAction"), null);
966
+ }, translationGlossaryTerm = defineType({
967
+ name: "translationGlossaryTerm",
968
+ title: "Translation Glossary Term",
969
+ icon: TranslateIcon,
970
+ type: "document",
971
+ fields: [
972
+ defineField({
973
+ name: "translations",
974
+ title: "Translations",
975
+ type: "array",
976
+ of: [
977
+ {
978
+ name: "translation",
979
+ title: "Translation",
980
+ type: "object",
981
+ fields: [
982
+ defineField({
983
+ name: "locale",
984
+ type: "string"
985
+ }),
986
+ defineField({
987
+ name: "value",
988
+ type: "string"
989
+ })
990
+ ]
991
+ }
992
+ ]
993
+ })
994
+ ],
995
+ preview: {
996
+ select: {
997
+ translations: "translations"
998
+ },
999
+ prepare({ translations }) {
1000
+ const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
1001
+ return {
1002
+ title: enTranslation?.value || firstTranslation?.value || "No translation available"
1003
+ };
1004
+ }
1005
+ }
1006
+ }), translationHistory = defineField({
1007
+ name: "translationHistory",
1008
+ type: "array",
1009
+ of: [
1010
+ {
1011
+ name: "targetLanguageHistory",
1012
+ type: "object",
1013
+ fields: [
1014
+ defineField({
1015
+ name: "language",
1016
+ title: "Language",
1017
+ description: "The language of the target document",
1018
+ type: "string"
1019
+ }),
1020
+ defineField({
1021
+ name: "sourceId",
1022
+ title: "Source ID",
1023
+ description: "The ID of the source document",
1024
+ type: "string"
1025
+ }),
1026
+ defineField({
1027
+ name: "sourceRev",
1028
+ title: "Source Revision",
1029
+ description: "The revision ID of the source document at the time of translation",
1030
+ type: "string"
1031
+ }),
1032
+ defineField({
1033
+ name: "sourceLanguage",
1034
+ title: "Source Language",
1035
+ description: "The language of the source document",
1036
+ type: "string"
1037
+ }),
1038
+ defineField({
1039
+ name: "sourceSnapshot",
1040
+ title: "Source Snapshot",
1041
+ description: "The snapshot of the source document at the time of translation",
1042
+ type: "text"
1043
+ }),
1044
+ defineField({
1045
+ name: "targetId",
1046
+ title: "Target ID",
1047
+ description: "The ID of the target document",
1048
+ type: "string"
1049
+ })
1050
+ ]
1051
+ }
1052
+ ]
1053
+ }), localePrompt = defineField({
1054
+ name: "localePrompt",
1055
+ title: "Locale Prompt",
1056
+ type: "object",
1057
+ fields: [
1058
+ defineField({
1059
+ name: "locale",
1060
+ type: "string"
1061
+ }),
1062
+ defineField({
1063
+ name: "prompt",
1064
+ type: "text",
1065
+ rows: 5
1066
+ })
1067
+ ]
1068
+ }), translationSettings = defineType({
1069
+ name: "translationSettings",
1070
+ title: "Translation Settings",
1071
+ icon: TranslateIcon,
1072
+ type: "document",
1073
+ fields: [
1074
+ defineField({
1075
+ name: "globalPrompt",
1076
+ title: "Global Translation Prompt",
1077
+ description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
1078
+ type: "text",
1079
+ rows: 5
1080
+ }),
1081
+ defineField({
1082
+ name: "localePrompts",
1083
+ title: "Locale Prompts",
1084
+ description: "Prompts that will be given to the Sanity AI Agent for each locale.",
1085
+ type: "array",
1086
+ of: [localePrompt]
1087
+ }),
1088
+ defineField({
1089
+ name: "protectedPhrases",
1090
+ title: "Protected Phrases",
1091
+ description: "Phrases that will never be translated.",
1092
+ type: "array",
1093
+ of: [{ type: "string" }]
1094
+ })
1095
+ ],
1096
+ preview: {
1097
+ prepare: () => ({
1098
+ title: "Translation Settings"
1099
+ })
1100
+ }
1101
+ }), fetchTranslationPrompts = async ({
1102
+ client,
1103
+ sourceLanguage,
1104
+ targetLanguages
1105
+ }) => {
1106
+ const translationSettings2 = await client.fetch(
1107
+ `*[_id == "translation-settings"][0] {
1108
+ globalPrompt,
1109
+ localePrompts[locale in $targetLanguages],
1110
+ protectedPhrases
1111
+ }`,
1112
+ { targetLanguages }
1113
+ ), translationGlossaryTerms = await client.fetch(
1114
+ `*[
1115
+ _type == "translationGlossaryTerm"
1116
+ && count(translations[locale in $targetLanguages]) > 0
1117
+ && count(translations[locale == $sourceLanguage]) > 0
1118
+ ].translations`,
1119
+ { targetLanguages, sourceLanguage }
1120
+ ) || [], glossaryTermsByLocale = {};
1121
+ for (const targetLanguage of targetLanguages) {
1122
+ const relevantTranslations = translationGlossaryTerms.filter(
1123
+ (termTranslations) => termTranslations.some((translation) => translation.locale === targetLanguage)
1124
+ ).map((termTranslations) => {
1125
+ const from = termTranslations.find((translation) => translation.locale === sourceLanguage)?.value, to = termTranslations.find((translation) => translation.locale === targetLanguage)?.value;
1126
+ return !from || !to ? null : { from, to };
1127
+ }).filter((item) => item !== null);
1128
+ glossaryTermsByLocale[targetLanguage] = relevantTranslations;
1129
+ }
1130
+ return {
1131
+ globalPrompt: translationSettings2?.globalPrompt,
1132
+ protectedPhrases: translationSettings2?.protectedPhrases,
1133
+ localeInstructions: Object.fromEntries(
1134
+ targetLanguages.map((locale) => [
1135
+ locale,
1136
+ {
1137
+ prompt: translationSettings2?.localePrompts?.find((lp) => lp.locale === locale)?.prompt,
1138
+ glossaryTerms: formatGlossaryTermInstruction(glossaryTermsByLocale[locale])
1139
+ }
1140
+ ])
1141
+ )
1142
+ };
1143
+ }, formatGlossaryTermInstruction = (terms) => {
1144
+ const instructions = ["Translate the following terms exactly as described below:"];
1145
+ return terms.forEach(({ from, to }) => {
1146
+ instructions.push(`"${from}" -> "${to}"`);
1147
+ }), instructions.join(`
1148
+ `);
1149
+ }, findKeyValues = (obj, targetKey) => {
1150
+ const results = [], recursiveSearch = (current, currentPath) => {
1151
+ if (current != null) {
1152
+ if (Array.isArray(current)) {
1153
+ current.forEach((item, index) => {
1154
+ recursiveSearch(item, [...currentPath, index]);
1155
+ });
1156
+ return;
1157
+ }
1158
+ if (typeof current == "object")
1159
+ for (const [key, value] of Object.entries(current)) {
1160
+ const newPath = [...currentPath, key];
1161
+ key === targetKey && results.push({
1162
+ value,
1163
+ path: newPath
1164
+ }), recursiveSearch(value, newPath);
1165
+ }
1166
+ }
1167
+ };
1168
+ return recursiveSearch(obj, []), results;
1169
+ }, localizeReferences = async ({ client, documentId, languageFieldName }) => {
1170
+ try {
1171
+ const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
1172
+ if (!doc) {
1173
+ console.error(documentId, "not found");
1174
+ return;
1175
+ }
1176
+ const targetLanguage = doc[languageFieldName];
1177
+ if (!targetLanguage) {
1178
+ console.error(documentId, "no value found for language field:", languageFieldName);
1179
+ return;
1180
+ }
1181
+ const refs = findKeyValues(doc, "_ref");
1182
+ if (!refs?.length)
1183
+ return;
1184
+ await updateReferences(documentId, client, refs, targetLanguage);
1185
+ } catch (error) {
1186
+ console.error("Error localizing references:", error);
1187
+ }
1188
+ }, updateReferences = async (documentId, client, refs, targetLanguage) => {
1189
+ const patch = client.patch(documentId);
1190
+ if (!refs?.length)
1191
+ return [];
1192
+ for (const ref of refs) {
1193
+ const newId = await client.fetch(
1194
+ "(*[_type == $translationMetadataSchemaType && references($sourceId)].translations[_key == $targetLanguage].value._ref)[0]",
1195
+ {
1196
+ translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
1197
+ sourceId: undraftId(ref.value),
1198
+ targetLanguage
1199
+ }
1200
+ );
1201
+ newId && (patch.set({ [pathToString(ref.path)]: undraftId(newId) }), patch.set({ [`${pathToString(ref.path.slice(0, -1))}._weak`]: !0 }), patch.set({
1202
+ [`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
1203
+ _type: "reference",
1204
+ _weak: !1
1205
+ }
1206
+ }), await patch.commit());
1207
+ }
1208
+ };
1209
+ class Semaphore {
1210
+ permits;
1211
+ queue = [];
1212
+ constructor(permits) {
1213
+ this.permits = permits;
1214
+ }
1215
+ /**
1216
+ * Acquire a permit from the semaphore
1217
+ * If no permits are available, the caller will wait until one becomes available
1218
+ */
1219
+ async acquire() {
1220
+ return this.permits > 0 ? (this.permits--, Promise.resolve()) : new Promise((resolve) => {
1221
+ this.queue.push(resolve);
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Release a permit back to the semaphore
1226
+ * If there are waiting callers, one will be woken up
1227
+ */
1228
+ release() {
1229
+ const next = this.queue.shift();
1230
+ next ? next() : this.permits++;
1231
+ }
1232
+ /**
1233
+ * Execute a function with semaphore protection
1234
+ * Automatically acquires and releases the permit
1235
+ */
1236
+ async withPermit(fn) {
1237
+ await this.acquire();
1238
+ try {
1239
+ return await fn();
1240
+ } finally {
1241
+ this.release();
1242
+ }
1243
+ }
1244
+ /**
1245
+ * Get the number of available permits
1246
+ */
1247
+ availablePermits() {
1248
+ return this.permits;
1249
+ }
1250
+ /**
1251
+ * Get the number of waiting callers
1252
+ */
1253
+ queueLength() {
1254
+ return this.queue.length;
1255
+ }
1256
+ /**
1257
+ * Update the number of permits
1258
+ * If increasing permits, waiting callers will be notified
1259
+ * If decreasing permits below current available, available permits will be reduced
1260
+ */
1261
+ updatePermits(newPermits) {
1262
+ const currentTotal = this.permits + this.queue.length, difference = newPermits - currentTotal;
1263
+ if (difference > 0) {
1264
+ for (let i = 0; i < difference && this.queue.length > 0; i++) {
1265
+ const next = this.queue.shift();
1266
+ next && next();
1267
+ }
1268
+ const remainingIncrease = difference - (currentTotal - this.permits - this.queue.length);
1269
+ remainingIncrease > 0 && (this.permits += remainingIncrease);
1270
+ } else difference < 0 && (this.permits = Math.max(0, this.permits + difference));
1271
+ }
1272
+ }
1273
+ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
1274
+ for (const sourceDocument of sourceDocuments)
1275
+ for (const lang of targetLanguages) {
1276
+ const payload = {
1277
+ _id: sourceDocument._id,
1278
+ _type: sourceDocument._type,
1279
+ language: lang,
1280
+ status: "queued",
1281
+ title: sourceDocument.title
1282
+ };
1283
+ dispatch({
1284
+ type: "SET_TRANSLATION_JOB_STATUS",
1285
+ payload
1286
+ });
1287
+ }
1288
+ }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1289
+ (translation) => translation._key === targetLanguage
1290
+ )?.value._ref || null, determineTranslationTarget = ({
1291
+ translationMetadata,
1292
+ targetLanguage
1293
+ }) => {
1294
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
1295
+ return existingTranslationId ? {
1296
+ // Update existing translation
1297
+ operation: "createOrReplace",
1298
+ _id: existingTranslationId
1299
+ } : {
1300
+ // Create new translation
1301
+ operation: "create"
1302
+ };
1303
+ }, getTranslationMetadata = async ({
1304
+ client,
1305
+ sourceDocument,
1306
+ sourceLanguage
1307
+ }) => {
1308
+ try {
1309
+ return await client.fetch(
1310
+ "*[_type == $tmType && references($sourceDocumentId)][0]",
1311
+ {
1312
+ tmType: TRANSLATION_METADATA_TYPE,
1313
+ sourceDocumentId: undraftId(sourceDocument._id)
1314
+ }
1315
+ ) || await client.create({
1316
+ _type: TRANSLATION_METADATA_TYPE,
1317
+ translations: [
1318
+ {
1319
+ _key: sourceLanguage,
1320
+ _type: "internationalizedArrayReferenceValue",
1321
+ value: {
1322
+ _type: "reference",
1323
+ _ref: undraftId(sourceDocument._id),
1324
+ _weak: !0,
1325
+ _strengthenOnPublish: {
1326
+ _type: sourceDocument._type,
1327
+ _weak: !1
1328
+ }
1329
+ }
1330
+ }
1331
+ ],
1332
+ translationHistory: []
1333
+ });
1334
+ } catch (error) {
1335
+ throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
1336
+ }
1337
+ }, setJobStatus = (dispatch, _id, language, status, payload) => {
1338
+ dispatch({
1339
+ type: "SET_TRANSLATION_JOB_STATUS",
1340
+ payload: defaults(payload, {
1341
+ _id,
1342
+ language,
1343
+ status
1344
+ })
1345
+ });
1346
+ }, translateDocumentInPlace = async ({
1347
+ client,
1348
+ schemaId,
1349
+ dispatch,
1350
+ sourceDocument,
1351
+ languageFieldName,
1352
+ targetLanguage,
1353
+ targetId,
1354
+ patch
1355
+ }) => {
1356
+ try {
1357
+ return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), await client.agent.action.translate({
1358
+ schemaId,
1359
+ documentId: targetId,
1360
+ toLanguage: {
1361
+ title: targetLanguage,
1362
+ id: targetLanguage
1363
+ },
1364
+ fromLanguage: {
1365
+ title: sourceDocument[languageFieldName],
1366
+ id: sourceDocument[languageFieldName]
1367
+ },
1368
+ languageFieldPath: [languageFieldName],
1369
+ styleGuide: `
1370
+ **IMPORTANT:** Much of this document is already translated into the target language. The original version of this document in the source language
1371
+ was updated. The following patch instruction shows precisely how the source document was edited: $patchInstruction. Your job is to update this
1372
+ translation to including matching changes.
1373
+ `,
1374
+ styleGuideParams: {
1375
+ patchInstruction: JSON.stringify(patch, null, 2)
1376
+ }
1377
+ });
1378
+ } catch (error) {
1379
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), console.error("Error translating document:", error);
1380
+ }
1381
+ }, assembleLanguageSpecs = ({
1382
+ sourceDocument,
1383
+ languageFieldName,
1384
+ targetLanguage
1385
+ }) => {
1386
+ const sourceLanugageId = sourceDocument[languageFieldName];
1387
+ if (!sourceLanugageId)
1388
+ throw new Error("[assembleLanguages] Source language ID is missing");
1389
+ return {
1390
+ fromLanguage: {
1391
+ id: sourceLanugageId,
1392
+ title: sourceLanugageId
1393
+ },
1394
+ toLanguage: {
1395
+ id: targetLanguage,
1396
+ title: targetLanguage
1397
+ }
1398
+ };
1399
+ }, translateEntireDocument = async ({
1400
+ dispatch,
1401
+ client,
1402
+ schemaId,
1403
+ sourceDocument,
1404
+ targetLanguage,
1405
+ languageFieldName,
1406
+ translationTarget,
1407
+ translationPrompts
1408
+ }) => {
1409
+ const logContext = {
1410
+ documentId: sourceDocument._id,
1411
+ targetLanguage,
1412
+ schemaId,
1413
+ operation: translationTarget.operation,
1414
+ targetId: translationTarget._id
1415
+ };
1416
+ devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
1417
+ try {
1418
+ const { toLanguage, fromLanguage } = assembleLanguageSpecs({
1419
+ sourceDocument,
1420
+ languageFieldName,
1421
+ targetLanguage
1422
+ });
1423
+ devLog.debug("Language specs assembled", {
1424
+ ...logContext,
1425
+ toLanguage,
1426
+ fromLanguage
1427
+ });
1428
+ let extantTranslation = "";
1429
+ translationTarget._id && (devLog.debug("Checking for existing translation", {
1430
+ ...logContext,
1431
+ targetId: translationTarget._id
1432
+ }), extantTranslation = await client.withConfig({
1433
+ perspective: "raw"
1434
+ }).fetch('*[_id in $ids] | order((_id match "drafts*") desc)[0]._id', {
1435
+ ids: [undraftId(translationTarget._id), draftId(translationTarget._id)]
1436
+ }), devLog.info("Extant translation found - will preserve existing content where possible", {
1437
+ ...logContext,
1438
+ hasExtantTranslation: !!extantTranslation
1439
+ })), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
1440
+ const styleGuide = [
1441
+ translationPrompts.globalPrompt,
1442
+ translationPrompts.localeInstructions[targetLanguage]?.prompt,
1443
+ translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
1444
+ extantTranslation ? UPDATE_EXISTING_TRANSLATION_PROMPT : null
1445
+ ].filter(Boolean).join(`
1446
+
1447
+ `);
1448
+ devLog.info("Style guide generated", {
1449
+ ...logContext,
1450
+ styleGuide
1451
+ });
1452
+ const newDocument = await client.agent.action.translate({
1453
+ schemaId,
1454
+ toLanguage,
1455
+ fromLanguage,
1456
+ documentId: sourceDocument._id,
1457
+ targetDocument: translationTarget,
1458
+ languageFieldPath: [languageFieldName],
1459
+ styleGuide,
1460
+ styleGuideParams: {
1461
+ extantTranslation
1462
+ },
1463
+ protectedPhrases: translationPrompts.protectedPhrases || []
1464
+ });
1465
+ return devLog.info("Document translation completed", {
1466
+ ...logContext,
1467
+ newDocumentId: newDocument._id,
1468
+ newDocumentRev: newDocument._rev
1469
+ }), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), newDocument;
1470
+ } catch (error) {
1471
+ throw devLog.error(
1472
+ "translateEntireDocument failed",
1473
+ error instanceof Error ? error : new Error(String(error)),
1474
+ logContext
1475
+ ), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), error;
1476
+ }
1477
+ }, updateTranslationMetadata = async ({
1478
+ client,
1479
+ targetLanguage,
1480
+ sourceDocument,
1481
+ sourceLanguage,
1482
+ newDocument,
1483
+ translationMetadata,
1484
+ isNewTranslation
1485
+ }) => {
1486
+ try {
1487
+ const refToNewTranslation = {
1488
+ _key: targetLanguage,
1489
+ _type: "internationalizedArrayReferenceValue",
1490
+ value: {
1491
+ _type: "reference",
1492
+ _ref: undraftId(newDocument._id),
1493
+ _weak: !0,
1494
+ _strengthenOnPublish: {
1495
+ _type: "reference",
1496
+ _weak: !1
1497
+ }
1498
+ }
1499
+ }, newTranslationHistory = {
1500
+ _key: targetLanguage,
1501
+ language: targetLanguage,
1502
+ sourceId: undraftId(sourceDocument._id),
1503
+ sourceRev: sourceDocument._rev,
1504
+ sourceLanguage,
1505
+ sourceSnapshot: JSON.stringify(sourceDocument),
1506
+ targetId: newDocument._id,
1507
+ targetRev: newDocument._rev,
1508
+ targetSnapshot: JSON.stringify(newDocument)
1509
+ }, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
1510
+ isNewTranslation ? await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit() : await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
1511
+ const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
1512
+ translationMetadata.translationHistory?.some(
1513
+ (history) => history.language === targetLanguage
1514
+ ) ? await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit() : await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
1515
+ } catch (error) {
1516
+ throw console.error("Error updating translation metadata:", error), error;
1517
+ }
1518
+ }, translateDocument = async ({
1519
+ dispatch,
1520
+ client,
1521
+ sourceDocument,
1522
+ targetLanguage,
1523
+ languageFieldName,
1524
+ shouldOverwriteFreshTranslations,
1525
+ schemaId,
1526
+ shouldTranslateDiffs,
1527
+ translationPrompts
1528
+ }) => {
1529
+ const logContext = {
1530
+ documentId: sourceDocument._id,
1531
+ targetLanguage,
1532
+ schemaId,
1533
+ shouldTranslateDiffs,
1534
+ shouldOverwriteFreshTranslations
1535
+ };
1536
+ devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
1537
+ try {
1538
+ const sourceLanguage = sourceDocument[languageFieldName];
1539
+ if (devLog.debug("Checking source document language", {
1540
+ ...logContext,
1541
+ sourceLanguage,
1542
+ languageFieldName
1543
+ }), !sourceLanguage) {
1544
+ const error = `Source document ${sourceDocument._id} does not have a language`;
1545
+ throw devLog.error(error, void 0, logContext), new Error(error);
1546
+ }
1547
+ devLog.debug("Getting translation metadata", logContext);
1548
+ const translationMetadata = await getTranslationMetadata({
1549
+ client,
1550
+ sourceDocument,
1551
+ sourceLanguage
1552
+ }), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
1553
+ (history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
1554
+ ) ?? !1;
1555
+ if (devLog.debug("Checking if translation is already fresh", {
1556
+ ...logContext,
1557
+ isAlreadyFreshTranslation,
1558
+ currentRev: sourceDocument._rev
1559
+ }), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
1560
+ devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
1561
+ targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
1562
+ }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
1563
+ return;
1564
+ }
1565
+ const translationTarget = determineTranslationTarget({
1566
+ translationMetadata,
1567
+ targetLanguage
1568
+ }), isNewTranslation = translationTarget.operation === "create";
1569
+ devLog.debug("Translation target determined", {
1570
+ ...logContext,
1571
+ operation: translationTarget.operation,
1572
+ isNewTranslation
1573
+ });
1574
+ let newDocument;
1575
+ if (!isNewTranslation && shouldTranslateDiffs) {
1576
+ devLog.info("Starting diff-based translation", logContext);
1577
+ const { currentTargetId, patch } = await patchTranslationTarget({
1578
+ client,
1579
+ currentSourceDocument: sourceDocument,
1580
+ sourceLanguage,
1581
+ translationMetadata,
1582
+ targetLanguage
1583
+ });
1584
+ if (!currentTargetId) {
1585
+ devLog.warn("No target ID found for diff translation", logContext), devLog.timeEnd("translateDocument"), devLog.groupEnd();
1586
+ return;
1587
+ }
1588
+ devLog.debug("Patching translation target", {
1589
+ ...logContext,
1590
+ currentTargetId,
1591
+ hasPatch: !!patch
1592
+ }), newDocument = await translateDocumentInPlace({
1593
+ client,
1594
+ schemaId,
1595
+ dispatch,
1596
+ sourceDocument,
1597
+ languageFieldName,
1598
+ targetLanguage,
1599
+ targetId: currentTargetId,
1600
+ patch
1601
+ });
1602
+ } else
1603
+ devLog.info("Starting full document translation", {
1604
+ ...logContext,
1605
+ translationTarget: translationTarget.operation
1606
+ }), newDocument = await translateEntireDocument({
1607
+ dispatch,
1608
+ client,
1609
+ schemaId,
1610
+ sourceDocument,
1611
+ targetLanguage,
1612
+ languageFieldName,
1613
+ translationTarget,
1614
+ translationPrompts
1615
+ });
1616
+ if (!newDocument)
1617
+ 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");
1618
+ return devLog.debug("Translation document created", {
1619
+ ...logContext,
1620
+ newDocumentId: newDocument._id
1621
+ }), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
1622
+ targetId: newDocument._id
1623
+ }), await updateTranslationMetadata({
1624
+ client,
1625
+ sourceDocument,
1626
+ sourceLanguage,
1627
+ newDocument,
1628
+ targetLanguage,
1629
+ translationMetadata,
1630
+ isNewTranslation
1631
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"), devLog.info("Translation completed successfully", {
1632
+ ...logContext,
1633
+ newDocumentId: newDocument._id
1634
+ }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
1635
+ } catch (error) {
1636
+ throw devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd(), error;
1637
+ }
1638
+ }, patchTranslationTarget = async ({
1639
+ client,
1640
+ currentSourceDocument,
1641
+ translationMetadata,
1642
+ targetLanguage
1643
+ }) => {
1644
+ const sourceSnapshotString = translationMetadata.translationHistory.find(
1645
+ (h) => undraftId(h.sourceId) === undraftId(currentSourceDocument._id)
1646
+ )?.sourceSnapshot;
1647
+ if (!sourceSnapshotString)
1648
+ return {};
1649
+ const sourceSnapshot = JSON.parse(sourceSnapshotString), targetId = translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref;
1650
+ if (!targetId)
1651
+ return {};
1652
+ const currentTargetId = await client.fetch(
1653
+ '*[_id in $ids] | order((_id match "drafts*") desc)[0]._id',
1654
+ { ids: [undraftId(targetId), draftId(targetId)] }
1655
+ );
1656
+ if (!currentTargetId)
1657
+ return console.log(`No document found for ${targetId}`), {};
1658
+ const patch = diffPatch(sourceSnapshot, currentSourceDocument, {
1659
+ id: draftId(currentTargetId)
1660
+ });
1661
+ return { currentTargetId, patch };
1662
+ }, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
1663
+ client,
1664
+ sourceDocument,
1665
+ targetLanguages,
1666
+ dispatch,
1667
+ languageFieldName,
1668
+ shouldOverwriteFreshTranslations,
1669
+ schemaId,
1670
+ references = [],
1671
+ maxConcurrentTranslationJobs,
1672
+ shouldTranslateDiffs
1673
+ }) => {
1674
+ const logContext = {
1675
+ documentId: sourceDocument._id,
1676
+ targetLanguages,
1677
+ referencesCount: references.length,
1678
+ maxConcurrentJobs: maxConcurrentTranslationJobs,
1679
+ shouldTranslateDiffs,
1680
+ shouldOverwriteFreshTranslations
1681
+ };
1682
+ devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
1683
+ try {
1684
+ const sourceLanguage = sourceDocument[languageFieldName];
1685
+ if (!sourceLanguage)
1686
+ throw devLog.error("Source language not found"), new Error("Source language not found");
1687
+ maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
1688
+ ...logContext,
1689
+ newPermits: maxConcurrentTranslationJobs
1690
+ }), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
1691
+ type: "SET_OVERALL_STATUS",
1692
+ payload: "translating"
1693
+ });
1694
+ const sourceDocuments = [
1695
+ {
1696
+ _id: sourceDocument._id,
1697
+ _type: sourceDocument._type,
1698
+ // TODO: Handle this properly
1699
+ title: sourceDocument.name || sourceDocument.title
1700
+ },
1701
+ ...references
1702
+ ], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), targetDocumentIds = [];
1703
+ devLog.debug("Prepared document set for translation", {
1704
+ ...logContext,
1705
+ sourceDocumentIds: sourceDocumentIds.length,
1706
+ totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
1707
+ });
1708
+ const translationPrompts = await fetchTranslationPrompts({
1709
+ client,
1710
+ sourceLanguage,
1711
+ targetLanguages
1712
+ });
1713
+ devLog.info("Translation prompts", translationPrompts), setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
1714
+ const translationTasks = sourceDocumentIds.flatMap(
1715
+ (documentId) => targetLanguages.map(async (targetLanguage) => {
1716
+ await translationSemaphore.withPermit(async () => {
1717
+ const document = await client.withConfig({
1718
+ // This enables us to detect documents that might be stale due to unpublished changes
1719
+ perspective: "raw"
1720
+ }).fetch("*[_id == $id][0]", {
1721
+ id: documentId
1722
+ });
1723
+ if (document) {
1724
+ const targetDocumentId = await translateDocument({
1725
+ dispatch,
1726
+ client,
1727
+ sourceDocument: document,
1728
+ targetLanguage,
1729
+ languageFieldName,
1730
+ shouldOverwriteFreshTranslations,
1731
+ schemaId,
1732
+ shouldTranslateDiffs,
1733
+ translationPrompts
1734
+ });
1735
+ targetDocumentId && targetDocumentIds.push(targetDocumentId);
1736
+ }
1737
+ });
1738
+ })
1739
+ );
1740
+ await Promise.all(translationTasks), devLog.info("All translation tasks completed", {
1741
+ ...logContext,
1742
+ targetDocumentIds: targetDocumentIds.length
1743
+ }), dispatch({
1744
+ type: "SET_OVERALL_STATUS",
1745
+ payload: "linking_references"
1746
+ }), devLog.debug("Starting reference localization", {
1747
+ ...logContext,
1748
+ documentsToLocalize: targetDocumentIds.length
1749
+ }), await Promise.all(
1750
+ targetDocumentIds.map(async (documentId) => {
1751
+ await localizeReferences({
1752
+ client,
1753
+ documentId,
1754
+ languageFieldName
1755
+ });
1756
+ })
1757
+ ), devLog.info("Document set translation completed successfully", logContext), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" });
1758
+ } catch (error) {
1759
+ return devLog.error("translateDocumentSet failed", error instanceof Error ? error : new Error(String(error)), logContext), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), Promise.reject(error);
1760
+ }
1761
+ }, ActionButtonBar = ({ setIsDialogOpen }) => {
1762
+ const {
1763
+ document,
1764
+ config,
1765
+ overallStatus,
1766
+ dispatch,
1767
+ references,
1768
+ selectedTargetLanguages,
1769
+ shouldOverwriteFreshTranslations,
1770
+ shouldTranslateDiffs
1771
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" });
1772
+ return /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
1773
+ /* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
1774
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: () => {
1775
+ dispatch({
1776
+ type: "SET_SELECTED_TARGET_LANGUAGES",
1777
+ payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
1778
+ });
1779
+ }, children: "Select All" }),
1780
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: () => {
1781
+ dispatch({
1782
+ type: "SET_SELECTED_TARGET_LANGUAGES",
1783
+ payload: []
1784
+ });
1785
+ }, children: "Select None" })
1786
+ ] }),
1787
+ /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
1788
+ /* @__PURE__ */ jsx(Button, { onClick: () => setIsDialogOpen(!1), mode: "ghost", size: 2, children: "Cancel" }),
1789
+ /* @__PURE__ */ jsx(
1790
+ Button,
1791
+ {
1792
+ mode: "ghost",
1793
+ tone: "positive",
1794
+ size: 2,
1795
+ onClick: () => {
1796
+ translateDocumentSet({
1797
+ sourceDocument: document,
1798
+ targetLanguages: selectedTargetLanguages,
1799
+ client,
1800
+ dispatch,
1801
+ languageFieldName: config.languageFieldName,
1802
+ schemaId: config.schemaId,
1803
+ references,
1804
+ shouldOverwriteFreshTranslations,
1805
+ maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
1806
+ shouldTranslateDiffs
1807
+ });
1808
+ },
1809
+ disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
1810
+ children: "Translate"
1811
+ }
1812
+ )
1813
+ ] })
1814
+ ] });
1815
+ }, LanguageCheckbox = ({ lang, isLoading }) => {
1816
+ const { document, dispatch, selectedTargetLanguages, translationMetadata } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), hasTranslation = translationMetadata?.translations?.some((translation) => translation._key === lang.id), isStale = hasTranslation && translationMetadata?.translationHistory?.some(
1817
+ (history) => history.language === lang.id && history.sourceRev !== document._rev
1818
+ ), handleChange = () => {
1819
+ if (isDisabled) return;
1820
+ const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
1821
+ dispatch({
1822
+ type: "SET_SELECTED_TARGET_LANGUAGES",
1823
+ payload: updatedLanguages
1824
+ });
1825
+ };
1826
+ return isLoading ? /* @__PURE__ */ jsx(Button, { type: "button", mode: "bleed", size: 2, onClick: handleChange, disabled: !0, muted: !0, children: /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", paddingY: 3, children: /* @__PURE__ */ jsx(Spinner, { size: 1 }) }) }) : /* @__PURE__ */ jsx(
1827
+ Button,
1828
+ {
1829
+ type: "button",
1830
+ mode: isChecked ? "ghost" : "bleed",
1831
+ size: 2,
1832
+ onClick: handleChange,
1833
+ disabled: isDisabled,
1834
+ tone: isChecked ? "primary" : "default",
1835
+ muted: !0,
1836
+ children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: [
1837
+ /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
1838
+ /* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
1839
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
1840
+ /* @__PURE__ */ jsx(Text, { children: lang.title }),
1841
+ isDisabled ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" }) : /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: hasTranslation && !isDisabled ? "Already translated" : "Not translated" })
1842
+ ] })
1843
+ ] }),
1844
+ !isDisabled && hasTranslation && /* @__PURE__ */ jsx(Flex, { flex: 1, justify: "flex-end", children: /* @__PURE__ */ jsx(Badge, { tone: isStale ? "caution" : "positive", size: 3, muted: !0, children: isStale ? "Stale" : "Fresh" }) })
1845
+ ] })
1846
+ }
1847
+ );
1848
+ }, LanguageSelectionPanel = () => {
1849
+ const { document, config } = useTranslationContext(), { loading } = useTranslationMetadata({
1850
+ documentId: document._id
1851
+ });
1852
+ return /* @__PURE__ */ jsx(Card, { padding: 4, muted: !0, border: !0, children: /* @__PURE__ */ jsx(Grid, { columns: [1, 2], gap: 3, children: config?.supportedLanguages.map((lang) => /* @__PURE__ */ jsx(LanguageCheckbox, { lang, isLoading: loading }, lang.id)) }) });
1853
+ }, assembleReferences = async ({
1854
+ client,
1855
+ sourceDocument,
1856
+ maxReferenceDepth,
1857
+ translatableSchemaTypes = []
1858
+ }) => {
1859
+ if (!client)
1860
+ throw new Error("assembleReferences: client is required");
1861
+ if (!sourceDocument || !sourceDocument._id)
1862
+ throw new Error("assembleReferences: sourceDocument with _id is required");
1863
+ if (typeof maxReferenceDepth != "number" || maxReferenceDepth < 0)
1864
+ throw new Error("assembleReferences: maxReferenceDepth must be a non-negative number");
1865
+ const references = /* @__PURE__ */ new Set(), documentCache = /* @__PURE__ */ new Map(), errors = [];
1866
+ let currentDepthDocuments = [sourceDocument];
1867
+ for (let depth = 0; depth < maxReferenceDepth && currentDepthDocuments.length > 0; depth++) {
1868
+ const allRefIds = /* @__PURE__ */ new Set();
1869
+ for (const document of currentDepthDocuments)
1870
+ try {
1871
+ const refResults = findKeyValues(document, "_ref");
1872
+ for (const result2 of refResults) {
1873
+ const refId = result2.value;
1874
+ typeof refId == "string" && refId.length > 0 && !references.has(refId) && !documentCache.has(refId) && allRefIds.add(refId);
1875
+ }
1876
+ } catch (error) {
1877
+ console.error("assembleReferences: Error finding references in document:", error);
1878
+ }
1879
+ if (allRefIds.size === 0)
1880
+ break;
1881
+ const refIdArray = Array.from(allRefIds);
1882
+ try {
1883
+ const batchResults = await client.fetch("*[_id in $ids]", { ids: refIdArray }), fetchedDocuments = /* @__PURE__ */ new Map();
1884
+ for (const doc of batchResults)
1885
+ fetchedDocuments.set(doc._id, doc);
1886
+ const nextDepthDocuments = [];
1887
+ for (const refId of refIdArray) {
1888
+ const fetchedDoc = fetchedDocuments.get(refId);
1889
+ if (fetchedDoc) {
1890
+ if (translatableSchemaTypes.length > 0 && !translatableSchemaTypes.includes(fetchedDoc._type)) {
1891
+ documentCache.set(refId, null);
1892
+ continue;
1893
+ }
1894
+ documentCache.set(refId, fetchedDoc), references.add(refId), nextDepthDocuments.push(fetchedDoc);
1895
+ } else
1896
+ documentCache.set(refId, null), console.warn(`assembleReferences: Document with _id "${refId}" not found`);
1897
+ }
1898
+ currentDepthDocuments = nextDepthDocuments;
1899
+ } catch (fetchError) {
1900
+ const error = fetchError instanceof Error ? fetchError : new Error(String(fetchError));
1901
+ errors.push({ refId: `batch(${refIdArray.join(",")})`, error }), console.error("assembleReferences: Failed to batch fetch documents:", error.message);
1902
+ break;
1903
+ }
1904
+ }
1905
+ errors.length > 0 && console.warn(`assembleReferences: Encountered ${errors.length} error(s) while fetching references`);
1906
+ const result = [];
1907
+ for (const refId of references) {
1908
+ const cachedDoc = documentCache.get(refId);
1909
+ cachedDoc && result.push({
1910
+ _id: cachedDoc._id,
1911
+ _type: cachedDoc._type,
1912
+ title: cachedDoc.name || cachedDoc.title
1913
+ });
1914
+ }
1915
+ return result;
1916
+ }, ReferenceConfigPanel = () => {
1917
+ const { dispatch, document, shouldTranslateReferences, maxReferenceDepth, config, references } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1);
1918
+ useEffect(() => {
1919
+ (async () => {
1920
+ if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
1921
+ dispatch({
1922
+ type: "SET_REFERENCES",
1923
+ payload: []
1924
+ });
1925
+ return;
1926
+ }
1927
+ const newReferences = await assembleReferences({
1928
+ client,
1929
+ sourceDocument: document,
1930
+ maxReferenceDepth,
1931
+ translatableSchemaTypes: config.translatableSchemaTypes
1932
+ });
1933
+ dispatch({
1934
+ type: "SET_REFERENCES",
1935
+ payload: newReferences
1936
+ }), setIsLoadingReferences(!1);
1937
+ })();
1938
+ }, [document, client, dispatch, shouldTranslateReferences, maxReferenceDepth, config.translatableSchemaTypes]);
1939
+ const handleMaxReferenceDepthChange = async (event) => {
1940
+ dispatch({
1941
+ type: "SET_MAX_REFERENCE_DEPTH",
1942
+ payload: Number.parseInt(event.target.value)
1943
+ });
1944
+ }, handleShouldTranslateChange = () => {
1945
+ dispatch({
1946
+ type: "SET_SHOULD_TRANSLATE_REFERENCES",
1947
+ payload: !shouldTranslateReferences
1948
+ });
1949
+ };
1950
+ return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1951
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
1952
+ /* @__PURE__ */ jsx(
1953
+ Button,
1954
+ {
1955
+ type: "button",
1956
+ mode: shouldTranslateReferences ? "ghost" : "bleed",
1957
+ size: 2,
1958
+ onClick: handleShouldTranslateChange,
1959
+ tone: shouldTranslateReferences ? "primary" : "neutral",
1960
+ children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
1961
+ /* @__PURE__ */ jsx(
1962
+ Checkbox,
1963
+ {
1964
+ checked: shouldTranslateReferences,
1965
+ onChange: handleShouldTranslateChange,
1966
+ label: shouldTranslateReferences ? "Disable" : "Enable"
1967
+ }
1968
+ ),
1969
+ /* @__PURE__ */ jsx(Text, { children: "Translate References" })
1970
+ ] })
1971
+ }
1972
+ ),
1973
+ shouldTranslateReferences ? /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
1974
+ /* @__PURE__ */ jsx(Text, { children: "Max Depth:" }),
1975
+ /* @__PURE__ */ jsxs(
1976
+ Select,
1977
+ {
1978
+ disabled: !shouldTranslateReferences,
1979
+ placeholder: "Depth",
1980
+ onChange: handleMaxReferenceDepthChange,
1981
+ value: maxReferenceDepth.toString(),
1982
+ children: [
1983
+ /* @__PURE__ */ jsx("option", { value: 1, children: "1" }),
1984
+ /* @__PURE__ */ jsx("option", { value: 2, children: "2" }),
1985
+ /* @__PURE__ */ jsx("option", { value: 3, children: "3" })
1986
+ ]
1987
+ }
1988
+ )
1989
+ ] }) : null
1990
+ ] }),
1991
+ shouldTranslateReferences ? /* @__PURE__ */ jsx(Flex, { padding: 2, justify: "flex-end", children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : `${references.length} translatable references found.` }) }) : null
1992
+ ] });
1993
+ }, WriteSettingsPanel = () => {
1994
+ const { dispatch, shouldOverwriteFreshTranslations, shouldTranslateDiffs } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
1995
+ dispatch({
1996
+ type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
1997
+ payload: !shouldOverwriteFreshTranslations
1998
+ });
1999
+ };
2000
+ return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2001
+ /* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
2002
+ /* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
2003
+ Button,
2004
+ {
2005
+ type: "button",
2006
+ mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
2007
+ size: 2,
2008
+ onClick: handleShouldOverwriteFreshTranslationsChange,
2009
+ tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
2010
+ children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2011
+ /* @__PURE__ */ jsx(
2012
+ Checkbox,
2013
+ {
2014
+ checked: shouldOverwriteFreshTranslations,
2015
+ onChange: handleShouldOverwriteFreshTranslationsChange,
2016
+ label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
2017
+ }
2018
+ ),
2019
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2020
+ /* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
2021
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
2022
+ ] })
2023
+ ] })
2024
+ }
2025
+ ) }),
2026
+ /* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
2027
+ Button,
2028
+ {
2029
+ type: "button",
2030
+ mode: shouldTranslateDiffs ? "ghost" : "bleed",
2031
+ size: 2,
2032
+ onClick: () => {
2033
+ dispatch({
2034
+ type: "SET_SHOULD_TRANSLATE_DIFFS",
2035
+ payload: !shouldTranslateDiffs
2036
+ });
2037
+ },
2038
+ tone: shouldTranslateDiffs ? "primary" : "neutral",
2039
+ children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2040
+ /* @__PURE__ */ jsx(
2041
+ Checkbox,
2042
+ {
2043
+ checked: shouldTranslateDiffs,
2044
+ onChange: handleShouldOverwriteFreshTranslationsChange,
2045
+ label: shouldTranslateDiffs ? "Disable" : "Enable"
2046
+ }
2047
+ ),
2048
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2049
+ /* @__PURE__ */ jsxs(Inline, { space: 1, children: [
2050
+ /* @__PURE__ */ jsx(Text, { children: "Translate Diffs" }),
2051
+ /* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Experimental" })
2052
+ ] }),
2053
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Only re-translate content that has changed in the source document." })
2054
+ ] })
2055
+ ] })
2056
+ }
2057
+ ) })
2058
+ ] });
2059
+ }, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2060
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2061
+ /* @__PURE__ */ jsx(Label, { children: "Languages" }),
2062
+ /* @__PURE__ */ jsx(LanguageSelectionPanel, {})
2063
+ ] }),
2064
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2065
+ /* @__PURE__ */ jsx(Label, { children: "Settings" }),
2066
+ /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
2067
+ ] }),
2068
+ /* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
2069
+ ] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
2070
+ const { overallStatus, translationJobs } = useTranslationContext(), headline = useMemo(
2071
+ () => overallStatus === "completed" ? "Translation completed" : "Translation in progress",
2072
+ [overallStatus]
2073
+ ), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]);
2074
+ return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2075
+ /* @__PURE__ */ jsx(Heading, { size: 2, children: headline }),
2076
+ /* @__PURE__ */ jsx(ProgressBar, { progress: jobs.filter((job) => TERMINAL_STATUSES.includes(job.status)).length, max: jobs.length }),
2077
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2078
+ /* @__PURE__ */ jsx(Label, { children: "Translations" }),
2079
+ /* @__PURE__ */ jsx(Card, { padding: 2, tone: "transparent", children: /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx("ul", { children: jobs.map((job) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
2080
+ /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2081
+ /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", style: { width: "24px", height: "24px" }, children: ["completed", "skipped"].includes(job.status) ? /* @__PURE__ */ jsx(CheckmarkIcon, { fontSize: 24 }) : job.status === "queued" ? /* @__PURE__ */ jsx(CircleIcon, { fontSize: 24 }) : /* @__PURE__ */ jsx(Spinner, {}) }),
2082
+ /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2083
+ /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2084
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: job.title || "Untitled Document" }),
2085
+ /* @__PURE__ */ jsx(ArrowRightIcon, { fontSize: 18 }),
2086
+ /* @__PURE__ */ jsx(Code, { size: 1, children: job.language })
2087
+ ] }),
2088
+ /* @__PURE__ */ jsxs(Code, { size: 1, children: [
2089
+ job._type,
2090
+ " \u2014 ",
2091
+ job._id.slice(-6)
2092
+ ] })
2093
+ ] })
2094
+ ] }),
2095
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
2096
+ job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
2097
+ job.targetId ? /* @__PURE__ */ jsx(
2098
+ TranslationOpenerButton,
2099
+ {
2100
+ target: {
2101
+ _id: job.targetId,
2102
+ _type: job._type
2103
+ }
2104
+ }
2105
+ ) : null
2106
+ ] })
2107
+ ] }) }) }, job._id)) }) }) })
2108
+ ] })
2109
+ ] });
2110
+ }, ProgressBar = ({ progress, max = 100 }) => {
2111
+ const percentage = Math.min(progress / max * 100, 100);
2112
+ return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2113
+ /* @__PURE__ */ jsx(
2114
+ "div",
2115
+ {
2116
+ style: {
2117
+ width: "100%",
2118
+ height: "8px",
2119
+ backgroundColor: "var(--card-border-color)",
2120
+ borderRadius: "4px",
2121
+ overflow: "hidden"
2122
+ },
2123
+ children: /* @__PURE__ */ jsx(
2124
+ "div",
2125
+ {
2126
+ style: {
2127
+ width: `${percentage}%`,
2128
+ height: "100%",
2129
+ backgroundColor: "var(--card-accent-fg-color)",
2130
+ borderRadius: "4px",
2131
+ transition: "width 0.3s ease-in-out"
2132
+ }
2133
+ }
2134
+ )
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
2138
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2139
+ percentage.toFixed(0),
2140
+ "%"
2141
+ ] }),
2142
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2143
+ progress,
2144
+ " / ",
2145
+ max,
2146
+ " jobs completed."
2147
+ ] })
2148
+ ] })
2149
+ ] });
2150
+ }, TranslationOpenerButton = ({ target }) => {
2151
+ const openInNewPane = useOpenInNewPane(target._id, target._type);
2152
+ return /* @__PURE__ */ jsx(Button, { mode: "bleed", text: "View", iconRight: PanelRightIcon, onClick: () => openInNewPane() });
2153
+ }, SkippedBadge = () => /* @__PURE__ */ jsx(
2154
+ Tooltip,
2155
+ {
2156
+ portal: !0,
2157
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
2158
+ children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
2159
+ }
2160
+ ), STATES_IN_WHICH_TO_SHOW_CONFIG = ["fetching_metadata", "idle"], STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS = [
2161
+ "translating",
2162
+ "linking_references",
2163
+ "completed"
2164
+ ], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
2165
+ const { overallStatus, document } = useTranslationContext();
2166
+ return devLog.debug("TranslationDialog rendered", {
2167
+ documentId: document._id,
2168
+ overallStatus,
2169
+ component: "TranslationDialog"
2170
+ }), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
2171
+ documentId: document._id,
2172
+ overallStatus,
2173
+ component: "TranslationDialog"
2174
+ }), /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen })) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? (devLog.debug("Showing translation progress panel", {
2175
+ documentId: document._id,
2176
+ overallStatus,
2177
+ component: "TranslationDialog"
2178
+ }), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
2179
+ documentId: document._id,
2180
+ overallStatus,
2181
+ component: "TranslationDialog"
2182
+ }), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
2183
+ documentId: document._id,
2184
+ overallStatus,
2185
+ component: "TranslationDialog"
2186
+ }), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
2187
+ }, getTranslateAction = (config) => ({ draft, published }) => {
2188
+ const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
2189
+ return devLog.debug("TranslateAction initialized", {
2190
+ documentId: document?._id,
2191
+ documentType: document?._type
2192
+ }), !document || !config.translatableSchemaTypes?.includes(document._type) ? (devLog.warn("Document not in translatable schema types", {
2193
+ documentType: document?._type,
2194
+ translatableTypes: config.translatableSchemaTypes
2195
+ }), null) : config ? {
2196
+ label: "Translate",
2197
+ icon: TranslateIcon,
2198
+ onHandle: () => {
2199
+ devLog.info("Translation dialog opened", {
2200
+ documentId: document._id,
2201
+ documentType: document._type,
2202
+ component: "TranslateAction"
2203
+ }), setIsDialogOpen(!0);
2204
+ },
2205
+ dialog: isDialogOpen && {
2206
+ type: "dialog",
2207
+ onClose: () => setIsDialogOpen(!1),
2208
+ header: "Translate Document",
2209
+ content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
2210
+ }
2211
+ } : (devLog.error("Missing config in TranslateAction"), null);
2212
+ }, aiTranslationPlugin = definePlugin((config) => ({
2213
+ name: "sanity-plugin-ai-translation",
2214
+ document: {
2215
+ actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
2216
+ },
2217
+ schema: {
2218
+ types: [translationSettings, translationGlossaryTerm]
2219
+ }
2220
+ })), translationHistorySchema = translationHistory;
2221
+ export {
2222
+ aiTranslationPlugin,
2223
+ translationHistorySchema
2224
+ };
2225
+ //# sourceMappingURL=index.js.map