@braze/sanity-plugin-ai-translation 0.2.31 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +168 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1605,7 +1605,109 @@ const fetchTranslationPrompts = async ({
|
|
|
1605
1605
|
payload
|
|
1606
1606
|
});
|
|
1607
1607
|
}
|
|
1608
|
-
},
|
|
1608
|
+
}, DEFAULT_OPTIONS = {
|
|
1609
|
+
maxRetries: 3,
|
|
1610
|
+
initialDelayMs: 1e3,
|
|
1611
|
+
maxDelayMs: 3e4,
|
|
1612
|
+
backoffMultiplier: 2,
|
|
1613
|
+
jitter: !0
|
|
1614
|
+
};
|
|
1615
|
+
function isRetryableError(error) {
|
|
1616
|
+
if (error instanceof Error) {
|
|
1617
|
+
const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
|
|
1618
|
+
"schema not found",
|
|
1619
|
+
"invalid schema",
|
|
1620
|
+
"not found",
|
|
1621
|
+
"unauthorized",
|
|
1622
|
+
"forbidden",
|
|
1623
|
+
"invalid api key",
|
|
1624
|
+
"missing api key",
|
|
1625
|
+
"validation failed",
|
|
1626
|
+
"invalid document",
|
|
1627
|
+
"source language not found",
|
|
1628
|
+
"invalid configuration",
|
|
1629
|
+
"type mismatch"
|
|
1630
|
+
];
|
|
1631
|
+
for (const pattern of nonRetryablePatterns)
|
|
1632
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1633
|
+
return !1;
|
|
1634
|
+
const retryablePatterns = [
|
|
1635
|
+
"failed to fetch",
|
|
1636
|
+
"network",
|
|
1637
|
+
"timeout",
|
|
1638
|
+
"econnreset",
|
|
1639
|
+
"econnrefused",
|
|
1640
|
+
"enotfound",
|
|
1641
|
+
"socket",
|
|
1642
|
+
"rate limit",
|
|
1643
|
+
"too many requests",
|
|
1644
|
+
"429",
|
|
1645
|
+
"500",
|
|
1646
|
+
"502",
|
|
1647
|
+
"503",
|
|
1648
|
+
"504",
|
|
1649
|
+
"internal server error",
|
|
1650
|
+
"bad gateway",
|
|
1651
|
+
"service unavailable",
|
|
1652
|
+
"gateway timeout",
|
|
1653
|
+
"temporarily unavailable",
|
|
1654
|
+
"try again"
|
|
1655
|
+
];
|
|
1656
|
+
for (const pattern of retryablePatterns)
|
|
1657
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1658
|
+
return !0;
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof error == "object" && error !== null) {
|
|
1661
|
+
const statusCode = error.statusCode || error.status;
|
|
1662
|
+
if (statusCode) {
|
|
1663
|
+
if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
|
|
1664
|
+
return !0;
|
|
1665
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
1666
|
+
return !1;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return !0;
|
|
1670
|
+
}
|
|
1671
|
+
function calculateBackoffDelay(attempt, options) {
|
|
1672
|
+
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
|
|
1673
|
+
let delay = initialDelayMs * backoffMultiplier ** attempt;
|
|
1674
|
+
if (delay = Math.min(delay, maxDelayMs), jitter) {
|
|
1675
|
+
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
1676
|
+
delay = Math.round(delay * jitterFactor);
|
|
1677
|
+
}
|
|
1678
|
+
return delay;
|
|
1679
|
+
}
|
|
1680
|
+
function sleep(ms) {
|
|
1681
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1682
|
+
}
|
|
1683
|
+
async function withRetry(fn, options = {}) {
|
|
1684
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
|
|
1685
|
+
let lastError, attempts = 0;
|
|
1686
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
1687
|
+
attempts = attempt + 1;
|
|
1688
|
+
try {
|
|
1689
|
+
return { success: !0, result: await fn(), attempts };
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
if (lastError = error, attempt >= opts.maxRetries) {
|
|
1692
|
+
devLog.debug("Max retries exhausted", { attempt: attempts, error });
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
if (!shouldRetry(error)) {
|
|
1696
|
+
devLog.debug("Error is not retryable", { attempt: attempts, error });
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
const delay = calculateBackoffDelay(attempt, opts);
|
|
1700
|
+
devLog.debug("Scheduling retry", {
|
|
1701
|
+
attempt: attempts,
|
|
1702
|
+
nextAttempt: attempt + 2,
|
|
1703
|
+
delayMs: delay,
|
|
1704
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1705
|
+
}), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return { success: !1, error: lastError, attempts };
|
|
1709
|
+
}
|
|
1710
|
+
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1609
1711
|
(translation) => translation._key === targetLanguage
|
|
1610
1712
|
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1611
1713
|
client,
|
|
@@ -2643,23 +2745,50 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2643
2745
|
});
|
|
2644
2746
|
return;
|
|
2645
2747
|
}
|
|
2646
|
-
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage),
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2748
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
|
|
2749
|
+
() => handleTranslation({
|
|
2750
|
+
dispatch,
|
|
2751
|
+
client,
|
|
2752
|
+
schemaId,
|
|
2753
|
+
sourceDocument,
|
|
2754
|
+
targetLanguage,
|
|
2755
|
+
existingTranslationId,
|
|
2756
|
+
languageFieldName,
|
|
2757
|
+
translationPrompts,
|
|
2758
|
+
translationMode,
|
|
2759
|
+
openaiConfig,
|
|
2760
|
+
releaseId
|
|
2761
|
+
}),
|
|
2762
|
+
{
|
|
2763
|
+
maxRetries: 3,
|
|
2764
|
+
initialDelayMs: 1e3,
|
|
2765
|
+
isRetryable: isRetryableError,
|
|
2766
|
+
onRetry: (attempt, error, delayMs) => {
|
|
2767
|
+
devLog.warn("Translation failed, scheduling retry", {
|
|
2768
|
+
...logContext,
|
|
2769
|
+
attempt,
|
|
2770
|
+
delayMs,
|
|
2771
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2772
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
2773
|
+
retryCount: attempt,
|
|
2774
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
);
|
|
2779
|
+
if (!translationResult.success || !translationResult.result) {
|
|
2780
|
+
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
2781
|
+
devLog.error("Translation failed after retries", translationResult.error, {
|
|
2782
|
+
...logContext,
|
|
2783
|
+
attempts: translationResult.attempts
|
|
2784
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2785
|
+
errorMessage,
|
|
2786
|
+
retryCount: translationResult.attempts - 1
|
|
2787
|
+
// -1 because attempts includes initial try
|
|
2788
|
+
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2661
2789
|
return;
|
|
2662
2790
|
}
|
|
2791
|
+
const newDocument = translationResult.result;
|
|
2663
2792
|
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2664
2793
|
targetId: newDocument._id
|
|
2665
2794
|
}), await updateTranslationMetadata({
|
|
@@ -3239,12 +3368,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3239
3368
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3240
3369
|
"Error: ",
|
|
3241
3370
|
job.errorMessage
|
|
3371
|
+
] }) }) : null,
|
|
3372
|
+
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3373
|
+
"Retrying after: ",
|
|
3374
|
+
job.errorMessage
|
|
3242
3375
|
] }) }) : null
|
|
3243
3376
|
] })
|
|
3244
3377
|
] }),
|
|
3245
3378
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3246
3379
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3247
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3380
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
|
|
3381
|
+
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3248
3382
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3249
3383
|
TranslationOpenerButton,
|
|
3250
3384
|
{
|
|
@@ -3324,12 +3458,25 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3324
3458
|
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
|
|
3325
3459
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3326
3460
|
}
|
|
3327
|
-
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3461
|
+
), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3328
3462
|
Tooltip,
|
|
3329
3463
|
{
|
|
3330
3464
|
portal: !0,
|
|
3331
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3332
|
-
children: /* @__PURE__ */
|
|
3465
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
|
|
3466
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3467
|
+
"Failed",
|
|
3468
|
+
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3469
|
+
] })
|
|
3470
|
+
}
|
|
3471
|
+
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3472
|
+
Tooltip,
|
|
3473
|
+
{
|
|
3474
|
+
portal: !0,
|
|
3475
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
|
|
3476
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
|
|
3477
|
+
"Retry ",
|
|
3478
|
+
retryCount || 1
|
|
3479
|
+
] })
|
|
3333
3480
|
}
|
|
3334
3481
|
), getStatusLabel = (status) => ({
|
|
3335
3482
|
idle: "Idle",
|
|
@@ -3339,6 +3486,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3339
3486
|
translating_with_openai: "Translating with GPT-5...",
|
|
3340
3487
|
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3341
3488
|
updating_metadata: "Saving metadata...",
|
|
3489
|
+
retrying: "Retrying...",
|
|
3342
3490
|
error: "Error",
|
|
3343
3491
|
skipped: "Skipped",
|
|
3344
3492
|
completed: "Completed"
|