@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 CHANGED
@@ -1605,7 +1605,109 @@ const fetchTranslationPrompts = async ({
1605
1605
  payload
1606
1606
  });
1607
1607
  }
1608
- }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
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), newDocument = await handleTranslation({
2647
- dispatch,
2648
- client,
2649
- schemaId,
2650
- sourceDocument,
2651
- targetLanguage,
2652
- existingTranslationId,
2653
- languageFieldName,
2654
- translationPrompts,
2655
- translationMode,
2656
- openaiConfig,
2657
- releaseId
2658
- });
2659
- if (!newDocument) {
2660
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
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__ */ jsx(Badge, { tone: "critical", children: "Failed" })
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"