@lingo.dev/_sdk 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -114,7 +114,13 @@ var engineParamsSchema = _zod2.default.object({
114
114
  apiUrl: _zod2.default.string().url().default("https://api.lingo.dev"),
115
115
  batchSize: _zod2.default.number().int().gt(0).lte(250).default(25),
116
116
  idealBatchItemSize: _zod2.default.number().int().gt(0).lte(2500).default(250),
117
- engineId: _zod2.default.string().optional()
117
+ engineId: _zod2.default.string().optional(),
118
+ // Number of times a localization request is retried after a transient
119
+ // failure (5xx response or network error). `0` disables retries.
120
+ maxRetries: _zod2.default.number().int().gte(0).default(3),
121
+ // Base delay (ms) for the exponential backoff between retries. The actual
122
+ // wait grows as `retryDelayMs * 2 ** attempt` plus a small random jitter.
123
+ retryDelayMs: _zod2.default.number().int().gte(0).default(500)
118
124
  }).passthrough();
119
125
  var normalizedLocaleCodeSchema = __spec.localeCodeSchema.transform(__spec.normalizeLocale);
120
126
  var payloadSchema = _zod2.default.record(_zod2.default.string(), _zod2.default.any());
@@ -129,6 +135,12 @@ var localizationParamsSchema = _zod2.default.object({
129
135
  filePath: _zod2.default.string().optional(),
130
136
  triggerType: _zod2.default.enum(["cli", "ci"]).optional()
131
137
  });
138
+ var estimateItemsSchema = _zod2.default.array(
139
+ _zod2.default.object({
140
+ targetLocale: normalizedLocaleCodeSchema,
141
+ sourceChars: _zod2.default.number().int().nonnegative()
142
+ })
143
+ ).min(1);
132
144
  var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
133
145
 
134
146
  __init() {this.sessionId = _cuid2.createId.call(void 0, )}
@@ -166,6 +178,74 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
166
178
  }
167
179
  throw new Error(context ? `${context}: ${msg}` : msg);
168
180
  }
181
+ /**
182
+ * Sleep for `ms` milliseconds, rejecting early if the signal is aborted.
183
+ */
184
+ static sleep(ms, signal) {
185
+ return new Promise((resolve, reject) => {
186
+ if (_optionalChain([signal, 'optionalAccess', _4 => _4.aborted])) {
187
+ reject(new Error("Operation was aborted"));
188
+ return;
189
+ }
190
+ const onAbort = () => {
191
+ clearTimeout(timer);
192
+ reject(new Error("Operation was aborted"));
193
+ };
194
+ const timer = setTimeout(() => {
195
+ _optionalChain([signal, 'optionalAccess', _5 => _5.removeEventListener, 'call', _6 => _6("abort", onAbort)]);
196
+ resolve();
197
+ }, ms);
198
+ _optionalChain([signal, 'optionalAccess', _7 => _7.addEventListener, 'call', _8 => _8("abort", onAbort, { once: true })]);
199
+ });
200
+ }
201
+ /**
202
+ * Exponential backoff with full jitter: a random delay in
203
+ * `[0, retryDelayMs * 2 ** attempt]`. Jitter spreads out retries from many
204
+ * clients so a recovering server is not hit by a synchronized wave.
205
+ */
206
+ backoffDelay(attempt) {
207
+ const ceiling = this.config.retryDelayMs * 2 ** attempt;
208
+ return Math.round(Math.random() * ceiling);
209
+ }
210
+ /**
211
+ * Perform a fetch, retrying on transient failures (5xx responses and
212
+ * network errors) with exponential backoff. The retry decision is made on
213
+ * the HTTP status code (>= 500), so non-retryable responses (e.g. 4xx) are
214
+ * returned immediately for the caller to handle. Aborted requests are never
215
+ * retried.
216
+ * @param url - The request URL
217
+ * @param init - Fetch init options (should include the AbortSignal)
218
+ * @param signal - Optional AbortSignal used to short-circuit retries
219
+ * @returns The fetch Response (which may still be a non-retryable error)
220
+ */
221
+ async fetchWithRetry(url, init, signal) {
222
+ const { maxRetries } = this.config;
223
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
224
+ if (_optionalChain([signal, 'optionalAccess', _9 => _9.aborted])) {
225
+ throw new Error("Operation was aborted");
226
+ }
227
+ try {
228
+ const res = await fetch(url, init);
229
+ const isServerError = res.status >= 500 && res.status < 600;
230
+ if (isServerError && attempt < maxRetries) {
231
+ await _optionalChain([res, 'access', _10 => _10.body, 'optionalAccess', _11 => _11.cancel, 'call', _12 => _12()]);
232
+ await _LingoDotDevEngine.sleep(this.backoffDelay(attempt), signal);
233
+ continue;
234
+ }
235
+ return res;
236
+ } catch (error) {
237
+ if (_optionalChain([signal, 'optionalAccess', _13 => _13.aborted])) {
238
+ throw error;
239
+ }
240
+ if (attempt < maxRetries) {
241
+ await _LingoDotDevEngine.sleep(this.backoffDelay(attempt), signal);
242
+ continue;
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+ throw new Error("Localization request failed after exhausting retries");
248
+ }
169
249
  /**
170
250
  * Create a new LingoDotDevEngine instance
171
251
  * @param config - Configuration options for the Engine
@@ -233,12 +313,16 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
233
313
  metadata: filePath ? { filePath } : void 0,
234
314
  ...this.config.engineId && { engineId: this.config.engineId }
235
315
  };
236
- const res = await fetch(url, {
237
- method: "POST",
238
- headers: this.headers,
239
- body: JSON.stringify(body, null, 2),
316
+ const res = await this.fetchWithRetry(
317
+ url,
318
+ {
319
+ method: "POST",
320
+ headers: this.headers,
321
+ body: JSON.stringify(body, null, 2),
322
+ signal
323
+ },
240
324
  signal
241
- });
325
+ );
242
326
  await _LingoDotDevEngine.throwOnHttpError(res);
243
327
  const jsonResponse = await res.json();
244
328
  if (!jsonResponse.data && jsonResponse.error) {
@@ -579,7 +663,7 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
579
663
  break;
580
664
  }
581
665
  const siblings = Array.from(parent.childNodes).filter(
582
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _4 => _4.textContent, 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()])
666
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _14 => _14.textContent, 'optionalAccess', _15 => _15.trim, 'call', _16 => _16()])
583
667
  );
584
668
  const index = siblings.indexOf(current);
585
669
  if (index !== -1) {
@@ -599,7 +683,7 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
599
683
  parent = parent.parentElement;
600
684
  }
601
685
  if (node.nodeType === 3) {
602
- const text = _optionalChain([node, 'access', _7 => _7.textContent, 'optionalAccess', _8 => _8.trim, 'call', _9 => _9()]) || "";
686
+ const text = _optionalChain([node, 'access', _17 => _17.textContent, 'optionalAccess', _18 => _18.trim, 'call', _19 => _19()]) || "";
603
687
  if (text) {
604
688
  extractedContent[getPath(node)] = text;
605
689
  }
@@ -614,15 +698,15 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
614
698
  }
615
699
  });
616
700
  Array.from(element.childNodes).filter(
617
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _10 => _10.textContent, 'optionalAccess', _11 => _11.trim, 'call', _12 => _12()])
701
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _20 => _20.textContent, 'optionalAccess', _21 => _21.trim, 'call', _22 => _22()])
618
702
  ).forEach(processNode);
619
703
  }
620
704
  };
621
705
  Array.from(document.head.childNodes).filter(
622
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _13 => _13.textContent, 'optionalAccess', _14 => _14.trim, 'call', _15 => _15()])
706
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _23 => _23.textContent, 'optionalAccess', _24 => _24.trim, 'call', _25 => _25()])
623
707
  ).forEach(processNode);
624
708
  Array.from(document.body.childNodes).filter(
625
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _16 => _16.textContent, 'optionalAccess', _17 => _17.trim, 'call', _18 => _18()])
709
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _26 => _26.textContent, 'optionalAccess', _27 => _27.trim, 'call', _28 => _28()])
626
710
  ).forEach(processNode);
627
711
  const localizedContent = await this._localizeRaw(
628
712
  extractedContent,
@@ -638,10 +722,10 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
638
722
  let current = parent;
639
723
  for (const index of indices) {
640
724
  const siblings = Array.from(parent.childNodes).filter(
641
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _19 => _19.textContent, 'optionalAccess', _20 => _20.trim, 'call', _21 => _21()])
725
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _29 => _29.textContent, 'optionalAccess', _30 => _30.trim, 'call', _31 => _31()])
642
726
  );
643
727
  current = siblings[parseInt(index)] || null;
644
- if (_optionalChain([current, 'optionalAccess', _22 => _22.nodeType]) === 1) {
728
+ if (_optionalChain([current, 'optionalAccess', _32 => _32.nodeType]) === 1) {
645
729
  parent = current;
646
730
  }
647
731
  }
@@ -721,6 +805,27 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
721
805
  throw error;
722
806
  }
723
807
  }
808
+ /**
809
+ * Estimate the cost of localizing content BEFORE submitting it.
810
+ * Pure computation server-side — nothing is translated, stored, or billed.
811
+ * @param items - Per-target-locale character counts of translatable source
812
+ * text (sum of source string lengths, excluding keys and markup). Duplicate
813
+ * locales are summed by the server.
814
+ * @param signal - Optional AbortSignal to cancel the operation
815
+ * @returns Promise resolving to an approximate cost with per-locale breakdown
816
+ */
817
+ async estimate(items, signal) {
818
+ const parsedItems = estimateItemsSchema.parse(items);
819
+ const url = `${this.config.apiUrl}/process/estimate`;
820
+ const res = await fetch(url, {
821
+ method: "POST",
822
+ headers: this.headers,
823
+ body: JSON.stringify({ items: parsedItems }),
824
+ signal
825
+ });
826
+ await _LingoDotDevEngine.throwOnHttpError(res, "Error estimating cost");
827
+ return res.json();
828
+ }
724
829
  async whoami(signal) {
725
830
  const url = `${this.config.apiUrl}/users/me`;
726
831
  const res = await fetch(url, {
@@ -730,7 +835,7 @@ var LingoDotDevEngine = (_class = class _LingoDotDevEngine {
730
835
  });
731
836
  if (res.ok) {
732
837
  const payload = await res.json();
733
- if (!_optionalChain([payload, 'optionalAccess', _23 => _23.email])) {
838
+ if (!_optionalChain([payload, 'optionalAccess', _33 => _33.email])) {
734
839
  return null;
735
840
  }
736
841
  return {
package/build/index.d.cts CHANGED
@@ -7,6 +7,8 @@ declare const engineParamsSchema: Z.ZodObject<{
7
7
  batchSize: Z.ZodDefault<Z.ZodNumber>;
8
8
  idealBatchItemSize: Z.ZodDefault<Z.ZodNumber>;
9
9
  engineId: Z.ZodOptional<Z.ZodString>;
10
+ maxRetries: Z.ZodDefault<Z.ZodNumber>;
11
+ retryDelayMs: Z.ZodDefault<Z.ZodNumber>;
10
12
  }, Z.core.$loose>;
11
13
  declare const payloadSchema: Z.ZodRecord<Z.ZodString, Z.ZodAny>;
12
14
  declare const localizationParamsSchema: Z.ZodObject<{
@@ -21,6 +23,27 @@ declare const localizationParamsSchema: Z.ZodObject<{
21
23
  ci: "ci";
22
24
  }>>;
23
25
  }, Z.core.$strip>;
26
+ /**
27
+ * Approximate localization cost returned by `/process/estimate`.
28
+ * `approximate` is always true — the estimate is a chars→tokens heuristic,
29
+ * not a quote. Actual cost may differ.
30
+ */
31
+ type CostEstimate = {
32
+ approximate: boolean;
33
+ totals: {
34
+ sourceChars: number;
35
+ estimatedOutputTokens: number;
36
+ estimatedLlmCostUsd: number;
37
+ estimatedLocalizationCostUsd: number;
38
+ estimatedTotalCostUsd: number;
39
+ };
40
+ byLocale: {
41
+ targetLocale: string;
42
+ sourceChars: number;
43
+ estimatedOutputTokens: number;
44
+ estimatedCostUsd: number;
45
+ }[];
46
+ };
24
47
  /**
25
48
  * LingoDotDevEngine class for interacting with the LingoDotDev API
26
49
  * A powerful localization engine that supports various content types including
@@ -32,6 +55,28 @@ declare class LingoDotDevEngine {
32
55
  private get headers();
33
56
  private static extractErrorMessage;
34
57
  private static throwOnHttpError;
58
+ /**
59
+ * Sleep for `ms` milliseconds, rejecting early if the signal is aborted.
60
+ */
61
+ private static sleep;
62
+ /**
63
+ * Exponential backoff with full jitter: a random delay in
64
+ * `[0, retryDelayMs * 2 ** attempt]`. Jitter spreads out retries from many
65
+ * clients so a recovering server is not hit by a synchronized wave.
66
+ */
67
+ private backoffDelay;
68
+ /**
69
+ * Perform a fetch, retrying on transient failures (5xx responses and
70
+ * network errors) with exponential backoff. The retry decision is made on
71
+ * the HTTP status code (>= 500), so non-retryable responses (e.g. 4xx) are
72
+ * returned immediately for the caller to handle. Aborted requests are never
73
+ * retried.
74
+ * @param url - The request URL
75
+ * @param init - Fetch init options (should include the AbortSignal)
76
+ * @param signal - Optional AbortSignal used to short-circuit retries
77
+ * @returns The fetch Response (which may still be a non-retryable error)
78
+ */
79
+ private fetchWithRetry;
35
80
  /**
36
81
  * Create a new LingoDotDevEngine instance
37
82
  * @param config - Configuration options for the Engine
@@ -158,6 +203,19 @@ declare class LingoDotDevEngine {
158
203
  * @returns Promise resolving to a locale code (e.g., 'en', 'es', 'fr')
159
204
  */
160
205
  recognizeLocale(text: string, signal?: AbortSignal): Promise<LocaleCode>;
206
+ /**
207
+ * Estimate the cost of localizing content BEFORE submitting it.
208
+ * Pure computation server-side — nothing is translated, stored, or billed.
209
+ * @param items - Per-target-locale character counts of translatable source
210
+ * text (sum of source string lengths, excluding keys and markup). Duplicate
211
+ * locales are summed by the server.
212
+ * @param signal - Optional AbortSignal to cancel the operation
213
+ * @returns Promise resolving to an approximate cost with per-locale breakdown
214
+ */
215
+ estimate(items: {
216
+ targetLocale: string;
217
+ sourceChars: number;
218
+ }[], signal?: AbortSignal): Promise<CostEstimate>;
161
219
  whoami(signal?: AbortSignal): Promise<{
162
220
  email: string;
163
221
  id: string;
@@ -178,4 +236,4 @@ declare class LingoEngine extends LingoDotDevEngine {
178
236
  constructor(config: Partial<Z.infer<typeof engineParamsSchema>>);
179
237
  }
180
238
 
181
- export { LingoDotDevEngine, LingoEngine, ReplexicaEngine };
239
+ export { type CostEstimate, LingoDotDevEngine, LingoEngine, ReplexicaEngine };
package/build/index.d.ts CHANGED
@@ -7,6 +7,8 @@ declare const engineParamsSchema: Z.ZodObject<{
7
7
  batchSize: Z.ZodDefault<Z.ZodNumber>;
8
8
  idealBatchItemSize: Z.ZodDefault<Z.ZodNumber>;
9
9
  engineId: Z.ZodOptional<Z.ZodString>;
10
+ maxRetries: Z.ZodDefault<Z.ZodNumber>;
11
+ retryDelayMs: Z.ZodDefault<Z.ZodNumber>;
10
12
  }, Z.core.$loose>;
11
13
  declare const payloadSchema: Z.ZodRecord<Z.ZodString, Z.ZodAny>;
12
14
  declare const localizationParamsSchema: Z.ZodObject<{
@@ -21,6 +23,27 @@ declare const localizationParamsSchema: Z.ZodObject<{
21
23
  ci: "ci";
22
24
  }>>;
23
25
  }, Z.core.$strip>;
26
+ /**
27
+ * Approximate localization cost returned by `/process/estimate`.
28
+ * `approximate` is always true — the estimate is a chars→tokens heuristic,
29
+ * not a quote. Actual cost may differ.
30
+ */
31
+ type CostEstimate = {
32
+ approximate: boolean;
33
+ totals: {
34
+ sourceChars: number;
35
+ estimatedOutputTokens: number;
36
+ estimatedLlmCostUsd: number;
37
+ estimatedLocalizationCostUsd: number;
38
+ estimatedTotalCostUsd: number;
39
+ };
40
+ byLocale: {
41
+ targetLocale: string;
42
+ sourceChars: number;
43
+ estimatedOutputTokens: number;
44
+ estimatedCostUsd: number;
45
+ }[];
46
+ };
24
47
  /**
25
48
  * LingoDotDevEngine class for interacting with the LingoDotDev API
26
49
  * A powerful localization engine that supports various content types including
@@ -32,6 +55,28 @@ declare class LingoDotDevEngine {
32
55
  private get headers();
33
56
  private static extractErrorMessage;
34
57
  private static throwOnHttpError;
58
+ /**
59
+ * Sleep for `ms` milliseconds, rejecting early if the signal is aborted.
60
+ */
61
+ private static sleep;
62
+ /**
63
+ * Exponential backoff with full jitter: a random delay in
64
+ * `[0, retryDelayMs * 2 ** attempt]`. Jitter spreads out retries from many
65
+ * clients so a recovering server is not hit by a synchronized wave.
66
+ */
67
+ private backoffDelay;
68
+ /**
69
+ * Perform a fetch, retrying on transient failures (5xx responses and
70
+ * network errors) with exponential backoff. The retry decision is made on
71
+ * the HTTP status code (>= 500), so non-retryable responses (e.g. 4xx) are
72
+ * returned immediately for the caller to handle. Aborted requests are never
73
+ * retried.
74
+ * @param url - The request URL
75
+ * @param init - Fetch init options (should include the AbortSignal)
76
+ * @param signal - Optional AbortSignal used to short-circuit retries
77
+ * @returns The fetch Response (which may still be a non-retryable error)
78
+ */
79
+ private fetchWithRetry;
35
80
  /**
36
81
  * Create a new LingoDotDevEngine instance
37
82
  * @param config - Configuration options for the Engine
@@ -158,6 +203,19 @@ declare class LingoDotDevEngine {
158
203
  * @returns Promise resolving to a locale code (e.g., 'en', 'es', 'fr')
159
204
  */
160
205
  recognizeLocale(text: string, signal?: AbortSignal): Promise<LocaleCode>;
206
+ /**
207
+ * Estimate the cost of localizing content BEFORE submitting it.
208
+ * Pure computation server-side — nothing is translated, stored, or billed.
209
+ * @param items - Per-target-locale character counts of translatable source
210
+ * text (sum of source string lengths, excluding keys and markup). Duplicate
211
+ * locales are summed by the server.
212
+ * @param signal - Optional AbortSignal to cancel the operation
213
+ * @returns Promise resolving to an approximate cost with per-locale breakdown
214
+ */
215
+ estimate(items: {
216
+ targetLocale: string;
217
+ sourceChars: number;
218
+ }[], signal?: AbortSignal): Promise<CostEstimate>;
161
219
  whoami(signal?: AbortSignal): Promise<{
162
220
  email: string;
163
221
  id: string;
@@ -178,4 +236,4 @@ declare class LingoEngine extends LingoDotDevEngine {
178
236
  constructor(config: Partial<Z.infer<typeof engineParamsSchema>>);
179
237
  }
180
238
 
181
- export { LingoDotDevEngine, LingoEngine, ReplexicaEngine };
239
+ export { type CostEstimate, LingoDotDevEngine, LingoEngine, ReplexicaEngine };
package/build/index.mjs CHANGED
@@ -114,7 +114,13 @@ var engineParamsSchema = Z.object({
114
114
  apiUrl: Z.string().url().default("https://api.lingo.dev"),
115
115
  batchSize: Z.number().int().gt(0).lte(250).default(25),
116
116
  idealBatchItemSize: Z.number().int().gt(0).lte(2500).default(250),
117
- engineId: Z.string().optional()
117
+ engineId: Z.string().optional(),
118
+ // Number of times a localization request is retried after a transient
119
+ // failure (5xx response or network error). `0` disables retries.
120
+ maxRetries: Z.number().int().gte(0).default(3),
121
+ // Base delay (ms) for the exponential backoff between retries. The actual
122
+ // wait grows as `retryDelayMs * 2 ** attempt` plus a small random jitter.
123
+ retryDelayMs: Z.number().int().gte(0).default(500)
118
124
  }).passthrough();
119
125
  var normalizedLocaleCodeSchema = localeCodeSchema.transform(normalizeLocale);
120
126
  var payloadSchema = Z.record(Z.string(), Z.any());
@@ -129,6 +135,12 @@ var localizationParamsSchema = Z.object({
129
135
  filePath: Z.string().optional(),
130
136
  triggerType: Z.enum(["cli", "ci"]).optional()
131
137
  });
138
+ var estimateItemsSchema = Z.array(
139
+ Z.object({
140
+ targetLocale: normalizedLocaleCodeSchema,
141
+ sourceChars: Z.number().int().nonnegative()
142
+ })
143
+ ).min(1);
132
144
  var LingoDotDevEngine = class _LingoDotDevEngine {
133
145
  config;
134
146
  sessionId = createId();
@@ -166,6 +178,74 @@ var LingoDotDevEngine = class _LingoDotDevEngine {
166
178
  }
167
179
  throw new Error(context ? `${context}: ${msg}` : msg);
168
180
  }
181
+ /**
182
+ * Sleep for `ms` milliseconds, rejecting early if the signal is aborted.
183
+ */
184
+ static sleep(ms, signal) {
185
+ return new Promise((resolve, reject) => {
186
+ if (signal?.aborted) {
187
+ reject(new Error("Operation was aborted"));
188
+ return;
189
+ }
190
+ const onAbort = () => {
191
+ clearTimeout(timer);
192
+ reject(new Error("Operation was aborted"));
193
+ };
194
+ const timer = setTimeout(() => {
195
+ signal?.removeEventListener("abort", onAbort);
196
+ resolve();
197
+ }, ms);
198
+ signal?.addEventListener("abort", onAbort, { once: true });
199
+ });
200
+ }
201
+ /**
202
+ * Exponential backoff with full jitter: a random delay in
203
+ * `[0, retryDelayMs * 2 ** attempt]`. Jitter spreads out retries from many
204
+ * clients so a recovering server is not hit by a synchronized wave.
205
+ */
206
+ backoffDelay(attempt) {
207
+ const ceiling = this.config.retryDelayMs * 2 ** attempt;
208
+ return Math.round(Math.random() * ceiling);
209
+ }
210
+ /**
211
+ * Perform a fetch, retrying on transient failures (5xx responses and
212
+ * network errors) with exponential backoff. The retry decision is made on
213
+ * the HTTP status code (>= 500), so non-retryable responses (e.g. 4xx) are
214
+ * returned immediately for the caller to handle. Aborted requests are never
215
+ * retried.
216
+ * @param url - The request URL
217
+ * @param init - Fetch init options (should include the AbortSignal)
218
+ * @param signal - Optional AbortSignal used to short-circuit retries
219
+ * @returns The fetch Response (which may still be a non-retryable error)
220
+ */
221
+ async fetchWithRetry(url, init, signal) {
222
+ const { maxRetries } = this.config;
223
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
224
+ if (signal?.aborted) {
225
+ throw new Error("Operation was aborted");
226
+ }
227
+ try {
228
+ const res = await fetch(url, init);
229
+ const isServerError = res.status >= 500 && res.status < 600;
230
+ if (isServerError && attempt < maxRetries) {
231
+ await res.body?.cancel();
232
+ await _LingoDotDevEngine.sleep(this.backoffDelay(attempt), signal);
233
+ continue;
234
+ }
235
+ return res;
236
+ } catch (error) {
237
+ if (signal?.aborted) {
238
+ throw error;
239
+ }
240
+ if (attempt < maxRetries) {
241
+ await _LingoDotDevEngine.sleep(this.backoffDelay(attempt), signal);
242
+ continue;
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+ throw new Error("Localization request failed after exhausting retries");
248
+ }
169
249
  /**
170
250
  * Create a new LingoDotDevEngine instance
171
251
  * @param config - Configuration options for the Engine
@@ -233,12 +313,16 @@ var LingoDotDevEngine = class _LingoDotDevEngine {
233
313
  metadata: filePath ? { filePath } : void 0,
234
314
  ...this.config.engineId && { engineId: this.config.engineId }
235
315
  };
236
- const res = await fetch(url, {
237
- method: "POST",
238
- headers: this.headers,
239
- body: JSON.stringify(body, null, 2),
316
+ const res = await this.fetchWithRetry(
317
+ url,
318
+ {
319
+ method: "POST",
320
+ headers: this.headers,
321
+ body: JSON.stringify(body, null, 2),
322
+ signal
323
+ },
240
324
  signal
241
- });
325
+ );
242
326
  await _LingoDotDevEngine.throwOnHttpError(res);
243
327
  const jsonResponse = await res.json();
244
328
  if (!jsonResponse.data && jsonResponse.error) {
@@ -721,6 +805,27 @@ var LingoDotDevEngine = class _LingoDotDevEngine {
721
805
  throw error;
722
806
  }
723
807
  }
808
+ /**
809
+ * Estimate the cost of localizing content BEFORE submitting it.
810
+ * Pure computation server-side — nothing is translated, stored, or billed.
811
+ * @param items - Per-target-locale character counts of translatable source
812
+ * text (sum of source string lengths, excluding keys and markup). Duplicate
813
+ * locales are summed by the server.
814
+ * @param signal - Optional AbortSignal to cancel the operation
815
+ * @returns Promise resolving to an approximate cost with per-locale breakdown
816
+ */
817
+ async estimate(items, signal) {
818
+ const parsedItems = estimateItemsSchema.parse(items);
819
+ const url = `${this.config.apiUrl}/process/estimate`;
820
+ const res = await fetch(url, {
821
+ method: "POST",
822
+ headers: this.headers,
823
+ body: JSON.stringify({ items: parsedItems }),
824
+ signal
825
+ });
826
+ await _LingoDotDevEngine.throwOnHttpError(res, "Error estimating cost");
827
+ return res.json();
828
+ }
724
829
  async whoami(signal) {
725
830
  const url = `${this.config.apiUrl}/users/me`;
726
831
  const res = await fetch(url, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingo.dev/_sdk",
3
- "version": "0.16.4",
3
+ "version": "0.17.0",
4
4
  "description": "Lingo.dev JS SDK",
5
5
  "private": false,
6
6
  "repository": {