@lingo.dev/_sdk 0.14.1 → 0.15.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.
Files changed (3) hide show
  1. package/build/index.cjs +410 -134
  2. package/build/index.mjs +409 -133
  3. package/package.json +3 -2
package/build/index.cjs CHANGED
@@ -2,6 +2,97 @@
2
2
  var _zod = require('zod'); var _zod2 = _interopRequireDefault(_zod);
3
3
  var __spec = require('@lingo.dev/_spec');
4
4
  var _cuid2 = require('@paralleldrive/cuid2');
5
+
6
+ // src/utils/observability.ts
7
+ var _crypto = require('crypto');
8
+
9
+ // src/utils/tracking-events.ts
10
+ var TRACKING_EVENTS = {
11
+ LOCALIZE_START: "sdk.localize.start",
12
+ LOCALIZE_SUCCESS: "sdk.localize.success",
13
+ LOCALIZE_ERROR: "sdk.localize.error",
14
+ RECOGNIZE_START: "sdk.recognize.start",
15
+ RECOGNIZE_SUCCESS: "sdk.recognize.success",
16
+ RECOGNIZE_ERROR: "sdk.recognize.error"
17
+ };
18
+ var TRACKING_VERSION = "1.0";
19
+ var SDK_PACKAGE = "@lingo.dev/_sdk";
20
+
21
+ // src/utils/observability.ts
22
+ var POSTHOG_API_KEY = "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk";
23
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
24
+ var identityCache = /* @__PURE__ */ new Map();
25
+ function trackEvent(apiKey, apiUrl, event, properties) {
26
+ if (process.env.DO_NOT_TRACK === "1") {
27
+ return;
28
+ }
29
+ resolveIdentityAndCapture(apiKey, apiUrl, event, properties).catch(
30
+ (error) => {
31
+ if (process.env.DEBUG === "true") {
32
+ console.error("[Tracking] Error:", error);
33
+ }
34
+ }
35
+ );
36
+ }
37
+ async function resolveIdentityAndCapture(apiKey, apiUrl, event, properties) {
38
+ const identityInfo = await getDistinctId(apiKey, apiUrl);
39
+ if (process.env.DEBUG === "true") {
40
+ console.log(
41
+ `[Tracking] Event: ${event}, ID: ${identityInfo.distinct_id}, Source: ${identityInfo.distinct_id_source}`
42
+ );
43
+ }
44
+ const { PostHog } = await Promise.resolve().then(() => _interopRequireWildcard(require("posthog-node")));
45
+ const posthog = new PostHog(POSTHOG_API_KEY, {
46
+ host: POSTHOG_HOST,
47
+ flushAt: 1,
48
+ flushInterval: 0
49
+ });
50
+ await posthog.capture({
51
+ distinctId: identityInfo.distinct_id,
52
+ event,
53
+ properties: {
54
+ ...properties,
55
+ tracking_version: TRACKING_VERSION,
56
+ sdk_package: SDK_PACKAGE,
57
+ distinct_id_source: identityInfo.distinct_id_source
58
+ }
59
+ });
60
+ await posthog.shutdown();
61
+ }
62
+ async function getDistinctId(apiKey, apiUrl) {
63
+ const cached = identityCache.get(apiKey);
64
+ if (cached) return cached;
65
+ try {
66
+ const res = await fetch(`${apiUrl}/whoami`, {
67
+ method: "POST",
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ "Content-Type": "application/json"
71
+ }
72
+ });
73
+ if (res.ok) {
74
+ const payload = await res.json();
75
+ if (_optionalChain([payload, 'optionalAccess', _ => _.email])) {
76
+ const identity2 = {
77
+ distinct_id: payload.email,
78
+ distinct_id_source: "email"
79
+ };
80
+ identityCache.set(apiKey, identity2);
81
+ return identity2;
82
+ }
83
+ }
84
+ } catch (e) {
85
+ }
86
+ const hash = _crypto.createHash.call(void 0, "sha256").update(apiKey).digest("hex").slice(0, 16);
87
+ const identity = {
88
+ distinct_id: `apikey-${hash}`,
89
+ distinct_id_source: "api_key_hash"
90
+ };
91
+ identityCache.set(apiKey, identity);
92
+ return identity;
93
+ }
94
+
95
+ // src/index.ts
5
96
  var engineParamsSchema = _zod2.default.object({
6
97
  apiKey: _zod2.default.string(),
7
98
  apiUrl: _zod2.default.string().url().default("https://engine.lingo.dev"),
@@ -171,7 +262,43 @@ var LingoDotDevEngine = class {
171
262
  * @returns A new object with the same structure but localized string values
172
263
  */
173
264
  async localizeObject(obj, params, progressCallback, signal) {
174
- return this._localizeRaw(obj, params, progressCallback, signal);
265
+ const trackProps = {
266
+ method: "localizeObject",
267
+ sourceLocale: params.sourceLocale,
268
+ targetLocale: params.targetLocale
269
+ };
270
+ trackEvent(
271
+ this.config.apiKey,
272
+ this.config.apiUrl,
273
+ TRACKING_EVENTS.LOCALIZE_START,
274
+ trackProps
275
+ );
276
+ try {
277
+ const result = await this._localizeRaw(
278
+ obj,
279
+ params,
280
+ progressCallback,
281
+ signal
282
+ );
283
+ trackEvent(
284
+ this.config.apiKey,
285
+ this.config.apiUrl,
286
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
287
+ trackProps
288
+ );
289
+ return result;
290
+ } catch (error) {
291
+ trackEvent(
292
+ this.config.apiKey,
293
+ this.config.apiUrl,
294
+ TRACKING_EVENTS.LOCALIZE_ERROR,
295
+ {
296
+ ...trackProps,
297
+ errorMessage: error instanceof Error ? error.message : String(error)
298
+ }
299
+ );
300
+ throw error;
301
+ }
175
302
  }
176
303
  /**
177
304
  * Localize a single text string
@@ -185,13 +312,44 @@ var LingoDotDevEngine = class {
185
312
  * @returns The localized text string
186
313
  */
187
314
  async localizeText(text, params, progressCallback, signal) {
188
- const response = await this._localizeRaw(
189
- { text },
190
- params,
191
- progressCallback,
192
- signal
315
+ const trackProps = {
316
+ method: "localizeText",
317
+ sourceLocale: params.sourceLocale,
318
+ targetLocale: params.targetLocale
319
+ };
320
+ trackEvent(
321
+ this.config.apiKey,
322
+ this.config.apiUrl,
323
+ TRACKING_EVENTS.LOCALIZE_START,
324
+ trackProps
193
325
  );
194
- return response.text || "";
326
+ try {
327
+ const response = await this._localizeRaw(
328
+ { text },
329
+ params,
330
+ progressCallback,
331
+ signal
332
+ );
333
+ const result = response.text || "";
334
+ trackEvent(
335
+ this.config.apiKey,
336
+ this.config.apiUrl,
337
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
338
+ trackProps
339
+ );
340
+ return result;
341
+ } catch (error) {
342
+ trackEvent(
343
+ this.config.apiKey,
344
+ this.config.apiUrl,
345
+ TRACKING_EVENTS.LOCALIZE_ERROR,
346
+ {
347
+ ...trackProps,
348
+ errorMessage: error instanceof Error ? error.message : String(error)
349
+ }
350
+ );
351
+ throw error;
352
+ }
195
353
  }
196
354
  /**
197
355
  * Localize a text string to multiple target locales
@@ -230,15 +388,45 @@ var LingoDotDevEngine = class {
230
388
  * @returns An array of localized strings in the same order
231
389
  */
232
390
  async localizeStringArray(strings, params) {
233
- const mapped = strings.reduce(
234
- (acc, str, i) => {
235
- acc[`item_${i}`] = str;
236
- return acc;
237
- },
238
- {}
391
+ const trackProps = {
392
+ method: "localizeStringArray",
393
+ sourceLocale: params.sourceLocale,
394
+ targetLocale: params.targetLocale
395
+ };
396
+ trackEvent(
397
+ this.config.apiKey,
398
+ this.config.apiUrl,
399
+ TRACKING_EVENTS.LOCALIZE_START,
400
+ trackProps
239
401
  );
240
- const result = await this.localizeObject(mapped, params);
241
- return Object.values(result);
402
+ try {
403
+ const mapped = strings.reduce(
404
+ (acc, str, i) => {
405
+ acc[`item_${i}`] = str;
406
+ return acc;
407
+ },
408
+ {}
409
+ );
410
+ const result = await this._localizeRaw(mapped, params);
411
+ trackEvent(
412
+ this.config.apiKey,
413
+ this.config.apiUrl,
414
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
415
+ trackProps
416
+ );
417
+ return Object.values(result);
418
+ } catch (error) {
419
+ trackEvent(
420
+ this.config.apiKey,
421
+ this.config.apiUrl,
422
+ TRACKING_EVENTS.LOCALIZE_ERROR,
423
+ {
424
+ ...trackProps,
425
+ errorMessage: error instanceof Error ? error.message : String(error)
426
+ }
427
+ );
428
+ throw error;
429
+ }
242
430
  }
243
431
  /**
244
432
  * Localize a chat sequence while preserving speaker names
@@ -252,16 +440,47 @@ var LingoDotDevEngine = class {
252
440
  * @returns Array of localized chat messages with preserved structure
253
441
  */
254
442
  async localizeChat(chat, params, progressCallback, signal) {
255
- const localized = await this._localizeRaw(
256
- { chat },
257
- params,
258
- progressCallback,
259
- signal
443
+ const trackProps = {
444
+ method: "localizeChat",
445
+ sourceLocale: params.sourceLocale,
446
+ targetLocale: params.targetLocale
447
+ };
448
+ trackEvent(
449
+ this.config.apiKey,
450
+ this.config.apiUrl,
451
+ TRACKING_EVENTS.LOCALIZE_START,
452
+ trackProps
260
453
  );
261
- return Object.entries(localized).map(([key, value]) => ({
262
- name: chat[parseInt(key.split("_")[1])].name,
263
- text: value
264
- }));
454
+ try {
455
+ const localized = await this._localizeRaw(
456
+ { chat },
457
+ params,
458
+ progressCallback,
459
+ signal
460
+ );
461
+ const result = Object.entries(localized).map(([key, value]) => ({
462
+ name: chat[parseInt(key.split("_")[1])].name,
463
+ text: value
464
+ }));
465
+ trackEvent(
466
+ this.config.apiKey,
467
+ this.config.apiUrl,
468
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
469
+ trackProps
470
+ );
471
+ return result;
472
+ } catch (error) {
473
+ trackEvent(
474
+ this.config.apiKey,
475
+ this.config.apiUrl,
476
+ TRACKING_EVENTS.LOCALIZE_ERROR,
477
+ {
478
+ ...trackProps,
479
+ errorMessage: error instanceof Error ? error.message : String(error)
480
+ }
481
+ );
482
+ throw error;
483
+ }
265
484
  }
266
485
  /**
267
486
  * Localize an HTML document while preserving structure and formatting
@@ -276,105 +495,136 @@ var LingoDotDevEngine = class {
276
495
  * @returns The localized HTML document as a string, with updated lang attribute
277
496
  */
278
497
  async localizeHtml(html, params, progressCallback, signal) {
279
- const jsdomPackage = await Promise.resolve().then(() => _interopRequireWildcard(require("jsdom")));
280
- const { JSDOM } = jsdomPackage;
281
- const dom = new JSDOM(html);
282
- const document = dom.window.document;
283
- const LOCALIZABLE_ATTRIBUTES = {
284
- meta: ["content"],
285
- img: ["alt"],
286
- input: ["placeholder"],
287
- a: ["title"]
498
+ const trackProps = {
499
+ method: "localizeHtml",
500
+ sourceLocale: params.sourceLocale,
501
+ targetLocale: params.targetLocale
288
502
  };
289
- const UNLOCALIZABLE_TAGS = ["script", "style"];
290
- const extractedContent = {};
291
- const getPath = (node, attribute) => {
292
- const indices = [];
293
- let current = node;
294
- let rootParent = "";
295
- while (current) {
296
- const parent = current.parentElement;
297
- if (!parent) break;
298
- if (parent === document.documentElement) {
299
- rootParent = current.nodeName.toLowerCase();
300
- break;
503
+ trackEvent(
504
+ this.config.apiKey,
505
+ this.config.apiUrl,
506
+ TRACKING_EVENTS.LOCALIZE_START,
507
+ trackProps
508
+ );
509
+ try {
510
+ const jsdomPackage = await Promise.resolve().then(() => _interopRequireWildcard(require("jsdom")));
511
+ const { JSDOM } = jsdomPackage;
512
+ const dom = new JSDOM(html);
513
+ const document = dom.window.document;
514
+ const LOCALIZABLE_ATTRIBUTES = {
515
+ meta: ["content"],
516
+ img: ["alt"],
517
+ input: ["placeholder"],
518
+ a: ["title"]
519
+ };
520
+ const UNLOCALIZABLE_TAGS = ["script", "style"];
521
+ const extractedContent = {};
522
+ const getPath = (node, attribute) => {
523
+ const indices = [];
524
+ let current = node;
525
+ let rootParent = "";
526
+ while (current) {
527
+ const parent = current.parentElement;
528
+ if (!parent) break;
529
+ if (parent === document.documentElement) {
530
+ rootParent = current.nodeName.toLowerCase();
531
+ break;
532
+ }
533
+ const siblings = Array.from(parent.childNodes).filter(
534
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _2 => _2.textContent, 'optionalAccess', _3 => _3.trim, 'call', _4 => _4()])
535
+ );
536
+ const index = siblings.indexOf(current);
537
+ if (index !== -1) {
538
+ indices.unshift(index);
539
+ }
540
+ current = parent;
301
541
  }
302
- const siblings = Array.from(parent.childNodes).filter(
303
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _ => _.textContent, 'optionalAccess', _2 => _2.trim, 'call', _3 => _3()])
304
- );
305
- const index = siblings.indexOf(current);
306
- if (index !== -1) {
307
- indices.unshift(index);
542
+ const basePath = rootParent ? `${rootParent}/${indices.join("/")}` : indices.join("/");
543
+ return attribute ? `${basePath}#${attribute}` : basePath;
544
+ };
545
+ const processNode = (node) => {
546
+ let parent = node.parentElement;
547
+ while (parent) {
548
+ if (UNLOCALIZABLE_TAGS.includes(parent.tagName.toLowerCase())) {
549
+ return;
550
+ }
551
+ parent = parent.parentElement;
308
552
  }
309
- current = parent;
310
- }
311
- const basePath = rootParent ? `${rootParent}/${indices.join("/")}` : indices.join("/");
312
- return attribute ? `${basePath}#${attribute}` : basePath;
313
- };
314
- const processNode = (node) => {
315
- let parent = node.parentElement;
316
- while (parent) {
317
- if (UNLOCALIZABLE_TAGS.includes(parent.tagName.toLowerCase())) {
318
- return;
553
+ if (node.nodeType === 3) {
554
+ const text = _optionalChain([node, 'access', _5 => _5.textContent, 'optionalAccess', _6 => _6.trim, 'call', _7 => _7()]) || "";
555
+ if (text) {
556
+ extractedContent[getPath(node)] = text;
557
+ }
558
+ } else if (node.nodeType === 1) {
559
+ const element = node;
560
+ const tagName = element.tagName.toLowerCase();
561
+ const attributes = LOCALIZABLE_ATTRIBUTES[tagName] || [];
562
+ attributes.forEach((attr) => {
563
+ const value = element.getAttribute(attr);
564
+ if (value) {
565
+ extractedContent[getPath(element, attr)] = value;
566
+ }
567
+ });
568
+ Array.from(element.childNodes).filter(
569
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _8 => _8.textContent, 'optionalAccess', _9 => _9.trim, 'call', _10 => _10()])
570
+ ).forEach(processNode);
319
571
  }
320
- parent = parent.parentElement;
321
- }
322
- if (node.nodeType === 3) {
323
- const text = _optionalChain([node, 'access', _4 => _4.textContent, 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()]) || "";
324
- if (text) {
325
- extractedContent[getPath(node)] = text;
572
+ };
573
+ Array.from(document.head.childNodes).filter(
574
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _11 => _11.textContent, 'optionalAccess', _12 => _12.trim, 'call', _13 => _13()])
575
+ ).forEach(processNode);
576
+ Array.from(document.body.childNodes).filter(
577
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _14 => _14.textContent, 'optionalAccess', _15 => _15.trim, 'call', _16 => _16()])
578
+ ).forEach(processNode);
579
+ const localizedContent = await this._localizeRaw(
580
+ extractedContent,
581
+ params,
582
+ progressCallback,
583
+ signal
584
+ );
585
+ document.documentElement.setAttribute("lang", params.targetLocale);
586
+ Object.entries(localizedContent).forEach(([path, value]) => {
587
+ const [nodePath, attribute] = path.split("#");
588
+ const [rootTag, ...indices] = nodePath.split("/");
589
+ let parent = rootTag === "head" ? document.head : document.body;
590
+ let current = parent;
591
+ for (const index of indices) {
592
+ const siblings = Array.from(parent.childNodes).filter(
593
+ (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _17 => _17.textContent, 'optionalAccess', _18 => _18.trim, 'call', _19 => _19()])
594
+ );
595
+ current = siblings[parseInt(index)] || null;
596
+ if (_optionalChain([current, 'optionalAccess', _20 => _20.nodeType]) === 1) {
597
+ parent = current;
598
+ }
326
599
  }
327
- } else if (node.nodeType === 1) {
328
- const element = node;
329
- const tagName = element.tagName.toLowerCase();
330
- const attributes = LOCALIZABLE_ATTRIBUTES[tagName] || [];
331
- attributes.forEach((attr) => {
332
- const value = element.getAttribute(attr);
333
- if (value) {
334
- extractedContent[getPath(element, attr)] = value;
600
+ if (current) {
601
+ if (attribute) {
602
+ current.setAttribute(attribute, value);
603
+ } else {
604
+ current.textContent = value;
335
605
  }
336
- });
337
- Array.from(element.childNodes).filter(
338
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _7 => _7.textContent, 'optionalAccess', _8 => _8.trim, 'call', _9 => _9()])
339
- ).forEach(processNode);
340
- }
341
- };
342
- Array.from(document.head.childNodes).filter(
343
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _10 => _10.textContent, 'optionalAccess', _11 => _11.trim, 'call', _12 => _12()])
344
- ).forEach(processNode);
345
- Array.from(document.body.childNodes).filter(
346
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _13 => _13.textContent, 'optionalAccess', _14 => _14.trim, 'call', _15 => _15()])
347
- ).forEach(processNode);
348
- const localizedContent = await this._localizeRaw(
349
- extractedContent,
350
- params,
351
- progressCallback,
352
- signal
353
- );
354
- document.documentElement.setAttribute("lang", params.targetLocale);
355
- Object.entries(localizedContent).forEach(([path, value]) => {
356
- const [nodePath, attribute] = path.split("#");
357
- const [rootTag, ...indices] = nodePath.split("/");
358
- let parent = rootTag === "head" ? document.head : document.body;
359
- let current = parent;
360
- for (const index of indices) {
361
- const siblings = Array.from(parent.childNodes).filter(
362
- (n) => n.nodeType === 1 || n.nodeType === 3 && _optionalChain([n, 'access', _16 => _16.textContent, 'optionalAccess', _17 => _17.trim, 'call', _18 => _18()])
363
- );
364
- current = siblings[parseInt(index)] || null;
365
- if (_optionalChain([current, 'optionalAccess', _19 => _19.nodeType]) === 1) {
366
- parent = current;
367
606
  }
368
- }
369
- if (current) {
370
- if (attribute) {
371
- current.setAttribute(attribute, value);
372
- } else {
373
- current.textContent = value;
607
+ });
608
+ const result = dom.serialize();
609
+ trackEvent(
610
+ this.config.apiKey,
611
+ this.config.apiUrl,
612
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
613
+ trackProps
614
+ );
615
+ return result;
616
+ } catch (error) {
617
+ trackEvent(
618
+ this.config.apiKey,
619
+ this.config.apiUrl,
620
+ TRACKING_EVENTS.LOCALIZE_ERROR,
621
+ {
622
+ ...trackProps,
623
+ errorMessage: error instanceof Error ? error.message : String(error)
374
624
  }
375
- }
376
- });
377
- return dom.serialize();
625
+ );
626
+ throw error;
627
+ }
378
628
  }
379
629
  /**
380
630
  * Detect the language of a given text
@@ -383,25 +633,51 @@ var LingoDotDevEngine = class {
383
633
  * @returns Promise resolving to a locale code (e.g., 'en', 'es', 'fr')
384
634
  */
385
635
  async recognizeLocale(text, signal) {
386
- const response = await fetch(`${this.config.apiUrl}/recognize`, {
387
- method: "POST",
388
- headers: {
389
- "Content-Type": "application/json; charset=utf-8",
390
- Authorization: `Bearer ${this.config.apiKey}`
391
- },
392
- body: JSON.stringify({ text }),
393
- signal
394
- });
395
- if (!response.ok) {
396
- if (response.status >= 500 && response.status < 600) {
397
- throw new Error(
398
- `Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`
399
- );
636
+ const trackProps = { method: "recognizeLocale" };
637
+ trackEvent(
638
+ this.config.apiKey,
639
+ this.config.apiUrl,
640
+ TRACKING_EVENTS.RECOGNIZE_START,
641
+ trackProps
642
+ );
643
+ try {
644
+ const response = await fetch(`${this.config.apiUrl}/recognize`, {
645
+ method: "POST",
646
+ headers: {
647
+ "Content-Type": "application/json; charset=utf-8",
648
+ Authorization: `Bearer ${this.config.apiKey}`
649
+ },
650
+ body: JSON.stringify({ text }),
651
+ signal
652
+ });
653
+ if (!response.ok) {
654
+ if (response.status >= 500 && response.status < 600) {
655
+ throw new Error(
656
+ `Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`
657
+ );
658
+ }
659
+ throw new Error(`Error recognizing locale: ${response.statusText}`);
400
660
  }
401
- throw new Error(`Error recognizing locale: ${response.statusText}`);
661
+ const jsonResponse = await response.json();
662
+ trackEvent(
663
+ this.config.apiKey,
664
+ this.config.apiUrl,
665
+ TRACKING_EVENTS.RECOGNIZE_SUCCESS,
666
+ trackProps
667
+ );
668
+ return jsonResponse.locale;
669
+ } catch (error) {
670
+ trackEvent(
671
+ this.config.apiKey,
672
+ this.config.apiUrl,
673
+ TRACKING_EVENTS.RECOGNIZE_ERROR,
674
+ {
675
+ ...trackProps,
676
+ errorMessage: error instanceof Error ? error.message : String(error)
677
+ }
678
+ );
679
+ throw error;
402
680
  }
403
- const jsonResponse = await response.json();
404
- return jsonResponse.locale;
405
681
  }
406
682
  async whoami(signal) {
407
683
  try {
@@ -409,13 +685,13 @@ var LingoDotDevEngine = class {
409
685
  method: "POST",
410
686
  headers: {
411
687
  Authorization: `Bearer ${this.config.apiKey}`,
412
- ContentType: "application/json"
688
+ "Content-Type": "application/json"
413
689
  },
414
690
  signal
415
691
  });
416
692
  if (res.ok) {
417
693
  const payload = await res.json();
418
- if (!_optionalChain([payload, 'optionalAccess', _20 => _20.email])) {
694
+ if (!_optionalChain([payload, 'optionalAccess', _21 => _21.email])) {
419
695
  return null;
420
696
  }
421
697
  return {
package/build/index.mjs CHANGED
@@ -2,6 +2,97 @@
2
2
  import Z from "zod";
3
3
  import { localeCodeSchema } from "@lingo.dev/_spec";
4
4
  import { createId } from "@paralleldrive/cuid2";
5
+
6
+ // src/utils/observability.ts
7
+ import { createHash } from "crypto";
8
+
9
+ // src/utils/tracking-events.ts
10
+ var TRACKING_EVENTS = {
11
+ LOCALIZE_START: "sdk.localize.start",
12
+ LOCALIZE_SUCCESS: "sdk.localize.success",
13
+ LOCALIZE_ERROR: "sdk.localize.error",
14
+ RECOGNIZE_START: "sdk.recognize.start",
15
+ RECOGNIZE_SUCCESS: "sdk.recognize.success",
16
+ RECOGNIZE_ERROR: "sdk.recognize.error"
17
+ };
18
+ var TRACKING_VERSION = "1.0";
19
+ var SDK_PACKAGE = "@lingo.dev/_sdk";
20
+
21
+ // src/utils/observability.ts
22
+ var POSTHOG_API_KEY = "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk";
23
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
24
+ var identityCache = /* @__PURE__ */ new Map();
25
+ function trackEvent(apiKey, apiUrl, event, properties) {
26
+ if (process.env.DO_NOT_TRACK === "1") {
27
+ return;
28
+ }
29
+ resolveIdentityAndCapture(apiKey, apiUrl, event, properties).catch(
30
+ (error) => {
31
+ if (process.env.DEBUG === "true") {
32
+ console.error("[Tracking] Error:", error);
33
+ }
34
+ }
35
+ );
36
+ }
37
+ async function resolveIdentityAndCapture(apiKey, apiUrl, event, properties) {
38
+ const identityInfo = await getDistinctId(apiKey, apiUrl);
39
+ if (process.env.DEBUG === "true") {
40
+ console.log(
41
+ `[Tracking] Event: ${event}, ID: ${identityInfo.distinct_id}, Source: ${identityInfo.distinct_id_source}`
42
+ );
43
+ }
44
+ const { PostHog } = await import("posthog-node");
45
+ const posthog = new PostHog(POSTHOG_API_KEY, {
46
+ host: POSTHOG_HOST,
47
+ flushAt: 1,
48
+ flushInterval: 0
49
+ });
50
+ await posthog.capture({
51
+ distinctId: identityInfo.distinct_id,
52
+ event,
53
+ properties: {
54
+ ...properties,
55
+ tracking_version: TRACKING_VERSION,
56
+ sdk_package: SDK_PACKAGE,
57
+ distinct_id_source: identityInfo.distinct_id_source
58
+ }
59
+ });
60
+ await posthog.shutdown();
61
+ }
62
+ async function getDistinctId(apiKey, apiUrl) {
63
+ const cached = identityCache.get(apiKey);
64
+ if (cached) return cached;
65
+ try {
66
+ const res = await fetch(`${apiUrl}/whoami`, {
67
+ method: "POST",
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ "Content-Type": "application/json"
71
+ }
72
+ });
73
+ if (res.ok) {
74
+ const payload = await res.json();
75
+ if (payload?.email) {
76
+ const identity2 = {
77
+ distinct_id: payload.email,
78
+ distinct_id_source: "email"
79
+ };
80
+ identityCache.set(apiKey, identity2);
81
+ return identity2;
82
+ }
83
+ }
84
+ } catch {
85
+ }
86
+ const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
87
+ const identity = {
88
+ distinct_id: `apikey-${hash}`,
89
+ distinct_id_source: "api_key_hash"
90
+ };
91
+ identityCache.set(apiKey, identity);
92
+ return identity;
93
+ }
94
+
95
+ // src/index.ts
5
96
  var engineParamsSchema = Z.object({
6
97
  apiKey: Z.string(),
7
98
  apiUrl: Z.string().url().default("https://engine.lingo.dev"),
@@ -171,7 +262,43 @@ var LingoDotDevEngine = class {
171
262
  * @returns A new object with the same structure but localized string values
172
263
  */
173
264
  async localizeObject(obj, params, progressCallback, signal) {
174
- return this._localizeRaw(obj, params, progressCallback, signal);
265
+ const trackProps = {
266
+ method: "localizeObject",
267
+ sourceLocale: params.sourceLocale,
268
+ targetLocale: params.targetLocale
269
+ };
270
+ trackEvent(
271
+ this.config.apiKey,
272
+ this.config.apiUrl,
273
+ TRACKING_EVENTS.LOCALIZE_START,
274
+ trackProps
275
+ );
276
+ try {
277
+ const result = await this._localizeRaw(
278
+ obj,
279
+ params,
280
+ progressCallback,
281
+ signal
282
+ );
283
+ trackEvent(
284
+ this.config.apiKey,
285
+ this.config.apiUrl,
286
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
287
+ trackProps
288
+ );
289
+ return result;
290
+ } catch (error) {
291
+ trackEvent(
292
+ this.config.apiKey,
293
+ this.config.apiUrl,
294
+ TRACKING_EVENTS.LOCALIZE_ERROR,
295
+ {
296
+ ...trackProps,
297
+ errorMessage: error instanceof Error ? error.message : String(error)
298
+ }
299
+ );
300
+ throw error;
301
+ }
175
302
  }
176
303
  /**
177
304
  * Localize a single text string
@@ -185,13 +312,44 @@ var LingoDotDevEngine = class {
185
312
  * @returns The localized text string
186
313
  */
187
314
  async localizeText(text, params, progressCallback, signal) {
188
- const response = await this._localizeRaw(
189
- { text },
190
- params,
191
- progressCallback,
192
- signal
315
+ const trackProps = {
316
+ method: "localizeText",
317
+ sourceLocale: params.sourceLocale,
318
+ targetLocale: params.targetLocale
319
+ };
320
+ trackEvent(
321
+ this.config.apiKey,
322
+ this.config.apiUrl,
323
+ TRACKING_EVENTS.LOCALIZE_START,
324
+ trackProps
193
325
  );
194
- return response.text || "";
326
+ try {
327
+ const response = await this._localizeRaw(
328
+ { text },
329
+ params,
330
+ progressCallback,
331
+ signal
332
+ );
333
+ const result = response.text || "";
334
+ trackEvent(
335
+ this.config.apiKey,
336
+ this.config.apiUrl,
337
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
338
+ trackProps
339
+ );
340
+ return result;
341
+ } catch (error) {
342
+ trackEvent(
343
+ this.config.apiKey,
344
+ this.config.apiUrl,
345
+ TRACKING_EVENTS.LOCALIZE_ERROR,
346
+ {
347
+ ...trackProps,
348
+ errorMessage: error instanceof Error ? error.message : String(error)
349
+ }
350
+ );
351
+ throw error;
352
+ }
195
353
  }
196
354
  /**
197
355
  * Localize a text string to multiple target locales
@@ -230,15 +388,45 @@ var LingoDotDevEngine = class {
230
388
  * @returns An array of localized strings in the same order
231
389
  */
232
390
  async localizeStringArray(strings, params) {
233
- const mapped = strings.reduce(
234
- (acc, str, i) => {
235
- acc[`item_${i}`] = str;
236
- return acc;
237
- },
238
- {}
391
+ const trackProps = {
392
+ method: "localizeStringArray",
393
+ sourceLocale: params.sourceLocale,
394
+ targetLocale: params.targetLocale
395
+ };
396
+ trackEvent(
397
+ this.config.apiKey,
398
+ this.config.apiUrl,
399
+ TRACKING_EVENTS.LOCALIZE_START,
400
+ trackProps
239
401
  );
240
- const result = await this.localizeObject(mapped, params);
241
- return Object.values(result);
402
+ try {
403
+ const mapped = strings.reduce(
404
+ (acc, str, i) => {
405
+ acc[`item_${i}`] = str;
406
+ return acc;
407
+ },
408
+ {}
409
+ );
410
+ const result = await this._localizeRaw(mapped, params);
411
+ trackEvent(
412
+ this.config.apiKey,
413
+ this.config.apiUrl,
414
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
415
+ trackProps
416
+ );
417
+ return Object.values(result);
418
+ } catch (error) {
419
+ trackEvent(
420
+ this.config.apiKey,
421
+ this.config.apiUrl,
422
+ TRACKING_EVENTS.LOCALIZE_ERROR,
423
+ {
424
+ ...trackProps,
425
+ errorMessage: error instanceof Error ? error.message : String(error)
426
+ }
427
+ );
428
+ throw error;
429
+ }
242
430
  }
243
431
  /**
244
432
  * Localize a chat sequence while preserving speaker names
@@ -252,16 +440,47 @@ var LingoDotDevEngine = class {
252
440
  * @returns Array of localized chat messages with preserved structure
253
441
  */
254
442
  async localizeChat(chat, params, progressCallback, signal) {
255
- const localized = await this._localizeRaw(
256
- { chat },
257
- params,
258
- progressCallback,
259
- signal
443
+ const trackProps = {
444
+ method: "localizeChat",
445
+ sourceLocale: params.sourceLocale,
446
+ targetLocale: params.targetLocale
447
+ };
448
+ trackEvent(
449
+ this.config.apiKey,
450
+ this.config.apiUrl,
451
+ TRACKING_EVENTS.LOCALIZE_START,
452
+ trackProps
260
453
  );
261
- return Object.entries(localized).map(([key, value]) => ({
262
- name: chat[parseInt(key.split("_")[1])].name,
263
- text: value
264
- }));
454
+ try {
455
+ const localized = await this._localizeRaw(
456
+ { chat },
457
+ params,
458
+ progressCallback,
459
+ signal
460
+ );
461
+ const result = Object.entries(localized).map(([key, value]) => ({
462
+ name: chat[parseInt(key.split("_")[1])].name,
463
+ text: value
464
+ }));
465
+ trackEvent(
466
+ this.config.apiKey,
467
+ this.config.apiUrl,
468
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
469
+ trackProps
470
+ );
471
+ return result;
472
+ } catch (error) {
473
+ trackEvent(
474
+ this.config.apiKey,
475
+ this.config.apiUrl,
476
+ TRACKING_EVENTS.LOCALIZE_ERROR,
477
+ {
478
+ ...trackProps,
479
+ errorMessage: error instanceof Error ? error.message : String(error)
480
+ }
481
+ );
482
+ throw error;
483
+ }
265
484
  }
266
485
  /**
267
486
  * Localize an HTML document while preserving structure and formatting
@@ -276,105 +495,136 @@ var LingoDotDevEngine = class {
276
495
  * @returns The localized HTML document as a string, with updated lang attribute
277
496
  */
278
497
  async localizeHtml(html, params, progressCallback, signal) {
279
- const jsdomPackage = await import("jsdom");
280
- const { JSDOM } = jsdomPackage;
281
- const dom = new JSDOM(html);
282
- const document = dom.window.document;
283
- const LOCALIZABLE_ATTRIBUTES = {
284
- meta: ["content"],
285
- img: ["alt"],
286
- input: ["placeholder"],
287
- a: ["title"]
498
+ const trackProps = {
499
+ method: "localizeHtml",
500
+ sourceLocale: params.sourceLocale,
501
+ targetLocale: params.targetLocale
288
502
  };
289
- const UNLOCALIZABLE_TAGS = ["script", "style"];
290
- const extractedContent = {};
291
- const getPath = (node, attribute) => {
292
- const indices = [];
293
- let current = node;
294
- let rootParent = "";
295
- while (current) {
296
- const parent = current.parentElement;
297
- if (!parent) break;
298
- if (parent === document.documentElement) {
299
- rootParent = current.nodeName.toLowerCase();
300
- break;
503
+ trackEvent(
504
+ this.config.apiKey,
505
+ this.config.apiUrl,
506
+ TRACKING_EVENTS.LOCALIZE_START,
507
+ trackProps
508
+ );
509
+ try {
510
+ const jsdomPackage = await import("jsdom");
511
+ const { JSDOM } = jsdomPackage;
512
+ const dom = new JSDOM(html);
513
+ const document = dom.window.document;
514
+ const LOCALIZABLE_ATTRIBUTES = {
515
+ meta: ["content"],
516
+ img: ["alt"],
517
+ input: ["placeholder"],
518
+ a: ["title"]
519
+ };
520
+ const UNLOCALIZABLE_TAGS = ["script", "style"];
521
+ const extractedContent = {};
522
+ const getPath = (node, attribute) => {
523
+ const indices = [];
524
+ let current = node;
525
+ let rootParent = "";
526
+ while (current) {
527
+ const parent = current.parentElement;
528
+ if (!parent) break;
529
+ if (parent === document.documentElement) {
530
+ rootParent = current.nodeName.toLowerCase();
531
+ break;
532
+ }
533
+ const siblings = Array.from(parent.childNodes).filter(
534
+ (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
535
+ );
536
+ const index = siblings.indexOf(current);
537
+ if (index !== -1) {
538
+ indices.unshift(index);
539
+ }
540
+ current = parent;
301
541
  }
302
- const siblings = Array.from(parent.childNodes).filter(
303
- (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
304
- );
305
- const index = siblings.indexOf(current);
306
- if (index !== -1) {
307
- indices.unshift(index);
542
+ const basePath = rootParent ? `${rootParent}/${indices.join("/")}` : indices.join("/");
543
+ return attribute ? `${basePath}#${attribute}` : basePath;
544
+ };
545
+ const processNode = (node) => {
546
+ let parent = node.parentElement;
547
+ while (parent) {
548
+ if (UNLOCALIZABLE_TAGS.includes(parent.tagName.toLowerCase())) {
549
+ return;
550
+ }
551
+ parent = parent.parentElement;
308
552
  }
309
- current = parent;
310
- }
311
- const basePath = rootParent ? `${rootParent}/${indices.join("/")}` : indices.join("/");
312
- return attribute ? `${basePath}#${attribute}` : basePath;
313
- };
314
- const processNode = (node) => {
315
- let parent = node.parentElement;
316
- while (parent) {
317
- if (UNLOCALIZABLE_TAGS.includes(parent.tagName.toLowerCase())) {
318
- return;
553
+ if (node.nodeType === 3) {
554
+ const text = node.textContent?.trim() || "";
555
+ if (text) {
556
+ extractedContent[getPath(node)] = text;
557
+ }
558
+ } else if (node.nodeType === 1) {
559
+ const element = node;
560
+ const tagName = element.tagName.toLowerCase();
561
+ const attributes = LOCALIZABLE_ATTRIBUTES[tagName] || [];
562
+ attributes.forEach((attr) => {
563
+ const value = element.getAttribute(attr);
564
+ if (value) {
565
+ extractedContent[getPath(element, attr)] = value;
566
+ }
567
+ });
568
+ Array.from(element.childNodes).filter(
569
+ (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
570
+ ).forEach(processNode);
319
571
  }
320
- parent = parent.parentElement;
321
- }
322
- if (node.nodeType === 3) {
323
- const text = node.textContent?.trim() || "";
324
- if (text) {
325
- extractedContent[getPath(node)] = text;
572
+ };
573
+ Array.from(document.head.childNodes).filter(
574
+ (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
575
+ ).forEach(processNode);
576
+ Array.from(document.body.childNodes).filter(
577
+ (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
578
+ ).forEach(processNode);
579
+ const localizedContent = await this._localizeRaw(
580
+ extractedContent,
581
+ params,
582
+ progressCallback,
583
+ signal
584
+ );
585
+ document.documentElement.setAttribute("lang", params.targetLocale);
586
+ Object.entries(localizedContent).forEach(([path, value]) => {
587
+ const [nodePath, attribute] = path.split("#");
588
+ const [rootTag, ...indices] = nodePath.split("/");
589
+ let parent = rootTag === "head" ? document.head : document.body;
590
+ let current = parent;
591
+ for (const index of indices) {
592
+ const siblings = Array.from(parent.childNodes).filter(
593
+ (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
594
+ );
595
+ current = siblings[parseInt(index)] || null;
596
+ if (current?.nodeType === 1) {
597
+ parent = current;
598
+ }
326
599
  }
327
- } else if (node.nodeType === 1) {
328
- const element = node;
329
- const tagName = element.tagName.toLowerCase();
330
- const attributes = LOCALIZABLE_ATTRIBUTES[tagName] || [];
331
- attributes.forEach((attr) => {
332
- const value = element.getAttribute(attr);
333
- if (value) {
334
- extractedContent[getPath(element, attr)] = value;
600
+ if (current) {
601
+ if (attribute) {
602
+ current.setAttribute(attribute, value);
603
+ } else {
604
+ current.textContent = value;
335
605
  }
336
- });
337
- Array.from(element.childNodes).filter(
338
- (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
339
- ).forEach(processNode);
340
- }
341
- };
342
- Array.from(document.head.childNodes).filter(
343
- (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
344
- ).forEach(processNode);
345
- Array.from(document.body.childNodes).filter(
346
- (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
347
- ).forEach(processNode);
348
- const localizedContent = await this._localizeRaw(
349
- extractedContent,
350
- params,
351
- progressCallback,
352
- signal
353
- );
354
- document.documentElement.setAttribute("lang", params.targetLocale);
355
- Object.entries(localizedContent).forEach(([path, value]) => {
356
- const [nodePath, attribute] = path.split("#");
357
- const [rootTag, ...indices] = nodePath.split("/");
358
- let parent = rootTag === "head" ? document.head : document.body;
359
- let current = parent;
360
- for (const index of indices) {
361
- const siblings = Array.from(parent.childNodes).filter(
362
- (n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent?.trim()
363
- );
364
- current = siblings[parseInt(index)] || null;
365
- if (current?.nodeType === 1) {
366
- parent = current;
367
606
  }
368
- }
369
- if (current) {
370
- if (attribute) {
371
- current.setAttribute(attribute, value);
372
- } else {
373
- current.textContent = value;
607
+ });
608
+ const result = dom.serialize();
609
+ trackEvent(
610
+ this.config.apiKey,
611
+ this.config.apiUrl,
612
+ TRACKING_EVENTS.LOCALIZE_SUCCESS,
613
+ trackProps
614
+ );
615
+ return result;
616
+ } catch (error) {
617
+ trackEvent(
618
+ this.config.apiKey,
619
+ this.config.apiUrl,
620
+ TRACKING_EVENTS.LOCALIZE_ERROR,
621
+ {
622
+ ...trackProps,
623
+ errorMessage: error instanceof Error ? error.message : String(error)
374
624
  }
375
- }
376
- });
377
- return dom.serialize();
625
+ );
626
+ throw error;
627
+ }
378
628
  }
379
629
  /**
380
630
  * Detect the language of a given text
@@ -383,25 +633,51 @@ var LingoDotDevEngine = class {
383
633
  * @returns Promise resolving to a locale code (e.g., 'en', 'es', 'fr')
384
634
  */
385
635
  async recognizeLocale(text, signal) {
386
- const response = await fetch(`${this.config.apiUrl}/recognize`, {
387
- method: "POST",
388
- headers: {
389
- "Content-Type": "application/json; charset=utf-8",
390
- Authorization: `Bearer ${this.config.apiKey}`
391
- },
392
- body: JSON.stringify({ text }),
393
- signal
394
- });
395
- if (!response.ok) {
396
- if (response.status >= 500 && response.status < 600) {
397
- throw new Error(
398
- `Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`
399
- );
636
+ const trackProps = { method: "recognizeLocale" };
637
+ trackEvent(
638
+ this.config.apiKey,
639
+ this.config.apiUrl,
640
+ TRACKING_EVENTS.RECOGNIZE_START,
641
+ trackProps
642
+ );
643
+ try {
644
+ const response = await fetch(`${this.config.apiUrl}/recognize`, {
645
+ method: "POST",
646
+ headers: {
647
+ "Content-Type": "application/json; charset=utf-8",
648
+ Authorization: `Bearer ${this.config.apiKey}`
649
+ },
650
+ body: JSON.stringify({ text }),
651
+ signal
652
+ });
653
+ if (!response.ok) {
654
+ if (response.status >= 500 && response.status < 600) {
655
+ throw new Error(
656
+ `Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`
657
+ );
658
+ }
659
+ throw new Error(`Error recognizing locale: ${response.statusText}`);
400
660
  }
401
- throw new Error(`Error recognizing locale: ${response.statusText}`);
661
+ const jsonResponse = await response.json();
662
+ trackEvent(
663
+ this.config.apiKey,
664
+ this.config.apiUrl,
665
+ TRACKING_EVENTS.RECOGNIZE_SUCCESS,
666
+ trackProps
667
+ );
668
+ return jsonResponse.locale;
669
+ } catch (error) {
670
+ trackEvent(
671
+ this.config.apiKey,
672
+ this.config.apiUrl,
673
+ TRACKING_EVENTS.RECOGNIZE_ERROR,
674
+ {
675
+ ...trackProps,
676
+ errorMessage: error instanceof Error ? error.message : String(error)
677
+ }
678
+ );
679
+ throw error;
402
680
  }
403
- const jsonResponse = await response.json();
404
- return jsonResponse.locale;
405
681
  }
406
682
  async whoami(signal) {
407
683
  try {
@@ -409,7 +685,7 @@ var LingoDotDevEngine = class {
409
685
  method: "POST",
410
686
  headers: {
411
687
  Authorization: `Bearer ${this.config.apiKey}`,
412
- ContentType: "application/json"
688
+ "Content-Type": "application/json"
413
689
  },
414
690
  signal
415
691
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingo.dev/_sdk",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Lingo.dev JS SDK",
5
5
  "private": false,
6
6
  "repository": {
@@ -26,8 +26,9 @@
26
26
  "dependencies": {
27
27
  "@paralleldrive/cuid2": "2.2.2",
28
28
  "jsdom": "25.0.1",
29
+ "posthog-node": "5.14.0",
29
30
  "zod": "4.1.12",
30
- "@lingo.dev/_spec": "0.47.1"
31
+ "@lingo.dev/_spec": "0.48.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/jsdom": "21.1.7",