@braze/sanity-plugin-ai-translation 0.1.77 → 0.2.1
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/README.md +76 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1965 -531
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { useClient, defineType, defineField, pathToString, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, ArrowLeftIcon, EditIcon,
|
|
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,
|
|
3
|
+
import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, WarningOutlineIcon, InfoOutlineIcon, ArrowLeftIcon, EditIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
|
|
4
|
+
import { createContext, useContext, useEffect, useState, useReducer, useCallback, useMemo } from "react";
|
|
5
|
+
import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Select, TextInput, TextArea, Heading, Inline, Badge, Grid, Radio, Label, Code } from "@sanity/ui";
|
|
6
6
|
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
7
7
|
import defaults from "lodash.defaults";
|
|
8
|
-
import {
|
|
8
|
+
import { toPlainText } from "@portabletext/react";
|
|
9
|
+
import cloneDeep from "lodash.clonedeep";
|
|
10
|
+
import get from "lodash.get";
|
|
11
|
+
import set from "lodash.set";
|
|
9
12
|
const TranslationContext = createContext({
|
|
10
13
|
document: null,
|
|
11
14
|
config: null,
|
|
12
15
|
overallStatus: "idle",
|
|
13
16
|
selectedTargetLanguages: [],
|
|
14
17
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
15
|
-
shouldTranslateDiffs: !1,
|
|
16
18
|
shouldOverwriteFreshTranslations: !1,
|
|
17
19
|
shouldTranslateReferences: !1,
|
|
18
20
|
maxReferenceDepth: 1,
|
|
19
21
|
references: [],
|
|
20
22
|
translationMetadata: null,
|
|
23
|
+
translationMode: "sanity-only",
|
|
24
|
+
openaiValidated: !1,
|
|
21
25
|
dispatch: () => {
|
|
22
26
|
}
|
|
23
27
|
}), useTranslationContext = () => {
|
|
@@ -27,7 +31,7 @@ const TranslationContext = createContext({
|
|
|
27
31
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
28
32
|
);
|
|
29
33
|
return context;
|
|
30
|
-
}, TRANSLATION_METADATA_TYPE = "translation.metadata",
|
|
34
|
+
}, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
31
35
|
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
32
36
|
params: {
|
|
33
37
|
documentId: undraftId(documentId),
|
|
@@ -80,6 +84,102 @@ const TranslationContext = createContext({
|
|
|
80
84
|
isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
|
|
81
85
|
}
|
|
82
86
|
};
|
|
87
|
+
async function validateSuggestion(client, suggestion) {
|
|
88
|
+
return suggestion.type === "glossaryTerm" ? validateGlossaryTerm(client, suggestion) : suggestion.type === "protectedPhrase" ? validateProtectedPhrase(client, suggestion) : suggestion.type === "globalPrompt" || suggestion.type === "localePrompt" ? validatePrompt(suggestion) : { valid: !0, status: "valid" };
|
|
89
|
+
}
|
|
90
|
+
async function validateGlossaryTerm(client, suggestion) {
|
|
91
|
+
const { sourceValue, targetValue, sourceLocale, targetLocale } = suggestion;
|
|
92
|
+
if (!sourceValue || !targetValue || !sourceLocale || !targetLocale)
|
|
93
|
+
return {
|
|
94
|
+
valid: !1,
|
|
95
|
+
status: "invalid",
|
|
96
|
+
reason: "Missing required fields for glossary term"
|
|
97
|
+
};
|
|
98
|
+
if (sourceValue.trim() === targetValue.trim())
|
|
99
|
+
return {
|
|
100
|
+
valid: !1,
|
|
101
|
+
status: "invalid",
|
|
102
|
+
reason: `Source and target are identical ("${sourceValue}") - this should be a protected phrase instead`
|
|
103
|
+
};
|
|
104
|
+
const protectedPhrases = await client.fetch('*[_id == "translation-settings"][0].protectedPhrases');
|
|
105
|
+
if (protectedPhrases?.some((phrase) => phrase.toLowerCase() === sourceValue.toLowerCase()))
|
|
106
|
+
return {
|
|
107
|
+
valid: !1,
|
|
108
|
+
status: "invalid",
|
|
109
|
+
reason: `Cannot add "${sourceValue}" to glossary because it is already a protected phrase. Protected phrases are never translated. If you want to create a translation mapping for this term, please remove it from protected phrases first by editing Translation Settings.`
|
|
110
|
+
};
|
|
111
|
+
if (targetValue && protectedPhrases?.some((phrase) => phrase.toLowerCase() === targetValue.toLowerCase()))
|
|
112
|
+
return {
|
|
113
|
+
valid: !1,
|
|
114
|
+
status: "invalid",
|
|
115
|
+
reason: `Cannot use "${targetValue}" as a translation target because it is a protected phrase. Protected phrases should not appear as translation targets.`
|
|
116
|
+
};
|
|
117
|
+
const existingTerm = await client.fetch(
|
|
118
|
+
`*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0][0] {
|
|
119
|
+
translations
|
|
120
|
+
}`,
|
|
121
|
+
{ sourceLocale, sourceValue }
|
|
122
|
+
);
|
|
123
|
+
if (existingTerm) {
|
|
124
|
+
const existingTarget = existingTerm.translations.find((t) => t._key === targetLocale);
|
|
125
|
+
if (existingTarget?.value === targetValue)
|
|
126
|
+
return {
|
|
127
|
+
valid: !0,
|
|
128
|
+
status: "warning",
|
|
129
|
+
reason: `This translation already exists in the glossary: "${sourceValue}" \u2192 "${targetValue}"`
|
|
130
|
+
};
|
|
131
|
+
if (existingTarget && existingTarget.value !== targetValue)
|
|
132
|
+
return {
|
|
133
|
+
valid: !0,
|
|
134
|
+
status: "warning",
|
|
135
|
+
reason: `"${sourceValue}" already has a different translation in the glossary: "${existingTarget.value}". Applying this will update it to "${targetValue}".`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return { valid: !0, status: "valid" };
|
|
139
|
+
}
|
|
140
|
+
async function validateProtectedPhrase(client, suggestion) {
|
|
141
|
+
const phrase = suggestion.description.trim();
|
|
142
|
+
if (!phrase)
|
|
143
|
+
return {
|
|
144
|
+
valid: !1,
|
|
145
|
+
status: "invalid",
|
|
146
|
+
reason: "Protected phrase cannot be empty"
|
|
147
|
+
};
|
|
148
|
+
if ((await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.some((p) => p.toLowerCase() === phrase.toLowerCase()))
|
|
149
|
+
return {
|
|
150
|
+
valid: !0,
|
|
151
|
+
status: "warning",
|
|
152
|
+
reason: `"${phrase}" is already in protected phrases`
|
|
153
|
+
};
|
|
154
|
+
const glossaryTermWithPhrase = await client.fetch(
|
|
155
|
+
`*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0][0] {
|
|
156
|
+
_id,
|
|
157
|
+
translations
|
|
158
|
+
}`,
|
|
159
|
+
{ phrase }
|
|
160
|
+
);
|
|
161
|
+
if (glossaryTermWithPhrase) {
|
|
162
|
+
const matchingTranslations = glossaryTermWithPhrase.translations.filter((t) => t.value.toLowerCase() === phrase.toLowerCase()).map((t) => t._key).join(", ");
|
|
163
|
+
return {
|
|
164
|
+
valid: !1,
|
|
165
|
+
status: "invalid",
|
|
166
|
+
reason: `Cannot add "${phrase}" as a protected phrase because it exists in the glossary as a translation (${matchingTranslations}). If you want to prevent this term from being translated, please remove it from the glossary first by editing Translation Settings.`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { valid: !0, status: "valid" };
|
|
170
|
+
}
|
|
171
|
+
function validatePrompt(suggestion) {
|
|
172
|
+
const update = suggestion.description.trim();
|
|
173
|
+
return update ? update.toLowerCase().includes("has been") || update.toLowerCase().includes("was changed") || update.toLowerCase().includes("corrected to") ? {
|
|
174
|
+
valid: !0,
|
|
175
|
+
status: "warning",
|
|
176
|
+
reason: "This update may be describing past changes rather than adding new rules"
|
|
177
|
+
} : { valid: !0, status: "valid" } : {
|
|
178
|
+
valid: !1,
|
|
179
|
+
status: "invalid",
|
|
180
|
+
reason: "Prompt update cannot be empty"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
83
183
|
async function analyzeFeedback({
|
|
84
184
|
client,
|
|
85
185
|
sourceLanguage,
|
|
@@ -107,8 +207,25 @@ async function analyzeFeedback({
|
|
|
107
207
|
currentDocument
|
|
108
208
|
}), analysis = await analyzeWithAI(client, analysisPrompt);
|
|
109
209
|
devLog.info("AI analysis complete", { analysis });
|
|
110
|
-
const suggestions = convertAnalysisToSuggestions(analysis);
|
|
111
|
-
|
|
210
|
+
const suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
|
|
211
|
+
devLog.info("Generated suggestions", { count: suggestions.length }), onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
|
|
212
|
+
const validatedSuggestions = await Promise.all(
|
|
213
|
+
suggestions.map(async (suggestion) => {
|
|
214
|
+
const validation = await validateSuggestion(client, suggestion);
|
|
215
|
+
return {
|
|
216
|
+
...suggestion,
|
|
217
|
+
warning: validation.reason,
|
|
218
|
+
validationStatus: validation.status,
|
|
219
|
+
// Only auto-approve valid suggestions; invalid ones cannot be approved
|
|
220
|
+
approved: validation.status === "valid"
|
|
221
|
+
};
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
return devLog.info("Suggestions validated", {
|
|
225
|
+
valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
|
|
226
|
+
warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
|
|
227
|
+
invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
|
|
228
|
+
}), validatedSuggestions;
|
|
112
229
|
}
|
|
113
230
|
async function applySuggestions({
|
|
114
231
|
client,
|
|
@@ -216,11 +333,67 @@ function buildAnalysisPrompt({
|
|
|
216
333
|
"",
|
|
217
334
|
"Be conservative: only suggest changes when there is clear evidence.",
|
|
218
335
|
"If in doubt, prefer locale-specific updates over global updates.",
|
|
219
|
-
"Avoid redundancy - if you add a protected phrase, do NOT mention it in prompts."
|
|
336
|
+
"Avoid redundancy - if you add a protected phrase, do NOT mention it in prompts.",
|
|
337
|
+
"",
|
|
338
|
+
"CONCRETE EXAMPLES:",
|
|
339
|
+
"",
|
|
340
|
+
"Example 1 - Brand name protection:",
|
|
341
|
+
'User feedback: "Braze should never be translated"',
|
|
342
|
+
"Correct output:",
|
|
343
|
+
"{",
|
|
344
|
+
' "protectedPhrasesToAdd": ["Braze"],',
|
|
345
|
+
' "reasoning": "User explicitly stated Braze is a brand name that should not be translated"',
|
|
346
|
+
"}",
|
|
347
|
+
"WRONG: Do not add this to glossary or prompts.",
|
|
348
|
+
"",
|
|
349
|
+
"Example 2 - Terminology correction:",
|
|
350
|
+
'Edit analysis: User changed "discussion" to "chat" consistently in French translation',
|
|
351
|
+
"Correct output:",
|
|
352
|
+
"{",
|
|
353
|
+
` "glossaryTermsToAdd": [{"sourceLocale": "${sourceLanguage}", "sourceValue": "discussion", "targetLocale": "${targetLanguage}", "targetValue": "chat"}],`,
|
|
354
|
+
' "reasoning": "User consistently changed discussion\u2192chat, indicating preferred terminology"',
|
|
355
|
+
"}",
|
|
356
|
+
"WRONG: Do not add to protected phrases (values differ) or prompts (specific mapping).",
|
|
357
|
+
"",
|
|
358
|
+
"Example 3 - Style guidance:",
|
|
359
|
+
'User feedback: "Translation is too formal, use casual tone"',
|
|
360
|
+
"Correct output:",
|
|
361
|
+
"{",
|
|
362
|
+
` "localePromptUpdates": "Use a casual, conversational tone when translating to ${targetLanguage}. Avoid overly formal language.",`,
|
|
363
|
+
' "reasoning": "This is style guidance specific to this language pair"',
|
|
364
|
+
"}",
|
|
365
|
+
"WRONG: Do not make global (applies only to this target language).",
|
|
366
|
+
"",
|
|
367
|
+
"Example 4 - Technical term in English:",
|
|
368
|
+
`User feedback: "Keep technical terms like 'API' and 'webhook' in English"`,
|
|
369
|
+
"Correct output:",
|
|
370
|
+
"{",
|
|
371
|
+
` "localePromptUpdates": "When translating to ${targetLanguage}, keep technical terms like 'API', 'webhook', and similar technical jargon in English.",`,
|
|
372
|
+
' "reasoning": "This is language-pair-specific guidance about handling technical terms"',
|
|
373
|
+
"}",
|
|
374
|
+
'WRONG: Do not add "API" or "webhook" to protected phrases (already covered by prompt guidance).',
|
|
375
|
+
"",
|
|
376
|
+
"Example 5 - Multiple issues:",
|
|
377
|
+
'User feedback: "MyApp is our product name. Also, use more concise language."',
|
|
378
|
+
"Correct output:",
|
|
379
|
+
"{",
|
|
380
|
+
' "protectedPhrasesToAdd": ["MyApp"],',
|
|
381
|
+
` "localePromptUpdates": "Use concise language when translating to ${targetLanguage}. Prefer shorter phrases over lengthy explanations.",`,
|
|
382
|
+
' "reasoning": "MyApp is a product name (protected). Conciseness is a style preference for this language pair (locale prompt)."',
|
|
383
|
+
"}",
|
|
384
|
+
"",
|
|
385
|
+
"Example 6 - Identical source and target:",
|
|
386
|
+
'Edit analysis: User kept "dashboard" as "dashboard" in Spanish',
|
|
387
|
+
"Correct output:",
|
|
388
|
+
"{",
|
|
389
|
+
' "protectedPhrasesToAdd": ["dashboard"],',
|
|
390
|
+
' "reasoning": "User kept the term unchanged, indicating it should not be translated"',
|
|
391
|
+
"}",
|
|
392
|
+
"WRONG: Do not add to glossary when source === target."
|
|
220
393
|
), parts.join(`
|
|
221
394
|
`);
|
|
222
395
|
}
|
|
223
|
-
function convertAnalysisToSuggestions(analysis) {
|
|
396
|
+
function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage) {
|
|
224
397
|
const suggestions = [];
|
|
225
398
|
if (analysis.globalPromptUpdates && suggestions.push({
|
|
226
399
|
id: crypto.randomUUID(),
|
|
@@ -247,9 +420,10 @@ function convertAnalysisToSuggestions(analysis) {
|
|
|
247
420
|
type: "glossaryTerm",
|
|
248
421
|
description: `${term.sourceValue} \u2192 ${term.targetValue}`,
|
|
249
422
|
approved: !0,
|
|
250
|
-
|
|
423
|
+
// Use the provided source/target languages instead of relying on AI to provide them
|
|
424
|
+
sourceLocale: sourceLanguage,
|
|
251
425
|
sourceValue: term.sourceValue,
|
|
252
|
-
targetLocale:
|
|
426
|
+
targetLocale: targetLanguage,
|
|
253
427
|
targetValue: term.targetValue
|
|
254
428
|
});
|
|
255
429
|
return suggestions;
|
|
@@ -296,13 +470,35 @@ function parseAIResponse(responseText) {
|
|
|
296
470
|
}
|
|
297
471
|
}
|
|
298
472
|
async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
|
|
299
|
-
devLog.info("Applying improvements from suggestions", {
|
|
473
|
+
devLog.info("Applying improvements from suggestions", {
|
|
474
|
+
count: suggestions.length,
|
|
475
|
+
targetLanguage,
|
|
476
|
+
suggestions: suggestions.map((s) => ({
|
|
477
|
+
type: s.type,
|
|
478
|
+
approved: s.approved,
|
|
479
|
+
sourceLocale: s.sourceLocale,
|
|
480
|
+
targetLocale: s.targetLocale,
|
|
481
|
+
sourceValue: s.sourceValue,
|
|
482
|
+
targetValue: s.targetValue
|
|
483
|
+
}))
|
|
484
|
+
});
|
|
300
485
|
const settingsId = "translation-settings";
|
|
301
486
|
await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
|
|
302
487
|
_id: settingsId,
|
|
303
488
|
_type: "translationSettings"
|
|
304
489
|
});
|
|
305
|
-
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()),
|
|
490
|
+
const updates = [], globalPrompts = suggestions.filter((s) => s.type === "globalPrompt" && s.description.trim()), localePrompts = suggestions.filter((s) => s.type === "localePrompt" && s.description.trim()), protectedPhrases = suggestions.filter((s) => s.type === "protectedPhrase" && s.description.trim()), glossaryTermsBeforeFilter = suggestions.filter((s) => s.type === "glossaryTerm");
|
|
491
|
+
devLog.info("Glossary terms before filtering", {
|
|
492
|
+
count: glossaryTermsBeforeFilter.length,
|
|
493
|
+
terms: glossaryTermsBeforeFilter.map((s) => ({
|
|
494
|
+
sourceLocale: s.sourceLocale,
|
|
495
|
+
sourceValue: s.sourceValue,
|
|
496
|
+
targetLocale: s.targetLocale,
|
|
497
|
+
targetValue: s.targetValue,
|
|
498
|
+
approved: s.approved
|
|
499
|
+
}))
|
|
500
|
+
});
|
|
501
|
+
const glossaryTerms = suggestions.filter(
|
|
306
502
|
(s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
|
|
307
503
|
);
|
|
308
504
|
if (devLog.info("Filtered suggestions by type", {
|
|
@@ -313,7 +509,7 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
|
|
|
313
509
|
}), (globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
|
|
314
510
|
stage: "applying",
|
|
315
511
|
message: globalPrompts.length > 0 && localePrompts.length > 0 ? "Updating global and locale translation prompts..." : globalPrompts.length > 0 ? "Updating global translation prompt..." : `Updating ${targetLanguage} translation prompt...`
|
|
316
|
-
}), updates.push(
|
|
512
|
+
}), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
|
|
317
513
|
onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
|
|
318
514
|
const phrases = protectedPhrases.map((s) => s.description);
|
|
319
515
|
devLog.debug("Applying protected phrases", { phrases }), updates.push(addProtectedPhrases(client, settingsId, phrases));
|
|
@@ -330,14 +526,17 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
|
|
|
330
526
|
}));
|
|
331
527
|
devLog.debug("Applying glossary terms", { terms }), updates.push(addGlossaryTerms(client, terms));
|
|
332
528
|
}
|
|
333
|
-
updates.length === 0 && devLog.warn("No valid updates to apply - all suggestions may have been empty"), await Promise.all(updates),
|
|
529
|
+
updates.length === 0 && devLog.warn("No valid updates to apply - all suggestions may have been empty"), await Promise.all(updates), onProgress?.({ stage: "applying", message: "Recording changes..." }), await recordChangeHistory(
|
|
530
|
+
client,
|
|
531
|
+
settingsId,
|
|
532
|
+
targetLanguage,
|
|
533
|
+
suggestions.filter((s) => s.approved)
|
|
534
|
+
), devLog.info("All improvements applied successfully");
|
|
334
535
|
}
|
|
335
|
-
async function
|
|
336
|
-
if (devLog.info("Updating prompts with
|
|
536
|
+
async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
|
|
537
|
+
if (devLog.info("Updating prompts with deterministic template", {
|
|
337
538
|
settingsId,
|
|
338
539
|
targetLanguage,
|
|
339
|
-
targetLanguageType: typeof targetLanguage,
|
|
340
|
-
targetLanguageValue: targetLanguage,
|
|
341
540
|
hasGlobalUpdates: globalPrompts.length > 0,
|
|
342
541
|
hasLocaleUpdates: localePrompts.length > 0
|
|
343
542
|
}), !targetLanguage)
|
|
@@ -346,91 +545,56 @@ async function updatePromptsWithTransform(client, schemaId, settingsId, targetLa
|
|
|
346
545
|
globalPrompts: globalPrompts.length,
|
|
347
546
|
localePrompts: localePrompts.length
|
|
348
547
|
}), new Error("targetLanguage is required but was undefined");
|
|
349
|
-
const
|
|
548
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], currentSettings = await client.fetch("*[_id == $settingsId][0]{ globalPrompt, localePrompts }", { settingsId });
|
|
350
549
|
if (globalPrompts.length > 0) {
|
|
351
|
-
const
|
|
352
|
-
|
|
550
|
+
const currentGlobalPrompt = currentSettings?.globalPrompt || "", updates = globalPrompts.map((s) => s.description.trim()).filter(Boolean), newGlobalPrompt = [
|
|
551
|
+
currentGlobalPrompt,
|
|
552
|
+
currentGlobalPrompt ? `
|
|
553
|
+
` : "",
|
|
554
|
+
`## User Feedback (${timestamp})`,
|
|
555
|
+
...updates.map((update) => `- ${update}`)
|
|
556
|
+
].filter(Boolean).join(`
|
|
353
557
|
`);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
Your task: Merge the new guidance below into the existing prompt text. Treat the new guidance as rules that will guide future translations, not as transformations to apply to the current text.
|
|
361
|
-
|
|
362
|
-
Integration guidelines:
|
|
363
|
-
- Add the new guidance as additional instructions/rules in the prompt
|
|
364
|
-
- If it conflicts with existing guidance, update or replace the conflicting parts
|
|
365
|
-
- If it's already covered, don't add redundant text
|
|
366
|
-
- Maintain clear structure and consistent tone
|
|
367
|
-
- Preserve all existing guidance that is still relevant
|
|
368
|
-
- Keep the prompt written as instructions for a translator (e.g., "When translating, you should..." not "The translation has been...")
|
|
369
|
-
|
|
370
|
-
New guidance to incorporate:
|
|
371
|
-
|
|
372
|
-
\`\`\`
|
|
373
|
-
${combinedGlobalUpdate}
|
|
374
|
-
\`\`\``
|
|
375
|
-
}), devLog.debug("Added global prompt target", { combinedUpdate: combinedGlobalUpdate });
|
|
558
|
+
await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit(), devLog.info("Global prompt updated", {
|
|
559
|
+
previousLength: currentGlobalPrompt.length,
|
|
560
|
+
newLength: newGlobalPrompt.length,
|
|
561
|
+
addedUpdates: updates.length
|
|
562
|
+
});
|
|
376
563
|
}
|
|
377
564
|
if (localePrompts.length > 0) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
- If it's already covered, don't add redundant text
|
|
410
|
-
- Maintain clear structure and consistent tone
|
|
411
|
-
- Preserve all existing guidance that is still relevant
|
|
412
|
-
- Keep the prompt written as instructions for a translator (e.g., "When translating, you should..." not "The translation has been...")
|
|
413
|
-
|
|
414
|
-
New guidance to incorporate:
|
|
415
|
-
|
|
416
|
-
\`\`\`
|
|
417
|
-
${combinedLocaleUpdate}
|
|
418
|
-
\`\`\``
|
|
419
|
-
}), devLog.debug("Added locale prompt target", {
|
|
420
|
-
targetLanguage,
|
|
421
|
-
localePromptPath,
|
|
422
|
-
combinedUpdate: combinedLocaleUpdate
|
|
423
|
-
});
|
|
565
|
+
const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
|
|
566
|
+
if (existingLocalePrompt) {
|
|
567
|
+
const currentValue = existingLocalePrompt.value || "", newValue = [
|
|
568
|
+
currentValue,
|
|
569
|
+
currentValue ? `
|
|
570
|
+
` : "",
|
|
571
|
+
`## User Feedback (${timestamp})`,
|
|
572
|
+
...updates.map((update) => `- ${update}`)
|
|
573
|
+
].filter(Boolean).join(`
|
|
574
|
+
`), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
|
|
575
|
+
await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit(), devLog.info("Locale prompt updated", {
|
|
576
|
+
targetLanguage,
|
|
577
|
+
previousLength: currentValue.length,
|
|
578
|
+
newLength: newValue.length,
|
|
579
|
+
addedUpdates: updates.length
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
|
|
583
|
+
`);
|
|
584
|
+
await client.patch(settingsId).setIfMissing({ localePrompts: [] }).append("localePrompts", [
|
|
585
|
+
{
|
|
586
|
+
_type: "internationalizedArrayTextValue",
|
|
587
|
+
_key: targetLanguage,
|
|
588
|
+
value: newValue
|
|
589
|
+
}
|
|
590
|
+
]).commit(), devLog.info("Locale prompt created", {
|
|
591
|
+
targetLanguage,
|
|
592
|
+
length: newValue.length,
|
|
593
|
+
addedUpdates: updates.length
|
|
594
|
+
});
|
|
595
|
+
}
|
|
424
596
|
}
|
|
425
|
-
|
|
426
|
-
schemaId,
|
|
427
|
-
documentId: settingsId,
|
|
428
|
-
instruction: "Update translation prompts based on user feedback to improve future translations.",
|
|
429
|
-
target: targets,
|
|
430
|
-
forcePublishedWrite: !0
|
|
431
|
-
});
|
|
432
|
-
devLog.info("Prompts updated via transform action", {
|
|
433
|
-
resultId: result._id,
|
|
597
|
+
devLog.info("Prompts updated successfully", {
|
|
434
598
|
globalUpdated: globalPrompts.length > 0,
|
|
435
599
|
localeUpdated: localePrompts.length > 0
|
|
436
600
|
});
|
|
@@ -439,8 +603,14 @@ async function addProtectedPhrases(client, settingsId, phrases) {
|
|
|
439
603
|
devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
|
|
440
604
|
const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
|
|
441
605
|
devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
|
|
442
|
-
|
|
443
|
-
|
|
606
|
+
let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
|
|
607
|
+
const conflictingPhrases = [];
|
|
608
|
+
for (const phrase of newPhrases)
|
|
609
|
+
await client.fetch(
|
|
610
|
+
'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
|
|
611
|
+
{ phrase }
|
|
612
|
+
) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
|
|
613
|
+
if (newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length === 0) {
|
|
444
614
|
devLog.info("No new protected phrases to add (all already exist)");
|
|
445
615
|
return;
|
|
446
616
|
}
|
|
@@ -448,7 +618,13 @@ async function addProtectedPhrases(client, settingsId, phrases) {
|
|
|
448
618
|
devLog.info("Protected phrases added", { count: newPhrases.length, phrases: newPhrases, resultId: result._id });
|
|
449
619
|
}
|
|
450
620
|
async function addGlossaryTerms(client, terms) {
|
|
451
|
-
const validTerms = terms.filter(({ sourceValue, targetValue }) => sourceValue === targetValue ? (devLog.warn("Skipping glossary term where source === target (should be a protected phrase)", {
|
|
621
|
+
const protectedPhrasesLower = (await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.map((p) => p.toLowerCase()) || [], validTerms = terms.filter(({ sourceValue, targetValue }) => sourceValue === targetValue ? (devLog.warn("Skipping glossary term where source === target (should be a protected phrase)", {
|
|
622
|
+
sourceValue,
|
|
623
|
+
targetValue
|
|
624
|
+
}), !1) : protectedPhrasesLower.includes(sourceValue.toLowerCase()) ? (devLog.warn("Skipping glossary term because source value is a protected phrase", {
|
|
625
|
+
sourceValue,
|
|
626
|
+
targetValue
|
|
627
|
+
}), !1) : protectedPhrasesLower.includes(targetValue.toLowerCase()) ? (devLog.warn("Skipping glossary term because target value is a protected phrase", {
|
|
452
628
|
sourceValue,
|
|
453
629
|
targetValue
|
|
454
630
|
}), !1) : !0);
|
|
@@ -465,32 +641,47 @@ async function addGlossaryTerms(client, terms) {
|
|
|
465
641
|
}`,
|
|
466
642
|
{ sourceLocale, sourceValue }
|
|
467
643
|
);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
targetLocale,
|
|
472
|
-
targetValue
|
|
473
|
-
}) : (await client.patch(existingTerm._id).append("translations", [
|
|
474
|
-
{
|
|
475
|
-
_type: "internationalizedArrayStringValue",
|
|
476
|
-
_key: targetLocale,
|
|
477
|
-
value: targetValue
|
|
478
|
-
}
|
|
479
|
-
]).commit(), devLog.info("Updated existing glossary term", { sourceLocale, sourceValue, targetLocale, targetValue })) : (await client.create({
|
|
480
|
-
_type: "translationGlossaryTerm",
|
|
481
|
-
translations: [
|
|
482
|
-
{
|
|
483
|
-
_type: "internationalizedArrayStringValue",
|
|
484
|
-
_key: sourceLocale,
|
|
485
|
-
value: sourceValue
|
|
486
|
-
},
|
|
644
|
+
if (existingTerm) {
|
|
645
|
+
const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
|
|
646
|
+
existingTargetIndex === -1 ? (await client.patch(existingTerm._id).append("translations", [
|
|
487
647
|
{
|
|
488
648
|
_type: "internationalizedArrayStringValue",
|
|
489
649
|
_key: targetLocale,
|
|
490
650
|
value: targetValue
|
|
491
651
|
}
|
|
492
|
-
]
|
|
493
|
-
|
|
652
|
+
]).commit(), devLog.info("Added target language to existing glossary term", {
|
|
653
|
+
sourceLocale,
|
|
654
|
+
sourceValue,
|
|
655
|
+
targetLocale,
|
|
656
|
+
targetValue
|
|
657
|
+
})) : existingTargetValue !== targetValue ? (await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit(), devLog.info("Updated existing glossary term translation", {
|
|
658
|
+
sourceLocale,
|
|
659
|
+
sourceValue,
|
|
660
|
+
targetLocale,
|
|
661
|
+
oldValue: existingTargetValue,
|
|
662
|
+
newValue: targetValue
|
|
663
|
+
})) : devLog.info("Glossary term already has this exact translation", {
|
|
664
|
+
sourceLocale,
|
|
665
|
+
sourceValue,
|
|
666
|
+
targetLocale,
|
|
667
|
+
targetValue
|
|
668
|
+
});
|
|
669
|
+
} else
|
|
670
|
+
await client.create({
|
|
671
|
+
_type: "translationGlossaryTerm",
|
|
672
|
+
translations: [
|
|
673
|
+
{
|
|
674
|
+
_type: "internationalizedArrayStringValue",
|
|
675
|
+
_key: sourceLocale,
|
|
676
|
+
value: sourceValue
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
_type: "internationalizedArrayStringValue",
|
|
680
|
+
_key: targetLocale,
|
|
681
|
+
value: targetValue
|
|
682
|
+
}
|
|
683
|
+
]
|
|
684
|
+
}), devLog.info("Created new glossary term", { sourceLocale, sourceValue, targetLocale, targetValue });
|
|
494
685
|
}
|
|
495
686
|
devLog.info("Glossary terms processed", {
|
|
496
687
|
totalRequested: terms.length,
|
|
@@ -498,6 +689,20 @@ async function addGlossaryTerms(client, terms) {
|
|
|
498
689
|
skipped: terms.length - validTerms.length
|
|
499
690
|
});
|
|
500
691
|
}
|
|
692
|
+
async function recordChangeHistory(client, settingsId, targetLanguage, suggestions) {
|
|
693
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString(), historyEntries = suggestions.map((suggestion) => ({
|
|
694
|
+
_type: "object",
|
|
695
|
+
_key: crypto.randomUUID(),
|
|
696
|
+
timestamp,
|
|
697
|
+
changeType: suggestion.type,
|
|
698
|
+
description: suggestion.description,
|
|
699
|
+
locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
|
|
700
|
+
}));
|
|
701
|
+
historyEntries.length !== 0 && (await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit(), devLog.info("Change history recorded", {
|
|
702
|
+
count: historyEntries.length,
|
|
703
|
+
timestamp
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
501
706
|
const ICON_SIZE = 64, progressIconMap = {
|
|
502
707
|
in_progress: /* @__PURE__ */ jsx(Spinner, {}),
|
|
503
708
|
completed: /* @__PURE__ */ jsx(CheckmarkCircleIcon, { fontSize: ICON_SIZE }),
|
|
@@ -520,7 +725,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
520
725
|
localePrompt: "Locale Prompt",
|
|
521
726
|
protectedPhrase: "Protected Phrase",
|
|
522
727
|
glossaryTerm: "Glossary Term"
|
|
523
|
-
}, createNewSuggestion = (type = "protectedPhrase") => {
|
|
728
|
+
}, createNewSuggestion = (type = "protectedPhrase", sourceLanguage, targetLanguage) => {
|
|
524
729
|
const baseSuggestion = {
|
|
525
730
|
id: crypto.randomUUID(),
|
|
526
731
|
type,
|
|
@@ -530,11 +735,18 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
530
735
|
return type === "glossaryTerm" ? {
|
|
531
736
|
...baseSuggestion,
|
|
532
737
|
sourceValue: "",
|
|
533
|
-
targetValue: ""
|
|
738
|
+
targetValue: "",
|
|
739
|
+
sourceLocale: sourceLanguage,
|
|
740
|
+
targetLocale: targetLanguage
|
|
534
741
|
} : baseSuggestion;
|
|
535
|
-
}, SuggestionReviewList = ({
|
|
742
|
+
}, SuggestionReviewList = ({
|
|
743
|
+
suggestions,
|
|
744
|
+
onSuggestionsChange,
|
|
745
|
+
sourceLanguage,
|
|
746
|
+
targetLanguage
|
|
747
|
+
}) => {
|
|
536
748
|
const [editingId, setEditingId] = useState(null), handleToggleApproval = (id) => {
|
|
537
|
-
const updated = suggestions.map((s) => s.id === id ? { ...s, approved: !s.approved } : s);
|
|
749
|
+
const updated = suggestions.map((s) => s.id === id && s.validationStatus !== "invalid" ? { ...s, approved: !s.approved } : s);
|
|
538
750
|
onSuggestionsChange(updated);
|
|
539
751
|
}, handleTypeChange = (id, newType) => {
|
|
540
752
|
const updated = suggestions.map((s) => {
|
|
@@ -546,7 +758,9 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
546
758
|
...s,
|
|
547
759
|
type: newType,
|
|
548
760
|
sourceValue: match ? match[1].trim() : "",
|
|
549
|
-
targetValue: match ? match[2].trim() : ""
|
|
761
|
+
targetValue: match ? match[2].trim() : "",
|
|
762
|
+
sourceLocale: sourceLanguage,
|
|
763
|
+
targetLocale: targetLanguage
|
|
550
764
|
};
|
|
551
765
|
}
|
|
552
766
|
return newType !== "glossaryTerm" && s.type === "glossaryTerm" ? {
|
|
@@ -567,6 +781,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
567
781
|
...s,
|
|
568
782
|
sourceValue: newSource,
|
|
569
783
|
description: `${newSource} \u2192 ${s.targetValue || ""}`
|
|
784
|
+
// Preserve sourceLocale and targetLocale
|
|
570
785
|
} : s
|
|
571
786
|
);
|
|
572
787
|
onSuggestionsChange(updated);
|
|
@@ -576,6 +791,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
576
791
|
...s,
|
|
577
792
|
targetValue: newTarget,
|
|
578
793
|
description: `${s.sourceValue || ""} \u2192 ${newTarget}`
|
|
794
|
+
// Preserve sourceLocale and targetLocale
|
|
579
795
|
} : s
|
|
580
796
|
);
|
|
581
797
|
onSuggestionsChange(updated);
|
|
@@ -583,7 +799,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
583
799
|
const updated = suggestions.filter((s) => s.id !== id);
|
|
584
800
|
onSuggestionsChange(updated);
|
|
585
801
|
}, handleAddSuggestion = (type) => {
|
|
586
|
-
const newSuggestion = createNewSuggestion(type);
|
|
802
|
+
const newSuggestion = createNewSuggestion(type, sourceLanguage, targetLanguage);
|
|
587
803
|
onSuggestionsChange([...suggestions, newSuggestion]), setEditingId(newSuggestion.id);
|
|
588
804
|
};
|
|
589
805
|
return suggestions.length === 0 ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
@@ -610,16 +826,24 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
610
826
|
) })
|
|
611
827
|
] }) : /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
612
828
|
suggestions.map((suggestion) => {
|
|
613
|
-
const isEditing = editingId === suggestion.id, isGlossaryTerm = suggestion.type === "glossaryTerm", isPrompt = suggestion.type === "globalPrompt" || suggestion.type === "localePrompt";
|
|
829
|
+
const isEditing = editingId === suggestion.id, isGlossaryTerm = suggestion.type === "glossaryTerm", isPrompt = suggestion.type === "globalPrompt" || suggestion.type === "localePrompt", hasWarning = suggestion.warning && suggestion.validationStatus !== "valid", isInvalid = suggestion.validationStatus === "invalid";
|
|
614
830
|
return /* @__PURE__ */ jsx(
|
|
615
831
|
Card,
|
|
616
832
|
{
|
|
617
833
|
padding: 4,
|
|
618
834
|
border: !0,
|
|
619
|
-
tone: suggestion.approved ? "default" : "transparent",
|
|
835
|
+
tone: isInvalid ? "critical" : hasWarning ? "caution" : suggestion.approved ? "default" : "transparent",
|
|
620
836
|
style: { opacity: suggestion.approved ? 1 : 0.5 },
|
|
621
837
|
children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
|
|
622
|
-
/* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(
|
|
838
|
+
/* @__PURE__ */ jsx(Box, { paddingTop: 1, children: isInvalid ? /* @__PURE__ */ jsx(
|
|
839
|
+
Tooltip,
|
|
840
|
+
{
|
|
841
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This suggestion cannot be applied because it conflicts with existing settings. See the error message below for details." }) }),
|
|
842
|
+
placement: "top",
|
|
843
|
+
portal: !0,
|
|
844
|
+
children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Checkbox, { checked: !1, disabled: !0, readOnly: !0 }) })
|
|
845
|
+
}
|
|
846
|
+
) : /* @__PURE__ */ jsx(Checkbox, { checked: suggestion.approved, onChange: () => handleToggleApproval(suggestion.id) }) }),
|
|
623
847
|
/* @__PURE__ */ jsxs(Stack, { space: 3, flex: 1, children: [
|
|
624
848
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", wrap: "wrap", children: [
|
|
625
849
|
/* @__PURE__ */ jsx(Box, { flex: 1, style: { minWidth: "200px" }, children: /* @__PURE__ */ jsx(
|
|
@@ -628,6 +852,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
628
852
|
fontSize: 1,
|
|
629
853
|
value: suggestion.type,
|
|
630
854
|
onChange: (event) => handleTypeChange(suggestion.id, event.currentTarget.value),
|
|
855
|
+
"aria-label": "Suggestion type",
|
|
631
856
|
children: Object.entries(IMPROVEMENT_TYPE_LABELS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
|
|
632
857
|
}
|
|
633
858
|
) }),
|
|
@@ -639,31 +864,42 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
639
864
|
icon: TrashIcon,
|
|
640
865
|
fontSize: 1,
|
|
641
866
|
padding: 2,
|
|
642
|
-
onClick: () => handleRemove(suggestion.id)
|
|
867
|
+
onClick: () => handleRemove(suggestion.id),
|
|
868
|
+
"aria-label": "Remove suggestion",
|
|
869
|
+
title: "Remove suggestion"
|
|
643
870
|
}
|
|
644
871
|
)
|
|
645
872
|
] }),
|
|
646
|
-
isGlossaryTerm ? /* @__PURE__ */
|
|
647
|
-
/* @__PURE__ */
|
|
648
|
-
|
|
649
|
-
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
873
|
+
isGlossaryTerm ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
874
|
+
(suggestion.sourceLocale || suggestion.targetLocale) && /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
875
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.sourceLocale ? `Source (${suggestion.sourceLocale})` : "Source" }) }),
|
|
876
|
+
/* @__PURE__ */ jsx(Box, { style: { width: "20px" } }),
|
|
877
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.targetLocale ? `Target (${suggestion.targetLocale})` : "Target" }) })
|
|
878
|
+
] }),
|
|
879
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
880
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
|
|
881
|
+
TextInput,
|
|
882
|
+
{
|
|
883
|
+
fontSize: 1,
|
|
884
|
+
placeholder: "Source term",
|
|
885
|
+
value: suggestion.sourceValue || "",
|
|
886
|
+
onChange: (event) => handleGlossarySourceChange(suggestion.id, event.currentTarget.value),
|
|
887
|
+
"aria-label": `Source term in ${suggestion.sourceLocale || "source language"}`
|
|
888
|
+
}
|
|
889
|
+
) }),
|
|
890
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, "aria-hidden": "true", children: "\u2192" }),
|
|
891
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
|
|
892
|
+
TextInput,
|
|
893
|
+
{
|
|
894
|
+
fontSize: 1,
|
|
895
|
+
placeholder: "Target term",
|
|
896
|
+
value: suggestion.targetValue || "",
|
|
897
|
+
onChange: (event) => handleGlossaryTargetChange(suggestion.id, event.currentTarget.value),
|
|
898
|
+
"aria-label": `Target term in ${suggestion.targetLocale || "target language"}`
|
|
899
|
+
}
|
|
900
|
+
) })
|
|
901
|
+
] })
|
|
902
|
+
] }) : isPrompt ? /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(
|
|
667
903
|
TextArea,
|
|
668
904
|
{
|
|
669
905
|
fontSize: 1,
|
|
@@ -672,7 +908,8 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
672
908
|
onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
|
|
673
909
|
onFocus: () => setEditingId(suggestion.id),
|
|
674
910
|
onBlur: () => setEditingId(null),
|
|
675
|
-
placeholder: "Enter prompt guidance..."
|
|
911
|
+
placeholder: "Enter prompt guidance...",
|
|
912
|
+
"aria-label": `${IMPROVEMENT_TYPE_LABELS[suggestion.type]} content`
|
|
676
913
|
}
|
|
677
914
|
) }) : /* @__PURE__ */ jsx(
|
|
678
915
|
TextInput,
|
|
@@ -680,9 +917,17 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
680
917
|
fontSize: 1,
|
|
681
918
|
value: suggestion.description,
|
|
682
919
|
onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
|
|
683
|
-
placeholder: "Enter protected phrase..."
|
|
920
|
+
placeholder: "Enter protected phrase...",
|
|
921
|
+
"aria-label": "Protected phrase"
|
|
684
922
|
}
|
|
685
|
-
)
|
|
923
|
+
),
|
|
924
|
+
hasWarning && /* @__PURE__ */ jsx(Card, { padding: 3, tone: isInvalid ? "critical" : "caution", border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 2, children: [
|
|
925
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: isInvalid ? /* @__PURE__ */ jsx(WarningOutlineIcon, {}) : /* @__PURE__ */ jsx(InfoOutlineIcon, {}) }),
|
|
926
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
|
|
927
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: isInvalid ? "Invalid Suggestion" : "Warning" }),
|
|
928
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: suggestion.warning })
|
|
929
|
+
] })
|
|
930
|
+
] }) })
|
|
686
931
|
] })
|
|
687
932
|
] }) })
|
|
688
933
|
},
|
|
@@ -704,7 +949,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
704
949
|
}, GiveFeedbackDialog = ({ setIsDialogOpen }) => {
|
|
705
950
|
const client = useClient({ apiVersion: "vX" }), { document, config, translationMetadata } = useTranslationContext(), { loading } = useTranslationMetadata({
|
|
706
951
|
documentId: document._id
|
|
707
|
-
}), [feedback, setFeedback] = useState(""), [shouldIncludeEdits, setShouldIncludeEdits] = useState(!1), [processingState, setProcessingState] = useState("idle"), [suggestions, setSuggestions] = useState([]), [changeSummary, setChangeSummary] = useState(""), [error, setError] = useState(""), handleShouldIncludeEditsChange = () => {
|
|
952
|
+
}), [feedback, setFeedback] = useState(""), [shouldIncludeEdits, setShouldIncludeEdits] = useState(!1), [processingState, setProcessingState] = useState("idle"), [suggestions, setSuggestions] = useState([]), [changeSummary, setChangeSummary] = useState(""), [error, setError] = useState(""), [sourceLanguage, setSourceLanguage] = useState(""), [targetLanguage, setTargetLanguage] = useState(""), handleShouldIncludeEditsChange = () => {
|
|
708
953
|
setShouldIncludeEdits(!shouldIncludeEdits);
|
|
709
954
|
}, handleCancel = () => {
|
|
710
955
|
setIsDialogOpen(!1);
|
|
@@ -726,14 +971,16 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
726
971
|
throw new Error(
|
|
727
972
|
"No translation history found for this document. Please ensure this document was translated using the AI Translation plugin."
|
|
728
973
|
);
|
|
729
|
-
const { sourceSnapshot, targetSnapshot, sourceLanguage } = translationHistory2;
|
|
974
|
+
const { sourceSnapshot, targetSnapshot, sourceLanguage: srcLang } = translationHistory2;
|
|
730
975
|
if (!sourceSnapshot || !targetSnapshot)
|
|
731
976
|
throw new Error("Translation snapshots are missing from history.");
|
|
977
|
+
const srcLanguage = srcLang || "en-us", tgtLanguage = document[config.languageFieldName];
|
|
978
|
+
setSourceLanguage(srcLanguage), setTargetLanguage(tgtLanguage);
|
|
732
979
|
const generatedSuggestions = await analyzeFeedback({
|
|
733
980
|
client,
|
|
734
981
|
schemaId: config.schemaId,
|
|
735
|
-
sourceLanguage:
|
|
736
|
-
targetLanguage:
|
|
982
|
+
sourceLanguage: srcLanguage,
|
|
983
|
+
targetLanguage: tgtLanguage,
|
|
737
984
|
userFeedback: feedback || void 0,
|
|
738
985
|
shouldIncludeEdits,
|
|
739
986
|
sourceSnapshot,
|
|
@@ -787,7 +1034,15 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
787
1034
|
/* @__PURE__ */ jsx(Heading, { children: "Review Suggestions" }),
|
|
788
1035
|
/* @__PURE__ */ jsx(Text, { muted: !0, children: "The AI has analyzed your feedback and generated the following suggestions. Review and edit them before applying." })
|
|
789
1036
|
] }),
|
|
790
|
-
/* @__PURE__ */ jsx(
|
|
1037
|
+
/* @__PURE__ */ jsx(
|
|
1038
|
+
SuggestionReviewList,
|
|
1039
|
+
{
|
|
1040
|
+
suggestions,
|
|
1041
|
+
onSuggestionsChange: setSuggestions,
|
|
1042
|
+
sourceLanguage,
|
|
1043
|
+
targetLanguage
|
|
1044
|
+
}
|
|
1045
|
+
),
|
|
791
1046
|
error && /* @__PURE__ */ jsx(Card, { padding: 3, tone: "critical", border: !0, children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
|
|
792
1047
|
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, paddingTop: 2, children: [
|
|
793
1048
|
/* @__PURE__ */ jsx(Flex, { gap: 2, children: /* @__PURE__ */ jsx(
|
|
@@ -895,6 +1150,33 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
895
1150
|
)
|
|
896
1151
|
] })
|
|
897
1152
|
] });
|
|
1153
|
+
}, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
|
|
1154
|
+
try {
|
|
1155
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
1156
|
+
method: "GET",
|
|
1157
|
+
headers: {
|
|
1158
|
+
Authorization: `Bearer ${apiKey}`
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
if (!response.ok)
|
|
1162
|
+
return {
|
|
1163
|
+
valid: !1,
|
|
1164
|
+
error: (await response.json()).error?.message || "Invalid API key"
|
|
1165
|
+
};
|
|
1166
|
+
const availableModels = (await response.json()).data.map((m) => m.id);
|
|
1167
|
+
return availableModels.includes(model) ? {
|
|
1168
|
+
valid: !0,
|
|
1169
|
+
modelAvailable: availableModels
|
|
1170
|
+
} : {
|
|
1171
|
+
valid: !1,
|
|
1172
|
+
error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
|
|
1173
|
+
};
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
return {
|
|
1176
|
+
valid: !1,
|
|
1177
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
898
1180
|
}, translationReducer = (context, action) => {
|
|
899
1181
|
const logContext = {
|
|
900
1182
|
documentId: context.document?._id,
|
|
@@ -914,10 +1196,15 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
914
1196
|
...context,
|
|
915
1197
|
selectedTargetLanguages: action.payload
|
|
916
1198
|
};
|
|
917
|
-
case "
|
|
1199
|
+
case "SET_TRANSLATION_MODE":
|
|
1200
|
+
return {
|
|
1201
|
+
...context,
|
|
1202
|
+
translationMode: action.payload
|
|
1203
|
+
};
|
|
1204
|
+
case "SET_OPENAI_VALIDATED":
|
|
918
1205
|
return {
|
|
919
1206
|
...context,
|
|
920
|
-
|
|
1207
|
+
openaiValidated: action.payload
|
|
921
1208
|
};
|
|
922
1209
|
case "SET_SHOULD_TRANSLATE_REFERENCES":
|
|
923
1210
|
return {
|
|
@@ -962,18 +1249,44 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
962
1249
|
overallStatus: "idle",
|
|
963
1250
|
translationMetadata: null,
|
|
964
1251
|
selectedTargetLanguages: [],
|
|
965
|
-
shouldTranslateDiffs: !1,
|
|
966
1252
|
shouldOverwriteFreshTranslations: !1,
|
|
967
1253
|
shouldTranslateReferences: !1,
|
|
968
1254
|
maxReferenceDepth: 1,
|
|
969
1255
|
references: [],
|
|
970
|
-
translationJobs: /* @__PURE__ */ new Map()
|
|
1256
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
1257
|
+
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
1258
|
+
openaiValidated: !1
|
|
971
1259
|
}, [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
972
|
-
return
|
|
1260
|
+
return useEffect(() => {
|
|
1261
|
+
(async () => {
|
|
1262
|
+
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
1263
|
+
try {
|
|
1264
|
+
const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
|
|
1265
|
+
dispatch({
|
|
1266
|
+
type: "SET_OPENAI_VALIDATED",
|
|
1267
|
+
payload: validation.valid
|
|
1268
|
+
}), validation.valid ? devLog.info("OpenAI API key validated successfully", {
|
|
1269
|
+
model: config.openai.model || "gpt-5",
|
|
1270
|
+
component: "TranslationContextProvider"
|
|
1271
|
+
}) : devLog.error("OpenAI API key validation failed", {
|
|
1272
|
+
error: validation.error,
|
|
1273
|
+
component: "TranslationContextProvider"
|
|
1274
|
+
});
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
|
|
1277
|
+
component: "TranslationContextProvider"
|
|
1278
|
+
}), dispatch({
|
|
1279
|
+
type: "SET_OPENAI_VALIDATED",
|
|
1280
|
+
payload: !1
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
})();
|
|
1284
|
+
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
|
|
973
1285
|
documentId: document._id,
|
|
974
1286
|
overallStatus: context.overallStatus,
|
|
975
1287
|
selectedLanguages: context.selectedTargetLanguages,
|
|
976
|
-
|
|
1288
|
+
translationMode: context.translationMode,
|
|
1289
|
+
openaiEnabled: config?.openai?.enabled,
|
|
977
1290
|
component: "TranslationContextProvider"
|
|
978
1291
|
}), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
|
|
979
1292
|
}, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
|
|
@@ -1076,26 +1389,98 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1076
1389
|
title: "Translation Settings",
|
|
1077
1390
|
icon: TranslateIcon,
|
|
1078
1391
|
type: "document",
|
|
1392
|
+
groups: [
|
|
1393
|
+
{
|
|
1394
|
+
name: "general",
|
|
1395
|
+
title: "General",
|
|
1396
|
+
default: !0
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: "history",
|
|
1400
|
+
title: "History"
|
|
1401
|
+
}
|
|
1402
|
+
],
|
|
1079
1403
|
fields: [
|
|
1080
1404
|
defineField({
|
|
1081
1405
|
name: "globalPrompt",
|
|
1082
1406
|
title: "Global Translation Prompt",
|
|
1083
1407
|
description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
|
|
1084
1408
|
type: "text",
|
|
1085
|
-
rows: 5
|
|
1409
|
+
rows: 5,
|
|
1410
|
+
group: "general"
|
|
1086
1411
|
}),
|
|
1087
1412
|
defineField({
|
|
1088
1413
|
name: "localePrompts",
|
|
1089
1414
|
title: "Locale Prompts",
|
|
1090
1415
|
description: "Prompts that will be given to the Sanity AI Agent for each locale.",
|
|
1091
|
-
type: "internationalizedArrayText"
|
|
1416
|
+
type: "internationalizedArrayText",
|
|
1417
|
+
group: "general"
|
|
1092
1418
|
}),
|
|
1093
1419
|
defineField({
|
|
1094
1420
|
name: "protectedPhrases",
|
|
1095
1421
|
title: "Protected Phrases",
|
|
1096
1422
|
description: "Phrases that will never be translated.",
|
|
1097
1423
|
type: "array",
|
|
1098
|
-
of: [{ type: "string" }]
|
|
1424
|
+
of: [{ type: "string" }],
|
|
1425
|
+
group: "general"
|
|
1426
|
+
}),
|
|
1427
|
+
defineField({
|
|
1428
|
+
name: "changeHistory",
|
|
1429
|
+
title: "Change History",
|
|
1430
|
+
description: "History of changes made via user feedback.",
|
|
1431
|
+
type: "array",
|
|
1432
|
+
readOnly: !0,
|
|
1433
|
+
group: "history",
|
|
1434
|
+
of: [
|
|
1435
|
+
{
|
|
1436
|
+
type: "object",
|
|
1437
|
+
fields: [
|
|
1438
|
+
defineField({
|
|
1439
|
+
name: "timestamp",
|
|
1440
|
+
title: "Timestamp",
|
|
1441
|
+
type: "datetime"
|
|
1442
|
+
}),
|
|
1443
|
+
defineField({
|
|
1444
|
+
name: "changeType",
|
|
1445
|
+
title: "Change Type",
|
|
1446
|
+
type: "string",
|
|
1447
|
+
options: {
|
|
1448
|
+
list: [
|
|
1449
|
+
{ title: "Global Prompt", value: "globalPrompt" },
|
|
1450
|
+
{ title: "Locale Prompt", value: "localePrompt" },
|
|
1451
|
+
{ title: "Protected Phrase", value: "protectedPhrase" },
|
|
1452
|
+
{ title: "Glossary Term", value: "glossaryTerm" }
|
|
1453
|
+
]
|
|
1454
|
+
}
|
|
1455
|
+
}),
|
|
1456
|
+
defineField({
|
|
1457
|
+
name: "description",
|
|
1458
|
+
title: "Description",
|
|
1459
|
+
type: "text",
|
|
1460
|
+
rows: 3
|
|
1461
|
+
}),
|
|
1462
|
+
defineField({
|
|
1463
|
+
name: "locale",
|
|
1464
|
+
title: "Locale",
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "Target locale for locale-specific changes"
|
|
1467
|
+
})
|
|
1468
|
+
],
|
|
1469
|
+
preview: {
|
|
1470
|
+
select: {
|
|
1471
|
+
timestamp: "timestamp",
|
|
1472
|
+
changeType: "changeType",
|
|
1473
|
+
description: "description"
|
|
1474
|
+
},
|
|
1475
|
+
prepare({ timestamp, changeType, description }) {
|
|
1476
|
+
return {
|
|
1477
|
+
title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
|
|
1478
|
+
subtitle: description
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
]
|
|
1099
1484
|
})
|
|
1100
1485
|
],
|
|
1101
1486
|
preview: {
|
|
@@ -1103,13 +1488,180 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1103
1488
|
title: "Translation Settings"
|
|
1104
1489
|
})
|
|
1105
1490
|
}
|
|
1106
|
-
})
|
|
1491
|
+
});
|
|
1492
|
+
class Semaphore {
|
|
1493
|
+
permits;
|
|
1494
|
+
queue = [];
|
|
1495
|
+
constructor(permits) {
|
|
1496
|
+
this.permits = permits;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Acquire a permit from the semaphore
|
|
1500
|
+
* If no permits are available, the caller will wait until one becomes available
|
|
1501
|
+
*/
|
|
1502
|
+
async acquire() {
|
|
1503
|
+
return this.permits > 0 ? (this.permits--, Promise.resolve()) : new Promise((resolve) => {
|
|
1504
|
+
this.queue.push(resolve);
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Release a permit back to the semaphore
|
|
1509
|
+
* If there are waiting callers, one will be woken up
|
|
1510
|
+
*/
|
|
1511
|
+
release() {
|
|
1512
|
+
const next = this.queue.shift();
|
|
1513
|
+
next ? next() : this.permits++;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Execute a function with semaphore protection
|
|
1517
|
+
* Automatically acquires and releases the permit
|
|
1518
|
+
*/
|
|
1519
|
+
async withPermit(fn) {
|
|
1520
|
+
await this.acquire();
|
|
1521
|
+
try {
|
|
1522
|
+
return await fn();
|
|
1523
|
+
} finally {
|
|
1524
|
+
this.release();
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Get the number of available permits
|
|
1529
|
+
*/
|
|
1530
|
+
availablePermits() {
|
|
1531
|
+
return this.permits;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Get the number of waiting callers
|
|
1535
|
+
*/
|
|
1536
|
+
queueLength() {
|
|
1537
|
+
return this.queue.length;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Update the number of permits
|
|
1541
|
+
* If increasing permits, waiting callers will be notified
|
|
1542
|
+
* If decreasing permits below current available, available permits will be reduced
|
|
1543
|
+
*/
|
|
1544
|
+
updatePermits(newPermits) {
|
|
1545
|
+
const currentTotal = this.permits + this.queue.length, difference = newPermits - currentTotal;
|
|
1546
|
+
if (difference > 0) {
|
|
1547
|
+
for (let i = 0; i < difference && this.queue.length > 0; i++) {
|
|
1548
|
+
const next = this.queue.shift();
|
|
1549
|
+
next && next();
|
|
1550
|
+
}
|
|
1551
|
+
const remainingIncrease = difference - (currentTotal - this.permits - this.queue.length);
|
|
1552
|
+
remainingIncrease > 0 && (this.permits += remainingIncrease);
|
|
1553
|
+
} else difference < 0 && (this.permits = Math.max(0, this.permits + difference));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function documentToPlainText(document) {
|
|
1557
|
+
if (!document) return "";
|
|
1558
|
+
const result = {};
|
|
1559
|
+
for (const [key, value] of Object.entries(document))
|
|
1560
|
+
Array.isArray(value) && value.some(
|
|
1561
|
+
(item) => typeof item == "object" && item !== null && "_type" in item && item._type === "block"
|
|
1562
|
+
) ? result[key] = toPlainText(value) : result[key] = value;
|
|
1563
|
+
return JSON.stringify(result);
|
|
1564
|
+
}
|
|
1565
|
+
const DEFAULTS = {
|
|
1566
|
+
windowSlack: 2,
|
|
1567
|
+
minTokenMatchRatio: 1,
|
|
1568
|
+
stopwords: [
|
|
1569
|
+
// common English fillers — extend per your locales
|
|
1570
|
+
"a",
|
|
1571
|
+
"an",
|
|
1572
|
+
"the",
|
|
1573
|
+
"and",
|
|
1574
|
+
"or",
|
|
1575
|
+
"to",
|
|
1576
|
+
"of",
|
|
1577
|
+
"for",
|
|
1578
|
+
"in",
|
|
1579
|
+
"on",
|
|
1580
|
+
"at",
|
|
1581
|
+
"by",
|
|
1582
|
+
"with",
|
|
1583
|
+
"using",
|
|
1584
|
+
"from",
|
|
1585
|
+
"into",
|
|
1586
|
+
"over",
|
|
1587
|
+
"under",
|
|
1588
|
+
"as",
|
|
1589
|
+
"is",
|
|
1590
|
+
"are",
|
|
1591
|
+
"be"
|
|
1592
|
+
],
|
|
1593
|
+
dropVeryShortTermTokens: !0,
|
|
1594
|
+
allowedWordCharsRegex: /[\p{L}\p{N}_]+/gu
|
|
1595
|
+
// letters/numbers/underscore (Unicode)
|
|
1596
|
+
};
|
|
1597
|
+
function normalizeForMatch(s) {
|
|
1598
|
+
return s.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}+", "gu"), "").trim();
|
|
1599
|
+
}
|
|
1600
|
+
function tokenize(text, allowedWordChars = DEFAULTS.allowedWordCharsRegex) {
|
|
1601
|
+
return normalizeForMatch(text).match(allowedWordChars) || [];
|
|
1602
|
+
}
|
|
1603
|
+
function preprocessSource(sourceText, opts) {
|
|
1604
|
+
return { tokens: tokenize(sourceText, DEFAULTS.allowedWordCharsRegex) };
|
|
1605
|
+
}
|
|
1606
|
+
function prepareTermTokens(term, opts) {
|
|
1607
|
+
const filtered = tokenize(term, opts.allowedWordCharsRegex).filter((t) => !opts.stopwords.includes(t));
|
|
1608
|
+
return opts.dropVeryShortTermTokens ? filtered.filter((t) => t.length > 2) : filtered;
|
|
1609
|
+
}
|
|
1610
|
+
function freqMap(tokens) {
|
|
1611
|
+
const m = /* @__PURE__ */ new Map();
|
|
1612
|
+
for (const t of tokens) m.set(t, (m.get(t) ?? 0) + 1);
|
|
1613
|
+
return m;
|
|
1614
|
+
}
|
|
1615
|
+
function matchTermInDocument(term, source, options) {
|
|
1616
|
+
const opts = { ...DEFAULTS, ...options ?? {} }, termTokens = prepareTermTokens(term, opts);
|
|
1617
|
+
if (termTokens.length === 0)
|
|
1618
|
+
return { matched: !1, term, reason: "No substantive tokens after filtering." };
|
|
1619
|
+
const termFreq = freqMap(termTokens), requiredCount = Math.ceil(termTokens.length * opts.minTokenMatchRatio), windowSize = Math.max(1, termTokens.length + opts.windowSlack), interesting = new Set(termFreq.keys()), windowCounts = /* @__PURE__ */ new Map();
|
|
1620
|
+
let satisfiedCount = 0;
|
|
1621
|
+
const addToken = (tok) => {
|
|
1622
|
+
if (!interesting.has(tok)) return;
|
|
1623
|
+
const before = windowCounts.get(tok) ?? 0, after = before + 1;
|
|
1624
|
+
windowCounts.set(tok, after);
|
|
1625
|
+
const cap = termFreq.get(tok);
|
|
1626
|
+
before < cap && (satisfiedCount += 1);
|
|
1627
|
+
}, removeToken = (tok) => {
|
|
1628
|
+
if (!interesting.has(tok)) return;
|
|
1629
|
+
const before = windowCounts.get(tok) ?? 0;
|
|
1630
|
+
if (before === 0) return;
|
|
1631
|
+
const after = before - 1;
|
|
1632
|
+
windowCounts.set(tok, after);
|
|
1633
|
+
const cap = termFreq.get(tok);
|
|
1634
|
+
before <= cap && (satisfiedCount -= 1);
|
|
1635
|
+
}, { tokens } = source;
|
|
1636
|
+
for (let i = 0; i < Math.min(windowSize, tokens.length); i++) addToken(tokens[i]);
|
|
1637
|
+
if (satisfiedCount >= requiredCount)
|
|
1638
|
+
return {
|
|
1639
|
+
matched: !0,
|
|
1640
|
+
term,
|
|
1641
|
+
windowStart: 0,
|
|
1642
|
+
windowEnd: Math.min(windowSize, tokens.length) - 1,
|
|
1643
|
+
matchedRatio: satisfiedCount / termTokens.length
|
|
1644
|
+
};
|
|
1645
|
+
for (let start = 1; start + windowSize <= tokens.length; start++) {
|
|
1646
|
+
const prev = tokens[start - 1], next = tokens[start + windowSize - 1];
|
|
1647
|
+
if (removeToken(prev), addToken(next), satisfiedCount >= requiredCount)
|
|
1648
|
+
return {
|
|
1649
|
+
matched: !0,
|
|
1650
|
+
term,
|
|
1651
|
+
windowStart: start,
|
|
1652
|
+
windowEnd: start + windowSize - 1,
|
|
1653
|
+
matchedRatio: satisfiedCount / termTokens.length
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
return { matched: !1, term, reason: "No window satisfied the token threshold." };
|
|
1657
|
+
}
|
|
1658
|
+
const fetchTranslationPrompts = async ({
|
|
1107
1659
|
client,
|
|
1108
1660
|
sourceLanguage,
|
|
1109
1661
|
sourceDocument,
|
|
1110
1662
|
targetLanguages
|
|
1111
1663
|
}) => {
|
|
1112
|
-
const sourceDocumentString =
|
|
1664
|
+
const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
|
|
1113
1665
|
`*[_id == "translation-settings"][0] {
|
|
1114
1666
|
globalPrompt,
|
|
1115
1667
|
localePrompts[locale in $targetLanguages],
|
|
@@ -1132,12 +1684,15 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1132
1684
|
return !from || !to ? null : { from, to };
|
|
1133
1685
|
}).filter((item) => item !== null);
|
|
1134
1686
|
sourceDocument && (relevantTranslations = relevantTranslations.filter(
|
|
1135
|
-
({ from }) =>
|
|
1687
|
+
({ from }) => glossaryTermAppearsInDocument(from, sourceDocumentString)
|
|
1136
1688
|
)), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
|
|
1137
1689
|
}
|
|
1138
|
-
|
|
1690
|
+
let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
|
|
1691
|
+
return sourceDocument && filteredProtectedPhrases && (filteredProtectedPhrases = filteredProtectedPhrases.filter(
|
|
1692
|
+
(phrase) => protectedPhraseAppearsInDocument(phrase, sourceDocumentString)
|
|
1693
|
+
)), {
|
|
1139
1694
|
globalPrompt: translationSettings2?.globalPrompt,
|
|
1140
|
-
protectedPhrases:
|
|
1695
|
+
protectedPhrases: filteredProtectedPhrases,
|
|
1141
1696
|
localeInstructions: Object.fromEntries(
|
|
1142
1697
|
targetLanguages.map((locale) => [
|
|
1143
1698
|
locale,
|
|
@@ -1148,17 +1703,36 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1148
1703
|
])
|
|
1149
1704
|
)
|
|
1150
1705
|
};
|
|
1151
|
-
},
|
|
1152
|
-
const baseTerm = term.split(":")[0].trim();
|
|
1153
|
-
return
|
|
1706
|
+
}, glossaryTermAppearsInDocument = (term, documentString) => {
|
|
1707
|
+
const baseTerm = term.split(":")[0].trim(), source = preprocessSource(documentString);
|
|
1708
|
+
return matchTermInDocument(baseTerm, source, {
|
|
1709
|
+
windowSlack: 10
|
|
1710
|
+
}).matched;
|
|
1711
|
+
}, protectedPhraseAppearsInDocument = (phrase, documentString) => {
|
|
1712
|
+
const source = preprocessSource(documentString);
|
|
1713
|
+
return matchTermInDocument(phrase, source, {
|
|
1714
|
+
windowSlack: 10
|
|
1715
|
+
}).matched;
|
|
1154
1716
|
}, formatGlossaryTermInstruction = (terms) => {
|
|
1717
|
+
if (!terms || terms.length === 0)
|
|
1718
|
+
return;
|
|
1155
1719
|
const instructions = [
|
|
1156
1720
|
"## Glossary Terms",
|
|
1157
|
-
"
|
|
1158
|
-
"
|
|
1721
|
+
"Use the translations below whenever the matching source term appears. Treat these mappings as required terminology.",
|
|
1722
|
+
"Guidelines:",
|
|
1723
|
+
"- Apply glossary terms when the meaning matches, even if there are slight variations in wording, punctuation, or word order.",
|
|
1724
|
+
"- Preserve surrounding punctuation and keep the sentence natural.",
|
|
1725
|
+
"- Match the source casing when possible; only adjust when required for correct grammar.",
|
|
1726
|
+
"- If the source term meaning is absent, do not introduce a translation from this list.",
|
|
1727
|
+
"- When a term includes a colon, translate the text before the colon for general use; use the text after the colon when the feminine form is required.",
|
|
1728
|
+
"- Do not remove important words or phrases from the source text when they are essential to the meaning.",
|
|
1729
|
+
"- Never add punctuation or symbols that are not present in the source text.",
|
|
1730
|
+
"- Never apply glossary terms in code examples."
|
|
1159
1731
|
];
|
|
1160
1732
|
return terms.forEach(({ from, to }) => {
|
|
1161
|
-
instructions.push(
|
|
1733
|
+
instructions.push(
|
|
1734
|
+
`When text conveys the same meaning as "${from}", translate it as "${to}" unless the term is used in a code example.`
|
|
1735
|
+
);
|
|
1162
1736
|
}), instructions.join(`
|
|
1163
1737
|
`);
|
|
1164
1738
|
}, findKeyValues = (obj, targetKey) => {
|
|
@@ -1185,12 +1759,19 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1185
1759
|
try {
|
|
1186
1760
|
const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
|
|
1187
1761
|
if (!doc) {
|
|
1188
|
-
|
|
1762
|
+
devLog.error("Document not found", new Error("Document not found"), {
|
|
1763
|
+
documentId,
|
|
1764
|
+
component: "localizeReferences"
|
|
1765
|
+
});
|
|
1189
1766
|
return;
|
|
1190
1767
|
}
|
|
1191
1768
|
const targetLanguage = doc[languageFieldName];
|
|
1192
1769
|
if (!targetLanguage) {
|
|
1193
|
-
|
|
1770
|
+
devLog.error("No value found for language field", new Error("Missing language field"), {
|
|
1771
|
+
documentId,
|
|
1772
|
+
languageFieldName,
|
|
1773
|
+
component: "localizeReferences"
|
|
1774
|
+
});
|
|
1194
1775
|
return;
|
|
1195
1776
|
}
|
|
1196
1777
|
const refs = findKeyValues(doc, "_ref");
|
|
@@ -1198,94 +1779,46 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1198
1779
|
return;
|
|
1199
1780
|
await updateReferences(documentId, client, refs, targetLanguage);
|
|
1200
1781
|
} catch (error) {
|
|
1201
|
-
|
|
1782
|
+
devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
|
|
1783
|
+
documentId,
|
|
1784
|
+
component: "localizeReferences"
|
|
1785
|
+
});
|
|
1202
1786
|
}
|
|
1203
1787
|
}, updateReferences = async (documentId, client, refs, targetLanguage) => {
|
|
1204
|
-
const patch = client.patch(documentId);
|
|
1205
1788
|
if (!refs?.length)
|
|
1206
1789
|
return [];
|
|
1790
|
+
const refIds = refs.map((r) => undraftId(r.value)), translations = await client.fetch(
|
|
1791
|
+
`*[_type == $translationMetadataSchemaType && count(translations[value._ref in $refIds]) > 0]
|
|
1792
|
+
.translations[value._ref in $refIds]{
|
|
1793
|
+
"sourceId": value._ref,
|
|
1794
|
+
"targetId": ^.translations[_key == $targetLanguage][0].value._ref
|
|
1795
|
+
}[defined(targetId)]`,
|
|
1796
|
+
{
|
|
1797
|
+
translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
|
|
1798
|
+
refIds,
|
|
1799
|
+
targetLanguage
|
|
1800
|
+
}
|
|
1801
|
+
), translationMap = /* @__PURE__ */ new Map();
|
|
1802
|
+
for (const t of translations)
|
|
1803
|
+
t.sourceId && t.targetId && translationMap.set(t.sourceId, t.targetId);
|
|
1804
|
+
const patch = client.patch(documentId);
|
|
1805
|
+
let hasChanges = !1;
|
|
1207
1806
|
for (const ref of refs) {
|
|
1208
|
-
const newId =
|
|
1209
|
-
"(*[_type == $translationMetadataSchemaType && references($sourceId)].translations[_key == $targetLanguage].value._ref)[0]",
|
|
1210
|
-
{
|
|
1211
|
-
translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
|
|
1212
|
-
sourceId: undraftId(ref.value),
|
|
1213
|
-
targetLanguage
|
|
1214
|
-
}
|
|
1215
|
-
);
|
|
1807
|
+
const sourceId = undraftId(ref.value), newId = translationMap.get(sourceId);
|
|
1216
1808
|
newId && (patch.set({ [pathToString(ref.path)]: undraftId(newId) }), patch.set({ [`${pathToString(ref.path.slice(0, -1))}._weak`]: !0 }), patch.set({
|
|
1217
1809
|
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
1218
1810
|
_type: "reference",
|
|
1219
1811
|
_weak: !1
|
|
1220
1812
|
}
|
|
1221
|
-
}),
|
|
1813
|
+
}), hasChanges = !0);
|
|
1222
1814
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Acquire a permit from the semaphore
|
|
1232
|
-
* If no permits are available, the caller will wait until one becomes available
|
|
1233
|
-
*/
|
|
1234
|
-
async acquire() {
|
|
1235
|
-
return this.permits > 0 ? (this.permits--, Promise.resolve()) : new Promise((resolve) => {
|
|
1236
|
-
this.queue.push(resolve);
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Release a permit back to the semaphore
|
|
1241
|
-
* If there are waiting callers, one will be woken up
|
|
1242
|
-
*/
|
|
1243
|
-
release() {
|
|
1244
|
-
const next = this.queue.shift();
|
|
1245
|
-
next ? next() : this.permits++;
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Execute a function with semaphore protection
|
|
1249
|
-
* Automatically acquires and releases the permit
|
|
1250
|
-
*/
|
|
1251
|
-
async withPermit(fn) {
|
|
1252
|
-
await this.acquire();
|
|
1253
|
-
try {
|
|
1254
|
-
return await fn();
|
|
1255
|
-
} finally {
|
|
1256
|
-
this.release();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Get the number of available permits
|
|
1261
|
-
*/
|
|
1262
|
-
availablePermits() {
|
|
1263
|
-
return this.permits;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Get the number of waiting callers
|
|
1267
|
-
*/
|
|
1268
|
-
queueLength() {
|
|
1269
|
-
return this.queue.length;
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Update the number of permits
|
|
1273
|
-
* If increasing permits, waiting callers will be notified
|
|
1274
|
-
* If decreasing permits below current available, available permits will be reduced
|
|
1275
|
-
*/
|
|
1276
|
-
updatePermits(newPermits) {
|
|
1277
|
-
const currentTotal = this.permits + this.queue.length, difference = newPermits - currentTotal;
|
|
1278
|
-
if (difference > 0) {
|
|
1279
|
-
for (let i = 0; i < difference && this.queue.length > 0; i++) {
|
|
1280
|
-
const next = this.queue.shift();
|
|
1281
|
-
next && next();
|
|
1282
|
-
}
|
|
1283
|
-
const remainingIncrease = difference - (currentTotal - this.permits - this.queue.length);
|
|
1284
|
-
remainingIncrease > 0 && (this.permits += remainingIncrease);
|
|
1285
|
-
} else difference < 0 && (this.permits = Math.max(0, this.permits + difference));
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
1815
|
+
hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
|
|
1816
|
+
documentId,
|
|
1817
|
+
count: refs.length,
|
|
1818
|
+
translatedCount: translationMap.size,
|
|
1819
|
+
component: "updateReferences"
|
|
1820
|
+
}));
|
|
1821
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
1289
1822
|
for (const sourceDocument of sourceDocuments)
|
|
1290
1823
|
for (const lang of targetLanguages) {
|
|
1291
1824
|
const payload = {
|
|
@@ -1300,21 +1833,6 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1300
1833
|
payload
|
|
1301
1834
|
});
|
|
1302
1835
|
}
|
|
1303
|
-
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1304
|
-
(translation) => translation._key === targetLanguage
|
|
1305
|
-
)?.value._ref || null, determineTranslationTarget = ({
|
|
1306
|
-
translationMetadata,
|
|
1307
|
-
targetLanguage
|
|
1308
|
-
}) => {
|
|
1309
|
-
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
1310
|
-
return existingTranslationId ? {
|
|
1311
|
-
// Update existing translation
|
|
1312
|
-
operation: "createOrReplace",
|
|
1313
|
-
_id: existingTranslationId
|
|
1314
|
-
} : {
|
|
1315
|
-
// Create new translation
|
|
1316
|
-
operation: "create"
|
|
1317
|
-
};
|
|
1318
1836
|
}, getTranslationMetadata = async ({
|
|
1319
1837
|
client,
|
|
1320
1838
|
sourceDocument,
|
|
@@ -1349,6 +1867,52 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1349
1867
|
} catch (error) {
|
|
1350
1868
|
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
1351
1869
|
}
|
|
1870
|
+
}, updateTranslationMetadata = async ({
|
|
1871
|
+
client,
|
|
1872
|
+
targetLanguage,
|
|
1873
|
+
sourceDocument,
|
|
1874
|
+
sourceLanguage,
|
|
1875
|
+
newDocument,
|
|
1876
|
+
translationMetadata,
|
|
1877
|
+
isNewTranslation
|
|
1878
|
+
}) => {
|
|
1879
|
+
try {
|
|
1880
|
+
const refToNewTranslation = {
|
|
1881
|
+
_key: targetLanguage,
|
|
1882
|
+
_type: "internationalizedArrayReferenceValue",
|
|
1883
|
+
value: {
|
|
1884
|
+
_type: "reference",
|
|
1885
|
+
_ref: undraftId(newDocument._id),
|
|
1886
|
+
_weak: !0,
|
|
1887
|
+
_strengthenOnPublish: {
|
|
1888
|
+
_type: "reference",
|
|
1889
|
+
_weak: !1
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}, newTranslationHistory = {
|
|
1893
|
+
_key: targetLanguage,
|
|
1894
|
+
language: targetLanguage,
|
|
1895
|
+
sourceId: undraftId(sourceDocument._id),
|
|
1896
|
+
sourceRev: sourceDocument._rev,
|
|
1897
|
+
sourceLanguage,
|
|
1898
|
+
sourceSnapshot: JSON.stringify(sourceDocument),
|
|
1899
|
+
targetId: newDocument._id,
|
|
1900
|
+
targetRev: newDocument._rev,
|
|
1901
|
+
targetSnapshot: JSON.stringify(newDocument)
|
|
1902
|
+
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
1903
|
+
isNewTranslation ? await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit() : await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
1904
|
+
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
1905
|
+
translationMetadata.translationHistory?.some(
|
|
1906
|
+
(history) => history.language === targetLanguage
|
|
1907
|
+
) ? await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit() : await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
1910
|
+
sourceDocumentId: sourceDocument._id,
|
|
1911
|
+
targetLanguage,
|
|
1912
|
+
newDocumentId: newDocument._id,
|
|
1913
|
+
component: "updateTranslationMetadata"
|
|
1914
|
+
}), error;
|
|
1915
|
+
}
|
|
1352
1916
|
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1353
1917
|
dispatch({
|
|
1354
1918
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
@@ -1358,45 +1922,44 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1358
1922
|
status
|
|
1359
1923
|
})
|
|
1360
1924
|
});
|
|
1361
|
-
},
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
languageFieldName,
|
|
1367
|
-
targetLanguage,
|
|
1368
|
-
targetId,
|
|
1369
|
-
patch
|
|
1925
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1926
|
+
(translation) => translation._key === targetLanguage
|
|
1927
|
+
)?.value._ref || null, determineTranslationTarget = ({
|
|
1928
|
+
translationMetadata,
|
|
1929
|
+
targetLanguage
|
|
1370
1930
|
}) => {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1931
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
1932
|
+
return existingTranslationId ? {
|
|
1933
|
+
// Update existing translation
|
|
1934
|
+
operation: "createOrReplace",
|
|
1935
|
+
_id: existingTranslationId
|
|
1936
|
+
} : {
|
|
1937
|
+
// Create new translation
|
|
1938
|
+
operation: "create"
|
|
1939
|
+
};
|
|
1940
|
+
}, analyzeDocumentContent = (document) => {
|
|
1941
|
+
let hasCodeBlocks = !1, hasInlineCode = !1, codeBlockCount = 0, inlineCodeCount = 0;
|
|
1942
|
+
const hasMarkdown = !1;
|
|
1943
|
+
let hasPortableText = !1, estimatedFieldCount = 0;
|
|
1944
|
+
const traverse = (value, depth = 0) => {
|
|
1945
|
+
value != null && (depth > 0 && typeof value == "object" && estimatedFieldCount++, Array.isArray(value) ? value.forEach((item) => {
|
|
1946
|
+
item && typeof item == "object" && "_type" in item && item._type === "code" && (hasCodeBlocks = !0, codeBlockCount++, hasPortableText = !0), item && typeof item == "object" && "_type" in item && item._type === "block" && "children" in item && Array.isArray(item.children) && (hasPortableText = !0, item.children.forEach((child) => {
|
|
1947
|
+
child && typeof child == "object" && "marks" in child && Array.isArray(child.marks) && child.marks.includes("code") && (hasInlineCode = !0, inlineCodeCount++);
|
|
1948
|
+
})), traverse(item, depth + 1);
|
|
1949
|
+
}) : typeof value == "object" && Object.values(value).forEach((val) => traverse(val, depth + 1)));
|
|
1950
|
+
};
|
|
1951
|
+
traverse(document);
|
|
1952
|
+
let complexity = "simple";
|
|
1953
|
+
return estimatedFieldCount > 50 ? complexity = "complex" : estimatedFieldCount > 20 && (complexity = "moderate"), {
|
|
1954
|
+
hasCodeBlocks,
|
|
1955
|
+
hasInlineCode,
|
|
1956
|
+
codeBlockCount,
|
|
1957
|
+
inlineCodeCount,
|
|
1958
|
+
hasMarkdown,
|
|
1959
|
+
hasPortableText,
|
|
1960
|
+
complexity,
|
|
1961
|
+
estimatedFieldCount
|
|
1962
|
+
};
|
|
1400
1963
|
}, assembleLanguageSpecs = ({
|
|
1401
1964
|
sourceDocument,
|
|
1402
1965
|
languageFieldName,
|
|
@@ -1415,6 +1978,441 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1415
1978
|
title: targetLanguage
|
|
1416
1979
|
}
|
|
1417
1980
|
};
|
|
1981
|
+
}, callGPT5 = async (apiKey, request) => {
|
|
1982
|
+
const model = request.model || "gpt-5", isGPT5 = model === "gpt-5", useStreaming = !!request.onReasoningChunk, requestBody = {
|
|
1983
|
+
model,
|
|
1984
|
+
messages: request.messages,
|
|
1985
|
+
stream: useStreaming
|
|
1986
|
+
};
|
|
1987
|
+
isGPT5 || (requestBody.temperature = request.temperature ?? 0.3), isGPT5 && (requestBody.reasoning_effort = request.reasoningEffort || "medium", requestBody.verbosity = request.verbosity || "medium");
|
|
1988
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1989
|
+
method: "POST",
|
|
1990
|
+
headers: {
|
|
1991
|
+
"Content-Type": "application/json",
|
|
1992
|
+
Authorization: `Bearer ${apiKey}`
|
|
1993
|
+
},
|
|
1994
|
+
body: JSON.stringify(requestBody)
|
|
1995
|
+
});
|
|
1996
|
+
if (!response.ok) {
|
|
1997
|
+
const error = await response.json();
|
|
1998
|
+
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
1999
|
+
}
|
|
2000
|
+
if (useStreaming && response.body && request.onReasoningChunk)
|
|
2001
|
+
return await handleStreamingResponse(response, request.onReasoningChunk, model);
|
|
2002
|
+
const result = await response.json(), usage = result.usage || {}, tokensUsed = {
|
|
2003
|
+
prompt: usage.prompt_tokens || 0,
|
|
2004
|
+
completion: usage.completion_tokens || 0,
|
|
2005
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
2006
|
+
// New in GPT-5
|
|
2007
|
+
total: usage.total_tokens || 0
|
|
2008
|
+
}, cost = calculateGPT5Cost(tokensUsed, request.model || "gpt-5"), messageContent = result.choices[0].message.content;
|
|
2009
|
+
let reasoningSummary, actualContent = messageContent;
|
|
2010
|
+
const reasoningMatch = messageContent.match(/^<reasoning>([\s\S]*?)<\/reasoning>\n\n([\s\S]*)$/i);
|
|
2011
|
+
return reasoningMatch && (reasoningSummary = reasoningMatch[1].trim(), actualContent = reasoningMatch[2]), {
|
|
2012
|
+
content: actualContent,
|
|
2013
|
+
tokensUsed,
|
|
2014
|
+
cost,
|
|
2015
|
+
reasoningSummary
|
|
2016
|
+
};
|
|
2017
|
+
}, handleStreamingResponse = async (response, onReasoningChunk, model) => {
|
|
2018
|
+
if (!response.body)
|
|
2019
|
+
throw new Error("Response body is null");
|
|
2020
|
+
const reader = response.body.getReader(), decoder = new TextDecoder();
|
|
2021
|
+
let fullContent = "", inReasoning = !1, reasoningBuffer = "", contentBuffer = "", tokensUsed = { prompt: 0, completion: 0, reasoning: 0, total: 0 };
|
|
2022
|
+
try {
|
|
2023
|
+
for (; ; ) {
|
|
2024
|
+
const { done, value } = await reader.read();
|
|
2025
|
+
if (done) break;
|
|
2026
|
+
const lines = decoder.decode(value, { stream: !0 }).split(`
|
|
2027
|
+
`).filter((line) => line.trim() !== "");
|
|
2028
|
+
for (const line of lines)
|
|
2029
|
+
if (line.startsWith("data: ")) {
|
|
2030
|
+
const data = line.slice(6);
|
|
2031
|
+
if (data === "[DONE]") continue;
|
|
2032
|
+
try {
|
|
2033
|
+
const parsed = JSON.parse(data);
|
|
2034
|
+
parsed.usage && (tokensUsed = {
|
|
2035
|
+
prompt: parsed.usage.prompt_tokens || 0,
|
|
2036
|
+
completion: parsed.usage.completion_tokens || 0,
|
|
2037
|
+
reasoning: parsed.usage.reasoning_tokens || 0,
|
|
2038
|
+
total: parsed.usage.total_tokens || 0
|
|
2039
|
+
});
|
|
2040
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
2041
|
+
if (delta)
|
|
2042
|
+
if (fullContent += delta, delta.includes("<reasoning>")) {
|
|
2043
|
+
inReasoning = !0;
|
|
2044
|
+
const parts = delta.split("<reasoning>");
|
|
2045
|
+
reasoningBuffer += parts[1] || "";
|
|
2046
|
+
} else if (delta.includes("</reasoning>")) {
|
|
2047
|
+
inReasoning = !1;
|
|
2048
|
+
const parts = delta.split("</reasoning>");
|
|
2049
|
+
reasoningBuffer += parts[0] || "", contentBuffer += parts[1] || "";
|
|
2050
|
+
} else inReasoning ? (reasoningBuffer += delta, onReasoningChunk(reasoningBuffer)) : contentBuffer += delta;
|
|
2051
|
+
} catch {
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
} finally {
|
|
2056
|
+
reader.releaseLock();
|
|
2057
|
+
}
|
|
2058
|
+
const cost = calculateGPT5Cost(tokensUsed, model);
|
|
2059
|
+
return {
|
|
2060
|
+
content: contentBuffer || fullContent,
|
|
2061
|
+
tokensUsed,
|
|
2062
|
+
cost,
|
|
2063
|
+
reasoningSummary: reasoningBuffer || void 0
|
|
2064
|
+
};
|
|
2065
|
+
}, calculateGPT5Cost = (tokens, model) => {
|
|
2066
|
+
const pricing = {
|
|
2067
|
+
"gpt-5": {
|
|
2068
|
+
input: 2e-6,
|
|
2069
|
+
// $0.002 per 1k input tokens
|
|
2070
|
+
output: 1e-5,
|
|
2071
|
+
// $0.010 per 1k output tokens
|
|
2072
|
+
reasoning: 5e-6
|
|
2073
|
+
// $0.005 per 1k reasoning tokens
|
|
2074
|
+
},
|
|
2075
|
+
"gpt-4o": {
|
|
2076
|
+
input: 5e-6,
|
|
2077
|
+
output: 14999999999999999e-21,
|
|
2078
|
+
reasoning: 0
|
|
2079
|
+
// No reasoning tokens in GPT-4
|
|
2080
|
+
},
|
|
2081
|
+
"gpt-4-turbo": {
|
|
2082
|
+
input: 1e-5,
|
|
2083
|
+
output: 29999999999999997e-21,
|
|
2084
|
+
reasoning: 0
|
|
2085
|
+
},
|
|
2086
|
+
"gpt-4": {
|
|
2087
|
+
input: 29999999999999997e-21,
|
|
2088
|
+
output: 59999999999999995e-21,
|
|
2089
|
+
reasoning: 0
|
|
2090
|
+
}
|
|
2091
|
+
}, modelPricing = pricing[model] || pricing["gpt-5"];
|
|
2092
|
+
return tokens.prompt * modelPricing.input + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
2093
|
+
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
2094
|
+
const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
|
|
2095
|
+
return applyTextChanges(result, parsed), result;
|
|
2096
|
+
}, parseReviewFormat = (text) => {
|
|
2097
|
+
const root = [], stack = [], lines = text.split(`
|
|
2098
|
+
`);
|
|
2099
|
+
for (const line of lines) {
|
|
2100
|
+
const trimmed = line.trim();
|
|
2101
|
+
if (!trimmed) continue;
|
|
2102
|
+
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
2103
|
+
if (openMatch) {
|
|
2104
|
+
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
2105
|
+
type: tagName,
|
|
2106
|
+
name: attributes.name,
|
|
2107
|
+
text: content ? unescapeXml(content) : void 0,
|
|
2108
|
+
attributes,
|
|
2109
|
+
children: []
|
|
2110
|
+
}, closingTag = `</${tagName}>`;
|
|
2111
|
+
if (content.endsWith(closingTag.replace(/[<>]/g, ""))) {
|
|
2112
|
+
const textContent = content.substring(0, content.lastIndexOf("<"));
|
|
2113
|
+
if (node.text = unescapeXml(textContent), stack.length > 0) {
|
|
2114
|
+
const parent = stack[stack.length - 1];
|
|
2115
|
+
parent && parent.children.push(node);
|
|
2116
|
+
} else
|
|
2117
|
+
root.push(node);
|
|
2118
|
+
} else if (content.includes(closingTag))
|
|
2119
|
+
if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
|
|
2120
|
+
const parent = stack[stack.length - 1];
|
|
2121
|
+
parent && parent.children.push(node);
|
|
2122
|
+
} else
|
|
2123
|
+
root.push(node);
|
|
2124
|
+
else
|
|
2125
|
+
stack.push(node);
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (trimmed.match(/^<\/(field|block|span)>$/) && stack.length > 0) {
|
|
2129
|
+
const node = stack.pop();
|
|
2130
|
+
if (node)
|
|
2131
|
+
if (stack.length > 0) {
|
|
2132
|
+
const parent = stack[stack.length - 1];
|
|
2133
|
+
parent && parent.children.push(node);
|
|
2134
|
+
} else
|
|
2135
|
+
root.push(node);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return root;
|
|
2139
|
+
}, parseAttributes = (attrStr) => {
|
|
2140
|
+
const attrs = {}, attrRegex = /(\w+)="([^"]*)"/g;
|
|
2141
|
+
let match = attrRegex.exec(attrStr);
|
|
2142
|
+
for (; match !== null; )
|
|
2143
|
+
attrs[match[1]] = unescapeXml(match[2]), match = attrRegex.exec(attrStr);
|
|
2144
|
+
return attrs;
|
|
2145
|
+
}, unescapeXml = (str) => str.replace(/'/g, "'").replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&"), applyTextChanges = (doc, parsedNodes) => {
|
|
2146
|
+
for (const node of parsedNodes)
|
|
2147
|
+
if (node.type === "field" && node.name) {
|
|
2148
|
+
const path = [node.name];
|
|
2149
|
+
applyFieldChanges(doc, path, node);
|
|
2150
|
+
}
|
|
2151
|
+
}, applyFieldChanges = (doc, path, node) => {
|
|
2152
|
+
if (node.attributes.type === "array") {
|
|
2153
|
+
node.children.forEach((child, idx) => {
|
|
2154
|
+
applyFieldChanges(doc, [...path, idx], child);
|
|
2155
|
+
});
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (node.attributes.type === "object") {
|
|
2159
|
+
for (const child of node.children)
|
|
2160
|
+
child.name && applyFieldChanges(doc, [...path, child.name], child);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
if (node.children.some((c) => c.type === "block")) {
|
|
2164
|
+
const fieldValue = get(doc, path);
|
|
2165
|
+
Array.isArray(fieldValue) && node.children.forEach((blockNode, idx) => {
|
|
2166
|
+
blockNode.type === "block" && applyBlockChanges(fieldValue[idx], blockNode);
|
|
2167
|
+
});
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
node.text !== void 0 && !node.attributes.readonly && set(doc, path, node.text);
|
|
2171
|
+
}, applyBlockChanges = (block, blockNode) => {
|
|
2172
|
+
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
2173
|
+
devLog.warn("Block key mismatch, skipping", {
|
|
2174
|
+
expected: block._key,
|
|
2175
|
+
found: blockNode.attributes._key,
|
|
2176
|
+
component: "deserializeDocument"
|
|
2177
|
+
});
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
const children = block.children;
|
|
2181
|
+
Array.isArray(children) && blockNode.children.forEach((spanNode, idx) => {
|
|
2182
|
+
const childItem = children[idx];
|
|
2183
|
+
spanNode.type === "span" && childItem && applySpanChanges(childItem, spanNode);
|
|
2184
|
+
});
|
|
2185
|
+
}, applySpanChanges = (span, spanNode) => {
|
|
2186
|
+
if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
|
|
2187
|
+
devLog.warn("Span key mismatch, skipping", {
|
|
2188
|
+
expected: span._key,
|
|
2189
|
+
found: spanNode.attributes._key,
|
|
2190
|
+
component: "deserializeDocument"
|
|
2191
|
+
});
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
spanNode.text !== void 0 && (span.text = spanNode.text);
|
|
2195
|
+
}, serializeDocument = (doc) => {
|
|
2196
|
+
const lines = [], serialize = (value, path = [], indent = 0) => {
|
|
2197
|
+
const indentStr = " ".repeat(indent);
|
|
2198
|
+
if (path.length === 0) {
|
|
2199
|
+
if (isPlainObject(value))
|
|
2200
|
+
for (const [key, val] of Object.entries(value))
|
|
2201
|
+
!key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const fieldName = path[path.length - 1];
|
|
2205
|
+
if (Array.isArray(value)) {
|
|
2206
|
+
value.some((item) => item?._type === "block") ? (lines.push(`${indentStr}<field name="${fieldName}">`), value.forEach((block, idx) => {
|
|
2207
|
+
serializeBlock(block, [...path, idx], indent + 1, lines);
|
|
2208
|
+
}), lines.push(`${indentStr}</field>`)) : (lines.push(`${indentStr}<field name="${fieldName}" type="array">`), value.forEach((item, idx) => {
|
|
2209
|
+
serialize(item, [...path, idx], indent + 1);
|
|
2210
|
+
}), lines.push(`${indentStr}</field>`));
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (isPlainObject(value)) {
|
|
2214
|
+
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2215
|
+
for (const [key, val] of Object.entries(value))
|
|
2216
|
+
key.startsWith("_") || serialize(val, [...path, key], indent + 1);
|
|
2217
|
+
lines.push(`${indentStr}</field>`);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (typeof value == "string") {
|
|
2221
|
+
value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
2225
|
+
};
|
|
2226
|
+
return serialize(doc), lines.join(`
|
|
2227
|
+
`);
|
|
2228
|
+
}, serializeBlock = (block, path, indent, lines) => {
|
|
2229
|
+
const indentStr = " ".repeat(indent), metadata = {};
|
|
2230
|
+
block._type && (metadata._type = block._type), block._key && (metadata._key = block._key), block.style && (metadata.style = block.style), block.listItem && (metadata.listItem = block.listItem), block.level && (metadata.level = block.level), block.markDefs && (metadata.markDefs = block.markDefs);
|
|
2231
|
+
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(typeof v == "string" ? v : JSON.stringify(v))}"`).join(" ");
|
|
2232
|
+
lines.push(`${indentStr}<block ${metaStr}>`), Array.isArray(block.children) && block.children.forEach((child, idx) => {
|
|
2233
|
+
serializeSpan(child, [...path, "children", idx], indent + 1, lines);
|
|
2234
|
+
}), lines.push(`${indentStr}</block>`);
|
|
2235
|
+
}, serializeSpan = (span, _path, indent, lines) => {
|
|
2236
|
+
const indentStr = " ".repeat(indent);
|
|
2237
|
+
if (!span || typeof span != "object") return;
|
|
2238
|
+
const metadata = {};
|
|
2239
|
+
span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
|
|
2240
|
+
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
|
|
2241
|
+
metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
|
|
2242
|
+
}, escapeXml = (str) => str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"), isPlainObject = (val) => typeof val == "object" && val !== null && !Array.isArray(val), validateStructureIntegrity = (original, modified) => {
|
|
2243
|
+
const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
|
|
2244
|
+
for (const field of criticalFields)
|
|
2245
|
+
original[field] !== modified[field] && errors.push({
|
|
2246
|
+
path: [field],
|
|
2247
|
+
expected: original[field],
|
|
2248
|
+
actual: modified[field],
|
|
2249
|
+
type: "metadata_changed"
|
|
2250
|
+
});
|
|
2251
|
+
return validateStructureRecursive(original, modified, [], errors), {
|
|
2252
|
+
valid: errors.length === 0,
|
|
2253
|
+
errors
|
|
2254
|
+
};
|
|
2255
|
+
}, validateStructureRecursive = (original, modified, path, errors) => {
|
|
2256
|
+
if (typeof original != typeof modified) {
|
|
2257
|
+
errors.push({
|
|
2258
|
+
path,
|
|
2259
|
+
expected: typeof original,
|
|
2260
|
+
actual: typeof modified,
|
|
2261
|
+
type: "type_mismatch"
|
|
2262
|
+
});
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (Array.isArray(original)) {
|
|
2266
|
+
if (!Array.isArray(modified)) {
|
|
2267
|
+
errors.push({
|
|
2268
|
+
path,
|
|
2269
|
+
expected: "array",
|
|
2270
|
+
actual: typeof modified,
|
|
2271
|
+
type: "type_mismatch"
|
|
2272
|
+
});
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (original.length !== modified.length) {
|
|
2276
|
+
errors.push({
|
|
2277
|
+
path,
|
|
2278
|
+
expected: `length ${original.length}`,
|
|
2279
|
+
actual: `length ${modified.length}`,
|
|
2280
|
+
type: "type_mismatch"
|
|
2281
|
+
});
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
original.forEach((item, idx) => {
|
|
2285
|
+
validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
|
|
2286
|
+
});
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
|
|
2290
|
+
const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
|
|
2291
|
+
for (const key of origKeys)
|
|
2292
|
+
key in modified || errors.push({
|
|
2293
|
+
path: [...path, key],
|
|
2294
|
+
expected: "present",
|
|
2295
|
+
actual: "missing",
|
|
2296
|
+
type: "missing"
|
|
2297
|
+
});
|
|
2298
|
+
for (const key of modKeys)
|
|
2299
|
+
key in original || errors.push({
|
|
2300
|
+
path: [...path, key],
|
|
2301
|
+
expected: "absent",
|
|
2302
|
+
actual: "present",
|
|
2303
|
+
type: "extra"
|
|
2304
|
+
});
|
|
2305
|
+
const origObj = original, modObj = modified;
|
|
2306
|
+
for (const key of origKeys)
|
|
2307
|
+
key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
|
|
2308
|
+
path: [...path, key],
|
|
2309
|
+
expected: origObj[key],
|
|
2310
|
+
actual: modObj[key],
|
|
2311
|
+
type: "metadata_changed"
|
|
2312
|
+
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2313
|
+
}
|
|
2314
|
+
}, assembleTranslationPrompt = (context, components) => {
|
|
2315
|
+
const sections = [
|
|
2316
|
+
{
|
|
2317
|
+
id: "agentDirectives",
|
|
2318
|
+
content: buildAgentDirectivesSection(context),
|
|
2319
|
+
required: !0,
|
|
2320
|
+
priority: 0
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
id: "criticalRules",
|
|
2324
|
+
content: buildCriticalRulesSection(context),
|
|
2325
|
+
required: !0,
|
|
2326
|
+
priority: 1
|
|
2327
|
+
}
|
|
2328
|
+
];
|
|
2329
|
+
components.glossaryTerms?.trim() && sections.push({
|
|
2330
|
+
id: "glossaryTerms",
|
|
2331
|
+
content: buildGlossarySection(components.glossaryTerms),
|
|
2332
|
+
required: !1,
|
|
2333
|
+
priority: 3
|
|
2334
|
+
});
|
|
2335
|
+
const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
|
|
2336
|
+
styleSection && sections.push({
|
|
2337
|
+
id: "styleGuide",
|
|
2338
|
+
content: styleSection,
|
|
2339
|
+
required: !1,
|
|
2340
|
+
priority: 4
|
|
2341
|
+
}), sections.sort((a, b) => a.priority - b.priority);
|
|
2342
|
+
let styleGuide = "";
|
|
2343
|
+
const omittedSections = [], truncatedSections = [];
|
|
2344
|
+
for (const section of sections) {
|
|
2345
|
+
const prefix = styleGuide.length > 0 ? `
|
|
2346
|
+
|
|
2347
|
+
---
|
|
2348
|
+
|
|
2349
|
+
` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
|
|
2350
|
+
if (availableCharacters <= 0) {
|
|
2351
|
+
if (section.required) {
|
|
2352
|
+
const truncatedContent = truncateSection(section.content, 0);
|
|
2353
|
+
truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
|
|
2354
|
+
} else
|
|
2355
|
+
omittedSections.push(section.id);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
if (section.content.length <= availableCharacters) {
|
|
2359
|
+
styleGuide += prefix + section.content;
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
if (section.required) {
|
|
2363
|
+
const truncatedContent = truncateSection(section.content, availableCharacters);
|
|
2364
|
+
styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
|
|
2365
|
+
} else
|
|
2366
|
+
omittedSections.push(section.id);
|
|
2367
|
+
}
|
|
2368
|
+
return {
|
|
2369
|
+
styleGuide,
|
|
2370
|
+
omittedSections,
|
|
2371
|
+
truncatedSections
|
|
2372
|
+
};
|
|
2373
|
+
}, truncateSection = (content, limit) => {
|
|
2374
|
+
if (limit <= 0)
|
|
2375
|
+
return "";
|
|
2376
|
+
if (content.length <= limit)
|
|
2377
|
+
return content;
|
|
2378
|
+
if (limit <= 1)
|
|
2379
|
+
return content.slice(0, limit);
|
|
2380
|
+
const ellipsis = "\u2026";
|
|
2381
|
+
if (limit <= ellipsis.length)
|
|
2382
|
+
return content.slice(0, limit);
|
|
2383
|
+
const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
|
|
2384
|
+
return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
|
|
2385
|
+
}, buildAgentDirectivesSection = (context) => {
|
|
2386
|
+
const { targetLanguage, sourceLanguage } = context;
|
|
2387
|
+
return `# AGENT WORKFLOW DIRECTIVES
|
|
2388
|
+
|
|
2389
|
+
- Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
|
|
2390
|
+
- Scan each field once, then move directly into translation work.
|
|
2391
|
+
- Plan updates before editing so document structure remains intact.
|
|
2392
|
+
- Default to medium reasoning effort and escalate only for ambiguous passages.
|
|
2393
|
+
- Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
|
|
2394
|
+
}, buildCriticalRulesSection = (context) => {
|
|
2395
|
+
const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
|
|
2396
|
+
return `# CRITICAL TRANSLATION RULES
|
|
2397
|
+
|
|
2398
|
+
Follow these without exception:
|
|
2399
|
+
|
|
2400
|
+
1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
|
|
2401
|
+
2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
|
|
2402
|
+
3. Maintain all layout, markdown, rich-text marks, and data structure.
|
|
2403
|
+
4. Never invent metadata or field values that are absent in the source.
|
|
2404
|
+
5. Leave technical tokens (code, IDs, filenames) untouched.
|
|
2405
|
+
6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
|
|
2406
|
+
}, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
|
|
2407
|
+
|
|
2408
|
+
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2409
|
+
|
|
2410
|
+
${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
2411
|
+
const components = [];
|
|
2412
|
+
return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
|
|
2413
|
+
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2414
|
+
), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
|
|
2415
|
+
`) : null;
|
|
1418
2416
|
}, translateEntireDocument = async ({
|
|
1419
2417
|
dispatch,
|
|
1420
2418
|
client,
|
|
@@ -1422,17 +2420,175 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1422
2420
|
sourceDocument,
|
|
1423
2421
|
targetLanguage,
|
|
1424
2422
|
languageFieldName,
|
|
1425
|
-
translationTarget,
|
|
2423
|
+
translationTarget,
|
|
2424
|
+
translationPrompts,
|
|
2425
|
+
translationMode,
|
|
2426
|
+
openaiConfig,
|
|
2427
|
+
releaseId,
|
|
2428
|
+
isNewTranslation
|
|
2429
|
+
}) => {
|
|
2430
|
+
const logContext = {
|
|
2431
|
+
documentId: sourceDocument._id,
|
|
2432
|
+
targetLanguage,
|
|
2433
|
+
schemaId,
|
|
2434
|
+
operation: translationTarget.operation,
|
|
2435
|
+
targetId: translationTarget._id,
|
|
2436
|
+
translationMode,
|
|
2437
|
+
releaseId,
|
|
2438
|
+
isNewTranslation
|
|
2439
|
+
};
|
|
2440
|
+
devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
|
|
2441
|
+
try {
|
|
2442
|
+
let versionedDocumentId, baseDocumentId;
|
|
2443
|
+
if (isNewTranslation) {
|
|
2444
|
+
const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
|
|
2445
|
+
...docWithoutMeta,
|
|
2446
|
+
_type: sourceDocument._type,
|
|
2447
|
+
[languageFieldName]: targetLanguage
|
|
2448
|
+
};
|
|
2449
|
+
devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
|
|
2450
|
+
actionType: "sanity.action.document.version.create",
|
|
2451
|
+
publishedId: baseDocumentId,
|
|
2452
|
+
document: {
|
|
2453
|
+
...newTranslationDoc,
|
|
2454
|
+
_type: sourceDocument._type,
|
|
2455
|
+
_id: `versions.${releaseId}.${baseDocumentId}`
|
|
2456
|
+
}
|
|
2457
|
+
}), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
|
|
2458
|
+
...logContext,
|
|
2459
|
+
versionedDocumentId,
|
|
2460
|
+
baseDocumentId
|
|
2461
|
+
});
|
|
2462
|
+
} else {
|
|
2463
|
+
const existingTranslationId = translationTarget._id;
|
|
2464
|
+
if (!existingTranslationId)
|
|
2465
|
+
throw new Error("Translation target ID not found for update operation");
|
|
2466
|
+
devLog.info("Creating version of existing translation in release", {
|
|
2467
|
+
...logContext,
|
|
2468
|
+
existingTranslationId
|
|
2469
|
+
});
|
|
2470
|
+
const existingDoc = await client.getDocument(existingTranslationId);
|
|
2471
|
+
if (!existingDoc)
|
|
2472
|
+
throw new Error(`Existing translation document not found: ${existingTranslationId}`);
|
|
2473
|
+
await client.action({
|
|
2474
|
+
actionType: "sanity.action.document.version.create",
|
|
2475
|
+
publishedId: existingTranslationId,
|
|
2476
|
+
document: {
|
|
2477
|
+
...existingDoc,
|
|
2478
|
+
_id: `versions.${releaseId}.${existingTranslationId}`
|
|
2479
|
+
}
|
|
2480
|
+
}), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
|
|
2481
|
+
...logContext,
|
|
2482
|
+
versionedDocumentId
|
|
2483
|
+
});
|
|
2484
|
+
const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
|
|
2485
|
+
await client.createOrReplace({
|
|
2486
|
+
...sourceContent,
|
|
2487
|
+
_id: versionedDocumentId,
|
|
2488
|
+
[languageFieldName]: targetLanguage
|
|
2489
|
+
}), devLog.info("Source content cloned onto version", {
|
|
2490
|
+
...logContext,
|
|
2491
|
+
versionedDocumentId
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
let translatedDocument;
|
|
2495
|
+
switch (translationMode) {
|
|
2496
|
+
case "openai-only": {
|
|
2497
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2498
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2499
|
+
devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
|
|
2500
|
+
const versionedDoc = await client.getDocument(versionedDocumentId);
|
|
2501
|
+
if (!versionedDoc)
|
|
2502
|
+
throw new Error(`Versioned document not found: ${versionedDocumentId}`);
|
|
2503
|
+
const translatedDoc = await translateInPlaceWithOpenAI({
|
|
2504
|
+
dispatch,
|
|
2505
|
+
sourceDocument,
|
|
2506
|
+
targetDocument: versionedDoc,
|
|
2507
|
+
targetLanguage,
|
|
2508
|
+
languageFieldName,
|
|
2509
|
+
translationPrompts,
|
|
2510
|
+
openaiConfig,
|
|
2511
|
+
isReview: !1
|
|
2512
|
+
});
|
|
2513
|
+
translatedDoc && (translatedDocument = await client.createOrReplace({
|
|
2514
|
+
...translatedDoc,
|
|
2515
|
+
_id: versionedDocumentId
|
|
2516
|
+
}));
|
|
2517
|
+
break;
|
|
2518
|
+
}
|
|
2519
|
+
case "sanity-with-review":
|
|
2520
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2521
|
+
throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
|
|
2522
|
+
if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2523
|
+
dispatch,
|
|
2524
|
+
client,
|
|
2525
|
+
schemaId,
|
|
2526
|
+
sourceDocument,
|
|
2527
|
+
targetLanguage,
|
|
2528
|
+
languageFieldName,
|
|
2529
|
+
versionedDocumentId,
|
|
2530
|
+
translationPrompts
|
|
2531
|
+
}), translatedDocument) {
|
|
2532
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
|
|
2533
|
+
const reviewedDoc = await translateInPlaceWithOpenAI({
|
|
2534
|
+
dispatch,
|
|
2535
|
+
sourceDocument,
|
|
2536
|
+
targetDocument: translatedDocument,
|
|
2537
|
+
targetLanguage,
|
|
2538
|
+
languageFieldName,
|
|
2539
|
+
translationPrompts,
|
|
2540
|
+
openaiConfig,
|
|
2541
|
+
isReview: !0
|
|
2542
|
+
});
|
|
2543
|
+
reviewedDoc && (translatedDocument = await client.createOrReplace({
|
|
2544
|
+
...reviewedDoc,
|
|
2545
|
+
_id: versionedDocumentId
|
|
2546
|
+
}));
|
|
2547
|
+
}
|
|
2548
|
+
break;
|
|
2549
|
+
default:
|
|
2550
|
+
devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2551
|
+
dispatch,
|
|
2552
|
+
client,
|
|
2553
|
+
schemaId,
|
|
2554
|
+
sourceDocument,
|
|
2555
|
+
targetLanguage,
|
|
2556
|
+
languageFieldName,
|
|
2557
|
+
versionedDocumentId,
|
|
2558
|
+
translationPrompts
|
|
2559
|
+
});
|
|
2560
|
+
break;
|
|
2561
|
+
}
|
|
2562
|
+
return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
|
|
2563
|
+
...translatedDocument,
|
|
2564
|
+
_id: baseDocumentId
|
|
2565
|
+
} : void 0;
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2568
|
+
devLog.error(
|
|
2569
|
+
"translateEntireDocument failed",
|
|
2570
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2571
|
+
logContext
|
|
2572
|
+
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2573
|
+
errorMessage
|
|
2574
|
+
}), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
}, translateWithSanityAgent = async ({
|
|
2578
|
+
dispatch,
|
|
2579
|
+
client,
|
|
2580
|
+
schemaId,
|
|
2581
|
+
sourceDocument,
|
|
2582
|
+
targetLanguage,
|
|
2583
|
+
languageFieldName,
|
|
2584
|
+
versionedDocumentId,
|
|
1426
2585
|
translationPrompts
|
|
1427
2586
|
}) => {
|
|
1428
2587
|
const logContext = {
|
|
1429
2588
|
documentId: sourceDocument._id,
|
|
1430
2589
|
targetLanguage,
|
|
1431
|
-
|
|
1432
|
-
operation: translationTarget.operation,
|
|
1433
|
-
targetId: translationTarget._id
|
|
2590
|
+
versionedDocumentId
|
|
1434
2591
|
};
|
|
1435
|
-
devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
|
|
1436
2592
|
try {
|
|
1437
2593
|
const { toLanguage, fromLanguage } = assembleLanguageSpecs({
|
|
1438
2594
|
sourceDocument,
|
|
@@ -1444,104 +2600,263 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1444
2600
|
toLanguage,
|
|
1445
2601
|
fromLanguage
|
|
1446
2602
|
});
|
|
1447
|
-
|
|
1448
|
-
|
|
2603
|
+
const documentAnalysis = analyzeDocumentContent(sourceDocument);
|
|
2604
|
+
devLog.debug("Document analysis completed", {
|
|
1449
2605
|
...logContext,
|
|
1450
|
-
|
|
1451
|
-
}),
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2606
|
+
analysis: documentAnalysis
|
|
2607
|
+
}), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
|
|
2608
|
+
const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
|
|
2609
|
+
{
|
|
2610
|
+
documentAnalysis,
|
|
2611
|
+
isUpdate: !1,
|
|
2612
|
+
// Always fresh translation in a version
|
|
2613
|
+
targetLanguage,
|
|
2614
|
+
sourceLanguage: sourceDocument[languageFieldName]
|
|
2615
|
+
},
|
|
2616
|
+
{
|
|
2617
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2618
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2619
|
+
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2620
|
+
protectedPhrases: translationPrompts.protectedPhrases
|
|
2621
|
+
}
|
|
2622
|
+
);
|
|
2623
|
+
devLog.info("Structured style guide generated", {
|
|
1456
2624
|
...logContext,
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
1462
|
-
translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
1463
|
-
extantTranslation ? UPDATE_EXISTING_TRANSLATION_PROMPT : null
|
|
1464
|
-
].filter(Boolean).join(`
|
|
1465
|
-
|
|
1466
|
-
`);
|
|
1467
|
-
devLog.info("Style guide generated", {
|
|
2625
|
+
styleGuideLength: styleGuide.length,
|
|
2626
|
+
omittedSections,
|
|
2627
|
+
truncatedSections
|
|
2628
|
+
}), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
|
|
1468
2629
|
...logContext,
|
|
1469
|
-
|
|
2630
|
+
omittedSections,
|
|
2631
|
+
truncatedSections
|
|
1470
2632
|
});
|
|
1471
2633
|
const newDocument = await client.agent.action.translate({
|
|
1472
2634
|
schemaId,
|
|
1473
2635
|
toLanguage,
|
|
1474
2636
|
fromLanguage,
|
|
1475
|
-
documentId:
|
|
1476
|
-
|
|
2637
|
+
documentId: versionedDocumentId,
|
|
2638
|
+
// Use versioned document ID
|
|
1477
2639
|
languageFieldPath: [languageFieldName],
|
|
1478
2640
|
styleGuide,
|
|
1479
|
-
styleGuideParams: {
|
|
1480
|
-
extantTranslation
|
|
1481
|
-
},
|
|
1482
2641
|
protectedPhrases: translationPrompts.protectedPhrases || [],
|
|
1483
2642
|
conditionalPaths: {
|
|
1484
2643
|
defaultHidden: !1,
|
|
1485
2644
|
defaultReadOnly: !1
|
|
1486
2645
|
}
|
|
1487
2646
|
});
|
|
1488
|
-
return devLog.info("
|
|
2647
|
+
return devLog.info("Sanity agent translation completed", {
|
|
1489
2648
|
...logContext,
|
|
1490
2649
|
newDocumentId: newDocument._id,
|
|
1491
2650
|
newDocumentRev: newDocument._rev
|
|
1492
|
-
}),
|
|
2651
|
+
}), newDocument;
|
|
1493
2652
|
} catch (error) {
|
|
1494
2653
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1495
2654
|
devLog.error(
|
|
1496
|
-
"
|
|
2655
|
+
"Sanity agent translation failed",
|
|
1497
2656
|
error instanceof Error ? error : new Error(String(error)),
|
|
1498
2657
|
logContext
|
|
1499
2658
|
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
1500
2659
|
errorMessage
|
|
1501
|
-
})
|
|
2660
|
+
});
|
|
1502
2661
|
return;
|
|
1503
2662
|
}
|
|
1504
|
-
},
|
|
1505
|
-
|
|
1506
|
-
targetLanguage,
|
|
2663
|
+
}, translateInPlaceWithOpenAI = async ({
|
|
2664
|
+
dispatch,
|
|
1507
2665
|
sourceDocument,
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2666
|
+
targetDocument,
|
|
2667
|
+
targetLanguage,
|
|
2668
|
+
languageFieldName,
|
|
2669
|
+
translationPrompts,
|
|
2670
|
+
openaiConfig,
|
|
2671
|
+
isReview
|
|
1512
2672
|
}) => {
|
|
2673
|
+
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2674
|
+
sourceId: sourceDocument._id,
|
|
2675
|
+
targetId: targetDocument._id,
|
|
2676
|
+
targetLanguage,
|
|
2677
|
+
sourceLanguage,
|
|
2678
|
+
isReview
|
|
2679
|
+
};
|
|
1513
2680
|
try {
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
_weak: !1
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}, newTranslationHistory = {
|
|
1527
|
-
_key: targetLanguage,
|
|
1528
|
-
language: targetLanguage,
|
|
1529
|
-
sourceId: undraftId(sourceDocument._id),
|
|
1530
|
-
sourceRev: sourceDocument._rev,
|
|
2681
|
+
const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
|
|
2682
|
+
devLog.debug("Documents serialized for OpenAI", {
|
|
2683
|
+
...logContext,
|
|
2684
|
+
sourceLength: sourceText.length,
|
|
2685
|
+
targetLength: targetText.length
|
|
2686
|
+
});
|
|
2687
|
+
const prompt = isReview ? buildReviewPrompt({
|
|
2688
|
+
sourceText,
|
|
2689
|
+
translatedText: targetText,
|
|
1531
2690
|
sourceLanguage,
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
2691
|
+
targetLanguage,
|
|
2692
|
+
translationPrompts
|
|
2693
|
+
}) : buildTranslationPrompt({
|
|
2694
|
+
sourceText,
|
|
2695
|
+
sourceLanguage,
|
|
2696
|
+
targetLanguage,
|
|
2697
|
+
translationPrompts
|
|
2698
|
+
});
|
|
2699
|
+
devLog.debug("Calling GPT-5", {
|
|
2700
|
+
...logContext,
|
|
2701
|
+
promptLength: prompt.length
|
|
2702
|
+
});
|
|
2703
|
+
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2704
|
+
messages: [
|
|
2705
|
+
{
|
|
2706
|
+
role: "system",
|
|
2707
|
+
content: isReview ? "You are a translation quality reviewer. Review and refine translations while preserving structure." : "You are a professional translator. Translate documents while preserving their exact structure."
|
|
2708
|
+
},
|
|
2709
|
+
{ role: "user", content: prompt }
|
|
2710
|
+
],
|
|
2711
|
+
model: openaiConfig.model,
|
|
2712
|
+
reasoningEffort: openaiConfig.reasoningEffort,
|
|
2713
|
+
verbosity: openaiConfig.verbosity,
|
|
2714
|
+
onReasoningChunk: (reasoning) => {
|
|
2715
|
+
const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2716
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
|
|
2717
|
+
reasoningProgress: reasoning
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
devLog.info("OpenAI operation completed", {
|
|
2722
|
+
...logContext,
|
|
2723
|
+
tokensUsed: response.tokensUsed.total,
|
|
2724
|
+
cost: response.cost
|
|
2725
|
+
});
|
|
2726
|
+
const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
|
|
2727
|
+
if (!validation.valid) {
|
|
2728
|
+
devLog.error("Structure validation failed", {
|
|
2729
|
+
...logContext,
|
|
2730
|
+
errors: validation.errors
|
|
2731
|
+
});
|
|
2732
|
+
const unexpectedErrors = validation.errors.filter(
|
|
2733
|
+
(err) => !(err.path.length === 1 && err.path[0] === languageFieldName)
|
|
2734
|
+
);
|
|
2735
|
+
if (unexpectedErrors.length > 0)
|
|
2736
|
+
throw new Error(
|
|
2737
|
+
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
devLog.info("Structure validation passed", logContext);
|
|
2741
|
+
const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2742
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
|
|
2743
|
+
tokensUsed: response.tokensUsed.total,
|
|
2744
|
+
estimatedCost: response.cost,
|
|
2745
|
+
reasoningSummary: response.reasoningSummary
|
|
2746
|
+
}), result;
|
|
1542
2747
|
} catch (error) {
|
|
1543
|
-
throw
|
|
2748
|
+
throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
|
|
1544
2749
|
}
|
|
2750
|
+
}, buildTranslationPrompt = ({
|
|
2751
|
+
sourceText,
|
|
2752
|
+
sourceLanguage,
|
|
2753
|
+
targetLanguage,
|
|
2754
|
+
translationPrompts
|
|
2755
|
+
}) => {
|
|
2756
|
+
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2757
|
+
return `# TRANSLATION TASK
|
|
2758
|
+
|
|
2759
|
+
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
2760
|
+
|
|
2761
|
+
## Guidelines
|
|
2762
|
+
|
|
2763
|
+
${globalPrompt ? `### General Guidelines
|
|
2764
|
+
${globalPrompt}
|
|
2765
|
+
` : ""}
|
|
2766
|
+
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2767
|
+
${localePrompt}
|
|
2768
|
+
` : ""}
|
|
2769
|
+
${glossaryTerms ? `### Required Terminology
|
|
2770
|
+
${glossaryTerms}
|
|
2771
|
+
` : ""}
|
|
2772
|
+
${protectedPhrases?.length ? `### Protected Phrases (do not translate)
|
|
2773
|
+
${protectedPhrases.join(", ")}
|
|
2774
|
+
` : ""}
|
|
2775
|
+
|
|
2776
|
+
## Source Document (${sourceLanguage})
|
|
2777
|
+
|
|
2778
|
+
\`\`\`
|
|
2779
|
+
${sourceText}
|
|
2780
|
+
\`\`\`
|
|
2781
|
+
|
|
2782
|
+
## Instructions
|
|
2783
|
+
|
|
2784
|
+
1. Translate all text content into ${targetLanguage}
|
|
2785
|
+
2. Follow the guidelines and terminology requirements above
|
|
2786
|
+
3. Preserve protected phrases exactly as they appear
|
|
2787
|
+
4. Return the document in THE EXACT SAME FORMAT
|
|
2788
|
+
5. **CRITICAL**: Only modify text content between tags
|
|
2789
|
+
6. Never add, remove, or change any tags, attributes, or structure
|
|
2790
|
+
|
|
2791
|
+
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2792
|
+
<reasoning>
|
|
2793
|
+
Brief summary of key translation decisions and reasoning
|
|
2794
|
+
</reasoning>
|
|
2795
|
+
|
|
2796
|
+
Then return the translated document:`;
|
|
2797
|
+
}, buildReviewPrompt = ({
|
|
2798
|
+
sourceText,
|
|
2799
|
+
translatedText,
|
|
2800
|
+
sourceLanguage,
|
|
2801
|
+
targetLanguage,
|
|
2802
|
+
translationPrompts
|
|
2803
|
+
}) => {
|
|
2804
|
+
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2805
|
+
return `# TRANSLATION QUALITY REVIEW
|
|
2806
|
+
|
|
2807
|
+
Review this ${targetLanguage} translation and return a corrected version.
|
|
2808
|
+
|
|
2809
|
+
## Review Criteria
|
|
2810
|
+
|
|
2811
|
+
1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
|
|
2812
|
+
2. **Terminology**: Are glossary terms used correctly?
|
|
2813
|
+
3. **Protected Phrases**: Are protected phrases preserved?
|
|
2814
|
+
4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
|
|
2815
|
+
5. **Style**: Does it follow the style guide?
|
|
2816
|
+
6. **Completeness**: Is all content translated?
|
|
2817
|
+
|
|
2818
|
+
## Guidelines
|
|
2819
|
+
|
|
2820
|
+
${globalPrompt ? `### General Guidelines
|
|
2821
|
+
${globalPrompt}
|
|
2822
|
+
` : ""}
|
|
2823
|
+
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2824
|
+
${localePrompt}
|
|
2825
|
+
` : ""}
|
|
2826
|
+
${glossaryTerms ? `### Required Terminology
|
|
2827
|
+
${glossaryTerms}
|
|
2828
|
+
` : ""}
|
|
2829
|
+
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2830
|
+
${protectedPhrases.join(", ")}
|
|
2831
|
+
` : ""}
|
|
2832
|
+
|
|
2833
|
+
## Source Document (${sourceLanguage})
|
|
2834
|
+
|
|
2835
|
+
\`\`\`
|
|
2836
|
+
${sourceText}
|
|
2837
|
+
\`\`\`
|
|
2838
|
+
|
|
2839
|
+
## Current Translation (${targetLanguage})
|
|
2840
|
+
|
|
2841
|
+
\`\`\`
|
|
2842
|
+
${translatedText}
|
|
2843
|
+
\`\`\`
|
|
2844
|
+
|
|
2845
|
+
## Instructions
|
|
2846
|
+
|
|
2847
|
+
1. Review the translation for quality issues
|
|
2848
|
+
2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
|
|
2849
|
+
3. Keep good translations as-is - only fix what needs fixing
|
|
2850
|
+
4. Return the corrected translation in THE EXACT SAME FORMAT
|
|
2851
|
+
5. **CRITICAL**: Only modify text content between tags
|
|
2852
|
+
6. Never add, remove, or change any tags, attributes, or structure
|
|
2853
|
+
|
|
2854
|
+
If you made corrections or have insights about the translation quality, include a brief summary:
|
|
2855
|
+
<reasoning>
|
|
2856
|
+
Brief summary of review findings and corrections made
|
|
2857
|
+
</reasoning>
|
|
2858
|
+
|
|
2859
|
+
Then return the corrected translation document:`;
|
|
1545
2860
|
}, translateDocument = async ({
|
|
1546
2861
|
dispatch,
|
|
1547
2862
|
client,
|
|
@@ -1550,15 +2865,17 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1550
2865
|
languageFieldName,
|
|
1551
2866
|
shouldOverwriteFreshTranslations,
|
|
1552
2867
|
schemaId,
|
|
1553
|
-
|
|
1554
|
-
|
|
2868
|
+
translationPrompts,
|
|
2869
|
+
translationMode,
|
|
2870
|
+
openaiConfig,
|
|
2871
|
+
releaseId
|
|
1555
2872
|
}) => {
|
|
1556
2873
|
const logContext = {
|
|
1557
2874
|
documentId: sourceDocument._id,
|
|
1558
2875
|
targetLanguage,
|
|
1559
2876
|
schemaId,
|
|
1560
|
-
|
|
1561
|
-
|
|
2877
|
+
shouldOverwriteFreshTranslations,
|
|
2878
|
+
translationMode
|
|
1562
2879
|
};
|
|
1563
2880
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
1564
2881
|
try {
|
|
@@ -1598,48 +2915,20 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1598
2915
|
operation: translationTarget.operation,
|
|
1599
2916
|
isNewTranslation
|
|
1600
2917
|
});
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
devLog.debug("Patching translation target", {
|
|
1616
|
-
...logContext,
|
|
1617
|
-
currentTargetId,
|
|
1618
|
-
hasPatch: !!patch
|
|
1619
|
-
}), newDocument = await translateDocumentInPlace({
|
|
1620
|
-
client,
|
|
1621
|
-
schemaId,
|
|
1622
|
-
dispatch,
|
|
1623
|
-
sourceDocument,
|
|
1624
|
-
languageFieldName,
|
|
1625
|
-
targetLanguage,
|
|
1626
|
-
targetId: currentTargetId,
|
|
1627
|
-
patch
|
|
1628
|
-
});
|
|
1629
|
-
} else
|
|
1630
|
-
devLog.info("Starting full document translation", {
|
|
1631
|
-
...logContext,
|
|
1632
|
-
translationTarget: translationTarget.operation
|
|
1633
|
-
}), newDocument = await translateEntireDocument({
|
|
1634
|
-
dispatch,
|
|
1635
|
-
client,
|
|
1636
|
-
schemaId,
|
|
1637
|
-
sourceDocument,
|
|
1638
|
-
targetLanguage,
|
|
1639
|
-
languageFieldName,
|
|
1640
|
-
translationTarget,
|
|
1641
|
-
translationPrompts
|
|
1642
|
-
});
|
|
2918
|
+
const newDocument = await translateEntireDocument({
|
|
2919
|
+
dispatch,
|
|
2920
|
+
client,
|
|
2921
|
+
schemaId,
|
|
2922
|
+
sourceDocument,
|
|
2923
|
+
targetLanguage,
|
|
2924
|
+
languageFieldName,
|
|
2925
|
+
translationTarget,
|
|
2926
|
+
translationPrompts,
|
|
2927
|
+
translationMode,
|
|
2928
|
+
openaiConfig,
|
|
2929
|
+
releaseId,
|
|
2930
|
+
isNewTranslation
|
|
2931
|
+
});
|
|
1643
2932
|
if (!newDocument)
|
|
1644
2933
|
throw devLog.error("Failed to create translation document", void 0, logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd(), new Error("Failed to create translation document");
|
|
1645
2934
|
return devLog.debug("Translation document created", {
|
|
@@ -1666,30 +2955,6 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1666
2955
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
1667
2956
|
return;
|
|
1668
2957
|
}
|
|
1669
|
-
}, patchTranslationTarget = async ({
|
|
1670
|
-
client,
|
|
1671
|
-
currentSourceDocument,
|
|
1672
|
-
translationMetadata,
|
|
1673
|
-
targetLanguage
|
|
1674
|
-
}) => {
|
|
1675
|
-
const sourceSnapshotString = translationMetadata.translationHistory.find(
|
|
1676
|
-
(h) => undraftId(h.sourceId) === undraftId(currentSourceDocument._id)
|
|
1677
|
-
)?.sourceSnapshot;
|
|
1678
|
-
if (!sourceSnapshotString)
|
|
1679
|
-
return {};
|
|
1680
|
-
const sourceSnapshot = JSON.parse(sourceSnapshotString), targetId = translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref;
|
|
1681
|
-
if (!targetId)
|
|
1682
|
-
return {};
|
|
1683
|
-
const currentTargetId = await client.fetch(
|
|
1684
|
-
'*[_id in $ids] | order((_id match "drafts*") desc)[0]._id',
|
|
1685
|
-
{ ids: [undraftId(targetId), draftId(targetId)] }
|
|
1686
|
-
);
|
|
1687
|
-
if (!currentTargetId)
|
|
1688
|
-
return console.log(`No document found for ${targetId}`), {};
|
|
1689
|
-
const patch = diffPatch(sourceSnapshot, currentSourceDocument, {
|
|
1690
|
-
id: draftId(currentTargetId)
|
|
1691
|
-
});
|
|
1692
|
-
return { currentTargetId, patch };
|
|
1693
2958
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
1694
2959
|
client,
|
|
1695
2960
|
sourceDocument,
|
|
@@ -1700,17 +2965,19 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1700
2965
|
schemaId,
|
|
1701
2966
|
references = [],
|
|
1702
2967
|
maxConcurrentTranslationJobs,
|
|
1703
|
-
|
|
2968
|
+
translationMode,
|
|
2969
|
+
openaiConfig
|
|
1704
2970
|
}) => {
|
|
1705
2971
|
const logContext = {
|
|
1706
2972
|
documentId: sourceDocument._id,
|
|
1707
2973
|
targetLanguages,
|
|
1708
2974
|
referencesCount: references.length,
|
|
1709
2975
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
1710
|
-
|
|
2976
|
+
translationMode,
|
|
1711
2977
|
shouldOverwriteFreshTranslations
|
|
1712
2978
|
};
|
|
1713
2979
|
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
2980
|
+
let releaseId;
|
|
1714
2981
|
try {
|
|
1715
2982
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
1716
2983
|
if (!sourceLanguage)
|
|
@@ -1722,6 +2989,22 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1722
2989
|
type: "SET_OVERALL_STATUS",
|
|
1723
2990
|
payload: "translating"
|
|
1724
2991
|
});
|
|
2992
|
+
const releaseTitle = `[AI Translation] ${sourceDocument.title || sourceDocument._id.split("-").slice(0, 2).join("-")} (${sourceDocument._type}) ${sourceLanguage} \u2192 ${targetLanguages.join(", ")}`, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2993
|
+
devLog.info("Creating Content Release for translation batch", {
|
|
2994
|
+
...logContext,
|
|
2995
|
+
releaseTitle
|
|
2996
|
+
});
|
|
2997
|
+
const { releaseId: newReleaseId } = await client.releases.create({
|
|
2998
|
+
metadata: {
|
|
2999
|
+
title: releaseTitle,
|
|
3000
|
+
description: releaseDescription,
|
|
3001
|
+
releaseType: "asap"
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
releaseId = newReleaseId, devLog.info("Content Release created", {
|
|
3005
|
+
...logContext,
|
|
3006
|
+
releaseId
|
|
3007
|
+
});
|
|
1725
3008
|
const sourceDocuments = [
|
|
1726
3009
|
{
|
|
1727
3010
|
_id: sourceDocument._id,
|
|
@@ -1753,7 +3036,7 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1753
3036
|
}).fetch("*[_id == $id][0]", {
|
|
1754
3037
|
id: documentId
|
|
1755
3038
|
});
|
|
1756
|
-
if (document) {
|
|
3039
|
+
if (document && releaseId) {
|
|
1757
3040
|
const targetDocumentId = await translateDocument({
|
|
1758
3041
|
dispatch,
|
|
1759
3042
|
client,
|
|
@@ -1762,8 +3045,10 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1762
3045
|
languageFieldName,
|
|
1763
3046
|
shouldOverwriteFreshTranslations,
|
|
1764
3047
|
schemaId,
|
|
1765
|
-
|
|
1766
|
-
|
|
3048
|
+
translationPrompts,
|
|
3049
|
+
translationMode,
|
|
3050
|
+
openaiConfig,
|
|
3051
|
+
releaseId
|
|
1767
3052
|
});
|
|
1768
3053
|
targetDocumentId && targetDocumentIds.push(targetDocumentId);
|
|
1769
3054
|
}
|
|
@@ -1811,11 +3096,24 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1811
3096
|
failed
|
|
1812
3097
|
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : (devLog.info("Document set translation completed successfully", logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
1813
3098
|
} catch (error) {
|
|
1814
|
-
|
|
3099
|
+
if (devLog.error(
|
|
1815
3100
|
"translateDocumentSet failed with unexpected error",
|
|
1816
3101
|
error instanceof Error ? error : new Error(String(error)),
|
|
1817
3102
|
logContext
|
|
1818
|
-
),
|
|
3103
|
+
), releaseId)
|
|
3104
|
+
try {
|
|
3105
|
+
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
3106
|
+
...logContext,
|
|
3107
|
+
releaseId
|
|
3108
|
+
});
|
|
3109
|
+
} catch (cleanupError) {
|
|
3110
|
+
devLog.error(
|
|
3111
|
+
"Failed to cleanup release",
|
|
3112
|
+
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
3113
|
+
logContext
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
1819
3117
|
}
|
|
1820
3118
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
1821
3119
|
const {
|
|
@@ -1826,22 +3124,50 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1826
3124
|
references,
|
|
1827
3125
|
selectedTargetLanguages,
|
|
1828
3126
|
shouldOverwriteFreshTranslations,
|
|
1829
|
-
|
|
1830
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" })
|
|
3127
|
+
translationMode
|
|
3128
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), handleTranslate = useCallback(() => {
|
|
3129
|
+
translateDocumentSet({
|
|
3130
|
+
sourceDocument: document,
|
|
3131
|
+
targetLanguages: selectedTargetLanguages,
|
|
3132
|
+
client,
|
|
3133
|
+
dispatch,
|
|
3134
|
+
languageFieldName: config.languageFieldName,
|
|
3135
|
+
schemaId: config.schemaId,
|
|
3136
|
+
references,
|
|
3137
|
+
shouldOverwriteFreshTranslations,
|
|
3138
|
+
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
3139
|
+
maxReferenceDepth: config.maxReferenceDepth,
|
|
3140
|
+
translationMode,
|
|
3141
|
+
openaiConfig: config.openai
|
|
3142
|
+
});
|
|
3143
|
+
}, [
|
|
3144
|
+
document,
|
|
3145
|
+
selectedTargetLanguages,
|
|
3146
|
+
client,
|
|
3147
|
+
dispatch,
|
|
3148
|
+
config.languageFieldName,
|
|
3149
|
+
config.schemaId,
|
|
3150
|
+
config.maxConcurrentTranslationJobs,
|
|
3151
|
+
config.maxReferenceDepth,
|
|
3152
|
+
config.openai,
|
|
3153
|
+
references,
|
|
3154
|
+
shouldOverwriteFreshTranslations,
|
|
3155
|
+
translationMode
|
|
3156
|
+
]), handleSelectAll = useCallback(() => {
|
|
3157
|
+
dispatch({
|
|
3158
|
+
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3159
|
+
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
3160
|
+
});
|
|
3161
|
+
}, [dispatch, config.supportedLanguages, document.language]), handleSelectNone = useCallback(() => {
|
|
3162
|
+
dispatch({
|
|
3163
|
+
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3164
|
+
payload: []
|
|
3165
|
+
});
|
|
3166
|
+
}, [dispatch]);
|
|
1831
3167
|
return /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
1832
3168
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
1833
|
-
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick:
|
|
1834
|
-
|
|
1835
|
-
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
1836
|
-
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
1837
|
-
});
|
|
1838
|
-
}, children: "Select All" }),
|
|
1839
|
-
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: () => {
|
|
1840
|
-
dispatch({
|
|
1841
|
-
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
1842
|
-
payload: []
|
|
1843
|
-
});
|
|
1844
|
-
}, children: "Select None" })
|
|
3169
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
3170
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
1845
3171
|
] }),
|
|
1846
3172
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
|
|
1847
3173
|
/* @__PURE__ */ jsx(Button, { onClick: () => setIsDialogOpen(!1), mode: "ghost", size: 2, children: "Cancel" }),
|
|
@@ -1851,21 +3177,7 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1851
3177
|
mode: "ghost",
|
|
1852
3178
|
tone: "positive",
|
|
1853
3179
|
size: 2,
|
|
1854
|
-
onClick:
|
|
1855
|
-
translateDocumentSet({
|
|
1856
|
-
sourceDocument: document,
|
|
1857
|
-
targetLanguages: selectedTargetLanguages,
|
|
1858
|
-
client,
|
|
1859
|
-
dispatch,
|
|
1860
|
-
languageFieldName: config.languageFieldName,
|
|
1861
|
-
schemaId: config.schemaId,
|
|
1862
|
-
references,
|
|
1863
|
-
shouldOverwriteFreshTranslations,
|
|
1864
|
-
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
1865
|
-
maxReferenceDepth: config.maxReferenceDepth,
|
|
1866
|
-
shouldTranslateDiffs
|
|
1867
|
-
});
|
|
1868
|
-
},
|
|
3180
|
+
onClick: handleTranslate,
|
|
1869
3181
|
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
1870
3182
|
children: "Translate"
|
|
1871
3183
|
}
|
|
@@ -1910,6 +3222,88 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1910
3222
|
documentId: document._id
|
|
1911
3223
|
});
|
|
1912
3224
|
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)) }) });
|
|
3225
|
+
}, TranslationModePanel = () => {
|
|
3226
|
+
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
3227
|
+
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
3228
|
+
return null;
|
|
3229
|
+
if (!openaiValidated)
|
|
3230
|
+
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3231
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Configuration" }),
|
|
3232
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Validating API key..." })
|
|
3233
|
+
] }) });
|
|
3234
|
+
const handleModeChange = (mode) => {
|
|
3235
|
+
dispatch({
|
|
3236
|
+
type: "SET_TRANSLATION_MODE",
|
|
3237
|
+
payload: mode
|
|
3238
|
+
});
|
|
3239
|
+
};
|
|
3240
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3241
|
+
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3242
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
3243
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "GPT-5 Available" })
|
|
3244
|
+
] }),
|
|
3245
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3246
|
+
/* @__PURE__ */ jsx(
|
|
3247
|
+
Button,
|
|
3248
|
+
{
|
|
3249
|
+
type: "button",
|
|
3250
|
+
mode: translationMode === "sanity-only" ? "ghost" : "bleed",
|
|
3251
|
+
tone: translationMode === "sanity-only" ? "primary" : "default",
|
|
3252
|
+
onClick: () => handleModeChange("sanity-only"),
|
|
3253
|
+
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3254
|
+
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-only", readOnly: !0 }),
|
|
3255
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3256
|
+
/* @__PURE__ */ jsx(Text, { children: "Sanity Agent Only" }),
|
|
3257
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Fast, reliable, and included with Sanity." })
|
|
3258
|
+
] })
|
|
3259
|
+
] })
|
|
3260
|
+
}
|
|
3261
|
+
),
|
|
3262
|
+
/* @__PURE__ */ jsx(
|
|
3263
|
+
Button,
|
|
3264
|
+
{
|
|
3265
|
+
type: "button",
|
|
3266
|
+
mode: translationMode === "openai-only" ? "ghost" : "bleed",
|
|
3267
|
+
tone: translationMode === "openai-only" ? "primary" : "default",
|
|
3268
|
+
onClick: () => handleModeChange("openai-only"),
|
|
3269
|
+
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3270
|
+
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
3271
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3272
|
+
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3273
|
+
/* @__PURE__ */ jsx(Text, { children: "GPT-5 Only" }),
|
|
3274
|
+
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
3275
|
+
] }),
|
|
3276
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
3277
|
+
] })
|
|
3278
|
+
] })
|
|
3279
|
+
}
|
|
3280
|
+
),
|
|
3281
|
+
/* @__PURE__ */ jsx(
|
|
3282
|
+
Button,
|
|
3283
|
+
{
|
|
3284
|
+
type: "button",
|
|
3285
|
+
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
3286
|
+
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
3287
|
+
onClick: () => handleModeChange("sanity-with-review"),
|
|
3288
|
+
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3289
|
+
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
3290
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3291
|
+
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3292
|
+
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
3293
|
+
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
3294
|
+
] }),
|
|
3295
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
3296
|
+
] })
|
|
3297
|
+
] })
|
|
3298
|
+
}
|
|
3299
|
+
)
|
|
3300
|
+
] }),
|
|
3301
|
+
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3302
|
+
"\u{1F4B0} ",
|
|
3303
|
+
/* @__PURE__ */ jsx("strong", { children: "OpenAI costs apply:" }),
|
|
3304
|
+
" You'll be charged for API usage based on tokens processed."
|
|
3305
|
+
] }) })
|
|
3306
|
+
] }) });
|
|
1913
3307
|
}, assembleReferences = async ({
|
|
1914
3308
|
client,
|
|
1915
3309
|
sourceDocument,
|
|
@@ -1934,7 +3328,14 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1934
3328
|
typeof refId == "string" && refId.length > 0 && !references.has(refId) && !documentCache.has(refId) && allRefIds.add(refId);
|
|
1935
3329
|
}
|
|
1936
3330
|
} catch (error) {
|
|
1937
|
-
|
|
3331
|
+
devLog.error(
|
|
3332
|
+
"Error finding references in document",
|
|
3333
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3334
|
+
{
|
|
3335
|
+
documentId: document._id,
|
|
3336
|
+
component: "assembleReferences"
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
1938
3339
|
}
|
|
1939
3340
|
if (allRefIds.size === 0)
|
|
1940
3341
|
break;
|
|
@@ -1953,16 +3354,27 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
1953
3354
|
}
|
|
1954
3355
|
documentCache.set(refId, fetchedDoc), references.add(refId), nextDepthDocuments.push(fetchedDoc);
|
|
1955
3356
|
} else
|
|
1956
|
-
documentCache.set(refId, null),
|
|
3357
|
+
documentCache.set(refId, null), devLog.warn(`Document with _id "${refId}" not found`, {
|
|
3358
|
+
refId,
|
|
3359
|
+
component: "assembleReferences"
|
|
3360
|
+
});
|
|
1957
3361
|
}
|
|
1958
3362
|
currentDepthDocuments = nextDepthDocuments;
|
|
1959
3363
|
} catch (fetchError) {
|
|
1960
3364
|
const error = fetchError instanceof Error ? fetchError : new Error(String(fetchError));
|
|
1961
|
-
errors.push({ refId: `batch(${refIdArray.join(",")})`, error }),
|
|
3365
|
+
errors.push({ refId: `batch(${refIdArray.join(",")})`, error }), devLog.error("Failed to batch fetch documents", error, {
|
|
3366
|
+
refIdCount: refIdArray.length,
|
|
3367
|
+
depth,
|
|
3368
|
+
component: "assembleReferences"
|
|
3369
|
+
});
|
|
1962
3370
|
break;
|
|
1963
3371
|
}
|
|
1964
3372
|
}
|
|
1965
|
-
errors.length > 0 &&
|
|
3373
|
+
errors.length > 0 && devLog.warn(`Encountered ${errors.length} error(s) while fetching references`, {
|
|
3374
|
+
errorCount: errors.length,
|
|
3375
|
+
errors: errors.map((e) => ({ refId: e.refId, message: e.error.message })),
|
|
3376
|
+
component: "assembleReferences"
|
|
3377
|
+
});
|
|
1966
3378
|
const result = [];
|
|
1967
3379
|
for (const refId of references) {
|
|
1968
3380
|
const cachedDoc = documentCache.get(refId);
|
|
@@ -2047,7 +3459,7 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2047
3459
|
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
|
|
2048
3460
|
] });
|
|
2049
3461
|
}, WriteSettingsPanel = () => {
|
|
2050
|
-
const { dispatch, shouldOverwriteFreshTranslations
|
|
3462
|
+
const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
|
|
2051
3463
|
dispatch({
|
|
2052
3464
|
type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
|
|
2053
3465
|
payload: !shouldOverwriteFreshTranslations
|
|
@@ -2078,38 +3490,6 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2078
3490
|
] })
|
|
2079
3491
|
] })
|
|
2080
3492
|
}
|
|
2081
|
-
) }),
|
|
2082
|
-
/* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
|
|
2083
|
-
Button,
|
|
2084
|
-
{
|
|
2085
|
-
type: "button",
|
|
2086
|
-
mode: shouldTranslateDiffs ? "ghost" : "bleed",
|
|
2087
|
-
size: 2,
|
|
2088
|
-
onClick: () => {
|
|
2089
|
-
dispatch({
|
|
2090
|
-
type: "SET_SHOULD_TRANSLATE_DIFFS",
|
|
2091
|
-
payload: !shouldTranslateDiffs
|
|
2092
|
-
});
|
|
2093
|
-
},
|
|
2094
|
-
tone: shouldTranslateDiffs ? "primary" : "neutral",
|
|
2095
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
2096
|
-
/* @__PURE__ */ jsx(
|
|
2097
|
-
Checkbox,
|
|
2098
|
-
{
|
|
2099
|
-
checked: shouldTranslateDiffs,
|
|
2100
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
2101
|
-
label: shouldTranslateDiffs ? "Disable" : "Enable"
|
|
2102
|
-
}
|
|
2103
|
-
),
|
|
2104
|
-
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2105
|
-
/* @__PURE__ */ jsxs(Inline, { space: 1, children: [
|
|
2106
|
-
/* @__PURE__ */ jsx(Text, { children: "Translate Diffs" }),
|
|
2107
|
-
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Experimental" })
|
|
2108
|
-
] }),
|
|
2109
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Only re-translate content that has changed in the source document." })
|
|
2110
|
-
] })
|
|
2111
|
-
] })
|
|
2112
|
-
}
|
|
2113
3493
|
) })
|
|
2114
3494
|
] });
|
|
2115
3495
|
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
@@ -2117,16 +3497,24 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2117
3497
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
2118
3498
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
2119
3499
|
] }),
|
|
3500
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3501
|
+
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3502
|
+
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3503
|
+
] }),
|
|
2120
3504
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2121
3505
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
2122
3506
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
2123
3507
|
] }),
|
|
2124
3508
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
2125
3509
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
2126
|
-
const { overallStatus, translationJobs } = useTranslationContext(), headline = useMemo(
|
|
3510
|
+
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
2127
3511
|
() => overallStatus === "completed" ? "Translation completed" : "Translation in progress",
|
|
2128
3512
|
[overallStatus]
|
|
2129
|
-
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs])
|
|
3513
|
+
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
|
|
3514
|
+
if (translationMode === "sanity-only") return null;
|
|
3515
|
+
const completedJobs = jobs.filter((j) => j.status === "completed"), totalTokens = completedJobs.reduce((sum, job) => sum + (job.tokensUsed || 0), 0), totalCost = completedJobs.reduce((sum, job) => sum + (job.estimatedCost || 0), 0);
|
|
3516
|
+
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
3517
|
+
}, [jobs, translationMode]);
|
|
2130
3518
|
return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2131
3519
|
/* @__PURE__ */ jsx(Heading, { size: 2, children: headline }),
|
|
2132
3520
|
/* @__PURE__ */ jsx(ProgressBar, { progress: jobs.filter((job) => TERMINAL_STATUSES.includes(job.status)).length, max: jobs.length }),
|
|
@@ -2146,6 +3534,25 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2146
3534
|
" \u2014 ",
|
|
2147
3535
|
job._id.slice(-6)
|
|
2148
3536
|
] }),
|
|
3537
|
+
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3538
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3539
|
+
job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3540
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
|
|
3541
|
+
/* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
|
|
3542
|
+
] }) })
|
|
3543
|
+
] }),
|
|
3544
|
+
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3545
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3546
|
+
job.tokensUsed.toLocaleString(),
|
|
3547
|
+
" tokens (~$",
|
|
3548
|
+
job.estimatedCost?.toFixed(4),
|
|
3549
|
+
")"
|
|
3550
|
+
] }),
|
|
3551
|
+
job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3552
|
+
"\u{1F4A1} ",
|
|
3553
|
+
job.reasoningSummary
|
|
3554
|
+
] }) })
|
|
3555
|
+
] }) : null,
|
|
2149
3556
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2150
3557
|
"Error: ",
|
|
2151
3558
|
job.errorMessage
|
|
@@ -2166,7 +3573,23 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2166
3573
|
) : null
|
|
2167
3574
|
] })
|
|
2168
3575
|
] }) }) }, job._id)) }) }) })
|
|
2169
|
-
] })
|
|
3576
|
+
] }),
|
|
3577
|
+
openaiStats && openaiStats.jobsWithUsage > 0 && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3578
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3579
|
+
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3580
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3581
|
+
"\u{1F4CA} Total Tokens: ",
|
|
3582
|
+
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3583
|
+
] }),
|
|
3584
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3585
|
+
"\u{1F4B0} Total Cost: ",
|
|
3586
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3587
|
+
"$",
|
|
3588
|
+
openaiStats.totalCost.toFixed(4)
|
|
3589
|
+
] })
|
|
3590
|
+
] })
|
|
3591
|
+
] })
|
|
3592
|
+
] }) })
|
|
2170
3593
|
] });
|
|
2171
3594
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
2172
3595
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -2225,7 +3648,18 @@ const setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages)
|
|
|
2225
3648
|
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
2226
3649
|
children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
|
|
2227
3650
|
}
|
|
2228
|
-
),
|
|
3651
|
+
), getStatusLabel = (status) => ({
|
|
3652
|
+
idle: "Idle",
|
|
3653
|
+
queued: "Queued...",
|
|
3654
|
+
preparing_translation: "Preparing translation...",
|
|
3655
|
+
translating: "Translating with Sanity Agent...",
|
|
3656
|
+
translating_with_openai: "Translating with GPT-5...",
|
|
3657
|
+
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3658
|
+
updating_metadata: "Saving metadata...",
|
|
3659
|
+
error: "Error",
|
|
3660
|
+
skipped: "Skipped",
|
|
3661
|
+
completed: "Completed"
|
|
3662
|
+
})[status] || status, STATES_IN_WHICH_TO_SHOW_CONFIG = ["fetching_metadata", "idle"], STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS = [
|
|
2229
3663
|
"translating",
|
|
2230
3664
|
"linking_references",
|
|
2231
3665
|
"completed"
|