@procore/ai-translations 0.2.0 → 0.3.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.
@@ -2,6 +2,20 @@ import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React, { ReactNode } from 'react';
4
4
 
5
+ interface TranslationProgress {
6
+ /**
7
+ * Progress percentage (0-100)
8
+ */
9
+ progress: number;
10
+ /**
11
+ * Current number of strings translated
12
+ */
13
+ current: number;
14
+ /**
15
+ * Total number of strings to translate
16
+ */
17
+ total: number;
18
+ }
5
19
  interface TranslationQueueEntry {
6
20
  originalText: string;
7
21
  isTranslated: boolean;
@@ -11,6 +25,26 @@ interface TranslationQueueEntry {
11
25
  tool: string;
12
26
  translationStrategy: 'frontend_translations' | 'backend_translations';
13
27
  }
28
+ interface ModelDownloadProgress {
29
+ /** Bytes already downloaded */
30
+ loaded: number;
31
+ /** Total bytes to download */
32
+ total: number;
33
+ /** Download percentage (0-100) */
34
+ progress: number;
35
+ /** True once the model has finished downloading */
36
+ isComplete: boolean;
37
+ }
38
+ interface AITranslationContextValue {
39
+ ait: (text: string) => Promise<string>;
40
+ locale: string;
41
+ renderVersion?: number;
42
+ /** Batch-translation progress; `null` while the queue is idle. */
43
+ translationProgress: TranslationProgress | null;
44
+ /** Chrome AI model download progress; `null` until a download begins. */
45
+ modelDownloadProgress: ModelDownloadProgress | null;
46
+ tool: string;
47
+ }
14
48
 
15
49
  interface ConfigurationResponse {
16
50
  [key: string]: ToolConfig;
@@ -63,6 +97,25 @@ interface UseConfigOptions {
63
97
  */
64
98
  declare function useConfig(toolName: string, options?: UseConfigOptions): _tanstack_react_query.UseQueryResult<ConfigurationResponse | null, unknown>;
65
99
 
100
+ /**
101
+ * Returns the full AI-translation context value provided by the nearest
102
+ * `AITranslationProvider`.
103
+ *
104
+ * Use this hook when you need more than one value from the context at once.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * function MyComponent() {
109
+ * const { ait, locale, translationProgress, modelDownloadProgress } = useAITranslation();
110
+ *
111
+ * return <span>{locale}</span>;
112
+ * }
113
+ * ```
114
+ *
115
+ * @throws if called outside of an `AITranslationProvider`.
116
+ */
117
+ declare function useAITranslation(): AITranslationContextValue;
118
+
66
119
  interface AITranslationProviderProps {
67
120
  children: ReactNode;
68
121
  locale: string;
@@ -123,4 +176,4 @@ declare global {
123
176
  var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
124
177
  }
125
178
 
126
- export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useConfig };
179
+ export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useAITranslation, useConfig };
@@ -2,6 +2,20 @@ import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React, { ReactNode } from 'react';
4
4
 
5
+ interface TranslationProgress {
6
+ /**
7
+ * Progress percentage (0-100)
8
+ */
9
+ progress: number;
10
+ /**
11
+ * Current number of strings translated
12
+ */
13
+ current: number;
14
+ /**
15
+ * Total number of strings to translate
16
+ */
17
+ total: number;
18
+ }
5
19
  interface TranslationQueueEntry {
6
20
  originalText: string;
7
21
  isTranslated: boolean;
@@ -11,6 +25,26 @@ interface TranslationQueueEntry {
11
25
  tool: string;
12
26
  translationStrategy: 'frontend_translations' | 'backend_translations';
13
27
  }
28
+ interface ModelDownloadProgress {
29
+ /** Bytes already downloaded */
30
+ loaded: number;
31
+ /** Total bytes to download */
32
+ total: number;
33
+ /** Download percentage (0-100) */
34
+ progress: number;
35
+ /** True once the model has finished downloading */
36
+ isComplete: boolean;
37
+ }
38
+ interface AITranslationContextValue {
39
+ ait: (text: string) => Promise<string>;
40
+ locale: string;
41
+ renderVersion?: number;
42
+ /** Batch-translation progress; `null` while the queue is idle. */
43
+ translationProgress: TranslationProgress | null;
44
+ /** Chrome AI model download progress; `null` until a download begins. */
45
+ modelDownloadProgress: ModelDownloadProgress | null;
46
+ tool: string;
47
+ }
14
48
 
15
49
  interface ConfigurationResponse {
16
50
  [key: string]: ToolConfig;
@@ -63,6 +97,25 @@ interface UseConfigOptions {
63
97
  */
64
98
  declare function useConfig(toolName: string, options?: UseConfigOptions): _tanstack_react_query.UseQueryResult<ConfigurationResponse | null, unknown>;
65
99
 
100
+ /**
101
+ * Returns the full AI-translation context value provided by the nearest
102
+ * `AITranslationProvider`.
103
+ *
104
+ * Use this hook when you need more than one value from the context at once.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * function MyComponent() {
109
+ * const { ait, locale, translationProgress, modelDownloadProgress } = useAITranslation();
110
+ *
111
+ * return <span>{locale}</span>;
112
+ * }
113
+ * ```
114
+ *
115
+ * @throws if called outside of an `AITranslationProvider`.
116
+ */
117
+ declare function useAITranslation(): AITranslationContextValue;
118
+
66
119
  interface AITranslationProviderProps {
67
120
  children: ReactNode;
68
121
  locale: string;
@@ -123,4 +176,4 @@ declare global {
123
176
  var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
124
177
  }
125
178
 
126
- export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useConfig };
179
+ export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useAITranslation, useConfig };
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  AITranslationProvider: () => AITranslationProvider,
27
27
  AI_TRANSLATION_FEATURE_FLAG_KEY: () => AI_TRANSLATION_FEATURE_FLAG_KEY,
28
28
  getAITranslationLDId: () => getAITranslationLDId,
29
+ useAITranslation: () => useAITranslation,
29
30
  useConfig: () => useConfig
30
31
  });
31
32
  module.exports = __toCommonJS(index_exports);
@@ -212,6 +213,9 @@ function useConfig(toolName, options = {}) {
212
213
  });
213
214
  }
214
215
 
216
+ // src/hooks/useAITranslation.ts
217
+ var import_react2 = require("react");
218
+
215
219
  // src/Provider.tsx
216
220
  var import_react = require("react");
217
221
 
@@ -616,6 +620,7 @@ var import_web_sdk_events = require("@procore/web-sdk-events");
616
620
  var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
617
621
  var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
618
622
  var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
623
+ var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
619
624
 
620
625
  // src/utils/eventHandler.ts
621
626
  var EventHandler = class {
@@ -682,6 +687,34 @@ var EventHandler = class {
682
687
  }
683
688
  );
684
689
  }
690
+ /**
691
+ * Publishes a Chrome AI language-model download-progress event.
692
+ *
693
+ * Not tool-scoped — a Chrome language model is shared across the entire
694
+ * browser session, so all Provider instances receive it.
695
+ */
696
+ publishModelDownloadProgressEvent(loaded, total, progress) {
697
+ this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
698
+ loaded,
699
+ total,
700
+ progress,
701
+ isComplete: loaded === total
702
+ });
703
+ }
704
+ subscribeToModelDownloadProgressEvent(callback) {
705
+ return this.aiTranslationEvents.subscribe(
706
+ MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
707
+ (detail) => {
708
+ var _a, _b, _c, _d;
709
+ return callback({
710
+ loaded: ((_a = detail.data) == null ? void 0 : _a.loaded) ?? 0,
711
+ total: ((_b = detail.data) == null ? void 0 : _b.total) ?? 0,
712
+ progress: ((_c = detail.data) == null ? void 0 : _c.progress) ?? 0,
713
+ isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false
714
+ });
715
+ }
716
+ );
717
+ }
685
718
  withEventName(eventName) {
686
719
  return eventName + ":" + this.toolName;
687
720
  }
@@ -974,6 +1007,13 @@ var ChromeLanguageDetector = class _ChromeLanguageDetector {
974
1007
  };
975
1008
 
976
1009
  // src/translators/frontend_translators/chrome/Translator.ts
1010
+ var _modelDownloadEventHandler = null;
1011
+ var getModelDownloadEventHandler = () => {
1012
+ if (!_modelDownloadEventHandler) {
1013
+ _modelDownloadEventHandler = new EventHandler("__chrome_model__");
1014
+ }
1015
+ return _modelDownloadEventHandler;
1016
+ };
977
1017
  var _ChromeTranslator = class _ChromeTranslator {
978
1018
  constructor(translator) {
979
1019
  __publicField(this, "translator");
@@ -1013,7 +1053,12 @@ var _ChromeTranslator = class _ChromeTranslator {
1013
1053
  "downloadprogress",
1014
1054
  (e) => {
1015
1055
  isDownloading = true;
1016
- console.log(`Progress: ${Math.round(e.loaded / e.total * 100)}%`);
1056
+ const progress = e.total > 0 ? Math.round(e.loaded / e.total * 100) : 0;
1057
+ getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1058
+ e.loaded,
1059
+ e.total,
1060
+ progress
1061
+ );
1017
1062
  if (e.loaded === e.total) {
1018
1063
  resolveReady();
1019
1064
  }
@@ -1295,6 +1340,7 @@ function AITranslationInnerProvider(props) {
1295
1340
  enableAIT = true
1296
1341
  } = props;
1297
1342
  const [translationProgress, setTranslationProgress] = (0, import_react.useState)(null);
1343
+ const [modelDownloadProgress, setModelDownloadProgress] = (0, import_react.useState)(null);
1298
1344
  const [config2, setConfig] = (0, import_react.useState)(new Config());
1299
1345
  const translator = (0, import_react.useRef)(new Translator2(config2));
1300
1346
  const eventHandler = (0, import_react.useRef)(new EventHandler(tool));
@@ -1333,6 +1379,14 @@ function AITranslationInnerProvider(props) {
1333
1379
  );
1334
1380
  return () => unsubscribe();
1335
1381
  }, []);
1382
+ (0, import_react.useEffect)(() => {
1383
+ const unsubscribe = eventHandler.current.subscribeToModelDownloadProgressEvent(
1384
+ (progress) => {
1385
+ setModelDownloadProgress(progress);
1386
+ }
1387
+ );
1388
+ return () => unsubscribe();
1389
+ }, []);
1336
1390
  (0, import_react.useEffect)(() => {
1337
1391
  if (isFetched && remoteConfig && remoteConfig[tool]) {
1338
1392
  setConfig((prevConfig) => {
@@ -1357,6 +1411,7 @@ function AITranslationInnerProvider(props) {
1357
1411
  locale,
1358
1412
  renderVersion: renderVersionManager.getVersion(),
1359
1413
  translationProgress,
1414
+ modelDownloadProgress,
1360
1415
  tool
1361
1416
  };
1362
1417
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AITranslationContext.Provider, { value: contextValue, children });
@@ -1366,11 +1421,22 @@ function AITranslationProvider(props) {
1366
1421
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_query2.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AITranslationInnerProvider, { ...props }) });
1367
1422
  }
1368
1423
 
1424
+ // src/hooks/useAITranslation.ts
1425
+ function useAITranslation() {
1426
+ const ctx = (0, import_react2.useContext)(AITranslationContext);
1427
+ if (!ctx) {
1428
+ throw new Error(
1429
+ "useAITranslation must be used inside an AITranslationProvider"
1430
+ );
1431
+ }
1432
+ return ctx;
1433
+ }
1434
+
1369
1435
  // src/components/AITranslateText.tsx
1370
- var import_react3 = require("react");
1436
+ var import_react4 = require("react");
1371
1437
 
1372
1438
  // src/components/TranslatedIcon.tsx
1373
- var import_react2 = require("react");
1439
+ var import_react3 = require("react");
1374
1440
  var import_jsx_runtime2 = require("react/jsx-runtime");
1375
1441
  var TranslatedIcon = ({
1376
1442
  width = 14,
@@ -1406,11 +1472,11 @@ var AITranslateText = ({
1406
1472
  showHighlight = false,
1407
1473
  translatedIconProps
1408
1474
  }) => {
1409
- const context = (0, import_react3.useContext)(AITranslationContext);
1410
- const [displayText, setDisplayText] = (0, import_react3.useState)(text ?? "");
1411
- const [showHighlightState, setShowHighlightState] = (0, import_react3.useState)(showHighlight);
1412
- const eventHandlerRef = (0, import_react3.useRef)(null);
1413
- (0, import_react3.useEffect)(() => {
1475
+ const context = (0, import_react4.useContext)(AITranslationContext);
1476
+ const [displayText, setDisplayText] = (0, import_react4.useState)(text ?? "");
1477
+ const [showHighlightState, setShowHighlightState] = (0, import_react4.useState)(showHighlight);
1478
+ const eventHandlerRef = (0, import_react4.useRef)(null);
1479
+ (0, import_react4.useEffect)(() => {
1414
1480
  if (!context) return;
1415
1481
  if (!eventHandlerRef.current) {
1416
1482
  eventHandlerRef.current = new EventHandler(context.tool);
@@ -1427,14 +1493,14 @@ var AITranslateText = ({
1427
1493
  );
1428
1494
  return () => unsubscribe();
1429
1495
  }, [context, text]);
1430
- const reset = (0, import_react3.useCallback)(
1496
+ const reset = (0, import_react4.useCallback)(
1431
1497
  (displayValue = text) => {
1432
1498
  setDisplayText(displayValue);
1433
1499
  setShowHighlightState(false);
1434
1500
  },
1435
1501
  [text]
1436
1502
  );
1437
- (0, import_react3.useEffect)(() => {
1503
+ (0, import_react4.useEffect)(() => {
1438
1504
  if (text == null || text === "") {
1439
1505
  reset(text ?? "");
1440
1506
  return;
@@ -1484,5 +1550,6 @@ var getAITranslationLDId = (domain) => {
1484
1550
  AITranslationProvider,
1485
1551
  AI_TRANSLATION_FEATURE_FLAG_KEY,
1486
1552
  getAITranslationLDId,
1553
+ useAITranslation,
1487
1554
  useConfig
1488
1555
  });
@@ -184,6 +184,9 @@ function useConfig(toolName, options = {}) {
184
184
  });
185
185
  }
186
186
 
187
+ // src/hooks/useAITranslation.ts
188
+ import { useContext } from "react";
189
+
187
190
  // src/Provider.tsx
188
191
  import {
189
192
  createContext,
@@ -594,6 +597,7 @@ import { SystemEvents } from "@procore/web-sdk-events";
594
597
  var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
595
598
  var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
596
599
  var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
600
+ var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
597
601
 
598
602
  // src/utils/eventHandler.ts
599
603
  var EventHandler = class {
@@ -660,6 +664,34 @@ var EventHandler = class {
660
664
  }
661
665
  );
662
666
  }
667
+ /**
668
+ * Publishes a Chrome AI language-model download-progress event.
669
+ *
670
+ * Not tool-scoped — a Chrome language model is shared across the entire
671
+ * browser session, so all Provider instances receive it.
672
+ */
673
+ publishModelDownloadProgressEvent(loaded, total, progress) {
674
+ this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
675
+ loaded,
676
+ total,
677
+ progress,
678
+ isComplete: loaded === total
679
+ });
680
+ }
681
+ subscribeToModelDownloadProgressEvent(callback) {
682
+ return this.aiTranslationEvents.subscribe(
683
+ MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
684
+ (detail) => {
685
+ var _a, _b, _c, _d;
686
+ return callback({
687
+ loaded: ((_a = detail.data) == null ? void 0 : _a.loaded) ?? 0,
688
+ total: ((_b = detail.data) == null ? void 0 : _b.total) ?? 0,
689
+ progress: ((_c = detail.data) == null ? void 0 : _c.progress) ?? 0,
690
+ isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false
691
+ });
692
+ }
693
+ );
694
+ }
663
695
  withEventName(eventName) {
664
696
  return eventName + ":" + this.toolName;
665
697
  }
@@ -952,6 +984,13 @@ var ChromeLanguageDetector = class _ChromeLanguageDetector {
952
984
  };
953
985
 
954
986
  // src/translators/frontend_translators/chrome/Translator.ts
987
+ var _modelDownloadEventHandler = null;
988
+ var getModelDownloadEventHandler = () => {
989
+ if (!_modelDownloadEventHandler) {
990
+ _modelDownloadEventHandler = new EventHandler("__chrome_model__");
991
+ }
992
+ return _modelDownloadEventHandler;
993
+ };
955
994
  var _ChromeTranslator = class _ChromeTranslator {
956
995
  constructor(translator) {
957
996
  __publicField(this, "translator");
@@ -991,7 +1030,12 @@ var _ChromeTranslator = class _ChromeTranslator {
991
1030
  "downloadprogress",
992
1031
  (e) => {
993
1032
  isDownloading = true;
994
- console.log(`Progress: ${Math.round(e.loaded / e.total * 100)}%`);
1033
+ const progress = e.total > 0 ? Math.round(e.loaded / e.total * 100) : 0;
1034
+ getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1035
+ e.loaded,
1036
+ e.total,
1037
+ progress
1038
+ );
995
1039
  if (e.loaded === e.total) {
996
1040
  resolveReady();
997
1041
  }
@@ -1273,6 +1317,7 @@ function AITranslationInnerProvider(props) {
1273
1317
  enableAIT = true
1274
1318
  } = props;
1275
1319
  const [translationProgress, setTranslationProgress] = useState(null);
1320
+ const [modelDownloadProgress, setModelDownloadProgress] = useState(null);
1276
1321
  const [config2, setConfig] = useState(new Config());
1277
1322
  const translator = useRef(new Translator2(config2));
1278
1323
  const eventHandler = useRef(new EventHandler(tool));
@@ -1311,6 +1356,14 @@ function AITranslationInnerProvider(props) {
1311
1356
  );
1312
1357
  return () => unsubscribe();
1313
1358
  }, []);
1359
+ useEffect(() => {
1360
+ const unsubscribe = eventHandler.current.subscribeToModelDownloadProgressEvent(
1361
+ (progress) => {
1362
+ setModelDownloadProgress(progress);
1363
+ }
1364
+ );
1365
+ return () => unsubscribe();
1366
+ }, []);
1314
1367
  useEffect(() => {
1315
1368
  if (isFetched && remoteConfig && remoteConfig[tool]) {
1316
1369
  setConfig((prevConfig) => {
@@ -1335,6 +1388,7 @@ function AITranslationInnerProvider(props) {
1335
1388
  locale,
1336
1389
  renderVersion: renderVersionManager.getVersion(),
1337
1390
  translationProgress,
1391
+ modelDownloadProgress,
1338
1392
  tool
1339
1393
  };
1340
1394
  return /* @__PURE__ */ jsx(AITranslationContext.Provider, { value: contextValue, children });
@@ -1344,11 +1398,22 @@ function AITranslationProvider(props) {
1344
1398
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(AITranslationInnerProvider, { ...props }) });
1345
1399
  }
1346
1400
 
1401
+ // src/hooks/useAITranslation.ts
1402
+ function useAITranslation() {
1403
+ const ctx = useContext(AITranslationContext);
1404
+ if (!ctx) {
1405
+ throw new Error(
1406
+ "useAITranslation must be used inside an AITranslationProvider"
1407
+ );
1408
+ }
1409
+ return ctx;
1410
+ }
1411
+
1347
1412
  // src/components/AITranslateText.tsx
1348
1413
  import {
1349
1414
  useState as useState2,
1350
1415
  useEffect as useEffect2,
1351
- useContext,
1416
+ useContext as useContext2,
1352
1417
  useCallback as useCallback2,
1353
1418
  useRef as useRef2
1354
1419
  } from "react";
@@ -1390,7 +1455,7 @@ var AITranslateText = ({
1390
1455
  showHighlight = false,
1391
1456
  translatedIconProps
1392
1457
  }) => {
1393
- const context = useContext(AITranslationContext);
1458
+ const context = useContext2(AITranslationContext);
1394
1459
  const [displayText, setDisplayText] = useState2(text ?? "");
1395
1460
  const [showHighlightState, setShowHighlightState] = useState2(showHighlight);
1396
1461
  const eventHandlerRef = useRef2(null);
@@ -1467,5 +1532,6 @@ export {
1467
1532
  AITranslationProvider,
1468
1533
  AI_TRANSLATION_FEATURE_FLAG_KEY,
1469
1534
  getAITranslationLDId,
1535
+ useAITranslation,
1470
1536
  useConfig
1471
1537
  };
@@ -2,6 +2,20 @@ import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React, { ReactNode } from 'react';
4
4
 
5
+ interface TranslationProgress {
6
+ /**
7
+ * Progress percentage (0-100)
8
+ */
9
+ progress: number;
10
+ /**
11
+ * Current number of strings translated
12
+ */
13
+ current: number;
14
+ /**
15
+ * Total number of strings to translate
16
+ */
17
+ total: number;
18
+ }
5
19
  interface TranslationQueueEntry {
6
20
  originalText: string;
7
21
  isTranslated: boolean;
@@ -11,6 +25,26 @@ interface TranslationQueueEntry {
11
25
  tool: string;
12
26
  translationStrategy: 'frontend_translations' | 'backend_translations';
13
27
  }
28
+ interface ModelDownloadProgress {
29
+ /** Bytes already downloaded */
30
+ loaded: number;
31
+ /** Total bytes to download */
32
+ total: number;
33
+ /** Download percentage (0-100) */
34
+ progress: number;
35
+ /** True once the model has finished downloading */
36
+ isComplete: boolean;
37
+ }
38
+ interface AITranslationContextValue {
39
+ ait: (text: string) => Promise<string>;
40
+ locale: string;
41
+ renderVersion?: number;
42
+ /** Batch-translation progress; `null` while the queue is idle. */
43
+ translationProgress: TranslationProgress | null;
44
+ /** Chrome AI model download progress; `null` until a download begins. */
45
+ modelDownloadProgress: ModelDownloadProgress | null;
46
+ tool: string;
47
+ }
14
48
 
15
49
  interface ConfigurationResponse {
16
50
  [key: string]: ToolConfig;
@@ -63,6 +97,25 @@ interface UseConfigOptions {
63
97
  */
64
98
  declare function useConfig(toolName: string, options?: UseConfigOptions): _tanstack_react_query.UseQueryResult<ConfigurationResponse | null, unknown>;
65
99
 
100
+ /**
101
+ * Returns the full AI-translation context value provided by the nearest
102
+ * `AITranslationProvider`.
103
+ *
104
+ * Use this hook when you need more than one value from the context at once.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * function MyComponent() {
109
+ * const { ait, locale, translationProgress, modelDownloadProgress } = useAITranslation();
110
+ *
111
+ * return <span>{locale}</span>;
112
+ * }
113
+ * ```
114
+ *
115
+ * @throws if called outside of an `AITranslationProvider`.
116
+ */
117
+ declare function useAITranslation(): AITranslationContextValue;
118
+
66
119
  interface AITranslationProviderProps {
67
120
  children: ReactNode;
68
121
  locale: string;
@@ -123,4 +176,4 @@ declare global {
123
176
  var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
124
177
  }
125
178
 
126
- export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useConfig };
179
+ export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useAITranslation, useConfig };
@@ -2,6 +2,20 @@ import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React, { ReactNode } from 'react';
4
4
 
5
+ interface TranslationProgress {
6
+ /**
7
+ * Progress percentage (0-100)
8
+ */
9
+ progress: number;
10
+ /**
11
+ * Current number of strings translated
12
+ */
13
+ current: number;
14
+ /**
15
+ * Total number of strings to translate
16
+ */
17
+ total: number;
18
+ }
5
19
  interface TranslationQueueEntry {
6
20
  originalText: string;
7
21
  isTranslated: boolean;
@@ -11,6 +25,26 @@ interface TranslationQueueEntry {
11
25
  tool: string;
12
26
  translationStrategy: 'frontend_translations' | 'backend_translations';
13
27
  }
28
+ interface ModelDownloadProgress {
29
+ /** Bytes already downloaded */
30
+ loaded: number;
31
+ /** Total bytes to download */
32
+ total: number;
33
+ /** Download percentage (0-100) */
34
+ progress: number;
35
+ /** True once the model has finished downloading */
36
+ isComplete: boolean;
37
+ }
38
+ interface AITranslationContextValue {
39
+ ait: (text: string) => Promise<string>;
40
+ locale: string;
41
+ renderVersion?: number;
42
+ /** Batch-translation progress; `null` while the queue is idle. */
43
+ translationProgress: TranslationProgress | null;
44
+ /** Chrome AI model download progress; `null` until a download begins. */
45
+ modelDownloadProgress: ModelDownloadProgress | null;
46
+ tool: string;
47
+ }
14
48
 
15
49
  interface ConfigurationResponse {
16
50
  [key: string]: ToolConfig;
@@ -63,6 +97,25 @@ interface UseConfigOptions {
63
97
  */
64
98
  declare function useConfig(toolName: string, options?: UseConfigOptions): _tanstack_react_query.UseQueryResult<ConfigurationResponse | null, unknown>;
65
99
 
100
+ /**
101
+ * Returns the full AI-translation context value provided by the nearest
102
+ * `AITranslationProvider`.
103
+ *
104
+ * Use this hook when you need more than one value from the context at once.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * function MyComponent() {
109
+ * const { ait, locale, translationProgress, modelDownloadProgress } = useAITranslation();
110
+ *
111
+ * return <span>{locale}</span>;
112
+ * }
113
+ * ```
114
+ *
115
+ * @throws if called outside of an `AITranslationProvider`.
116
+ */
117
+ declare function useAITranslation(): AITranslationContextValue;
118
+
66
119
  interface AITranslationProviderProps {
67
120
  children: ReactNode;
68
121
  locale: string;
@@ -123,4 +176,4 @@ declare global {
123
176
  var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
124
177
  }
125
178
 
126
- export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useConfig };
179
+ export { AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type TranslatedIconProps, type UseConfigOptions, getAITranslationLDId, useAITranslation, useConfig };
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  AITranslationProvider: () => AITranslationProvider,
25
25
  AI_TRANSLATION_FEATURE_FLAG_KEY: () => AI_TRANSLATION_FEATURE_FLAG_KEY,
26
26
  getAITranslationLDId: () => getAITranslationLDId,
27
+ useAITranslation: () => useAITranslation,
27
28
  useConfig: () => useConfig
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
@@ -208,6 +209,9 @@ function useConfig(toolName, options = {}) {
208
209
  });
209
210
  }
210
211
 
212
+ // src/hooks/useAITranslation.ts
213
+ var import_react2 = require("react");
214
+
211
215
  // src/Provider.tsx
212
216
  var import_react = require("react");
213
217
 
@@ -607,6 +611,7 @@ var import_web_sdk_events = require("@procore/web-sdk-events");
607
611
  var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
608
612
  var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
609
613
  var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
614
+ var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
610
615
 
611
616
  // src/utils/eventHandler.ts
612
617
  var EventHandler = class {
@@ -666,6 +671,31 @@ var EventHandler = class {
666
671
  )
667
672
  );
668
673
  }
674
+ /**
675
+ * Publishes a Chrome AI language-model download-progress event.
676
+ *
677
+ * Not tool-scoped — a Chrome language model is shared across the entire
678
+ * browser session, so all Provider instances receive it.
679
+ */
680
+ publishModelDownloadProgressEvent(loaded, total, progress) {
681
+ this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
682
+ loaded,
683
+ total,
684
+ progress,
685
+ isComplete: loaded === total
686
+ });
687
+ }
688
+ subscribeToModelDownloadProgressEvent(callback) {
689
+ return this.aiTranslationEvents.subscribe(
690
+ MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
691
+ (detail) => callback({
692
+ loaded: detail.data?.loaded ?? 0,
693
+ total: detail.data?.total ?? 0,
694
+ progress: detail.data?.progress ?? 0,
695
+ isComplete: detail.data?.isComplete ?? false
696
+ })
697
+ );
698
+ }
669
699
  withEventName(eventName) {
670
700
  return eventName + ":" + this.toolName;
671
701
  }
@@ -955,6 +985,13 @@ var ChromeLanguageDetector = class _ChromeLanguageDetector {
955
985
  };
956
986
 
957
987
  // src/translators/frontend_translators/chrome/Translator.ts
988
+ var _modelDownloadEventHandler = null;
989
+ var getModelDownloadEventHandler = () => {
990
+ if (!_modelDownloadEventHandler) {
991
+ _modelDownloadEventHandler = new EventHandler("__chrome_model__");
992
+ }
993
+ return _modelDownloadEventHandler;
994
+ };
958
995
  var ChromeTranslator = class _ChromeTranslator {
959
996
  translator;
960
997
  constructor(translator) {
@@ -995,7 +1032,12 @@ var ChromeTranslator = class _ChromeTranslator {
995
1032
  "downloadprogress",
996
1033
  (e) => {
997
1034
  isDownloading = true;
998
- console.log(`Progress: ${Math.round(e.loaded / e.total * 100)}%`);
1035
+ const progress = e.total > 0 ? Math.round(e.loaded / e.total * 100) : 0;
1036
+ getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1037
+ e.loaded,
1038
+ e.total,
1039
+ progress
1040
+ );
999
1041
  if (e.loaded === e.total) {
1000
1042
  resolveReady();
1001
1043
  }
@@ -1274,6 +1316,7 @@ function AITranslationInnerProvider(props) {
1274
1316
  enableAIT = true
1275
1317
  } = props;
1276
1318
  const [translationProgress, setTranslationProgress] = (0, import_react.useState)(null);
1319
+ const [modelDownloadProgress, setModelDownloadProgress] = (0, import_react.useState)(null);
1277
1320
  const [config2, setConfig] = (0, import_react.useState)(new Config());
1278
1321
  const translator = (0, import_react.useRef)(new Translator2(config2));
1279
1322
  const eventHandler = (0, import_react.useRef)(new EventHandler(tool));
@@ -1312,6 +1355,14 @@ function AITranslationInnerProvider(props) {
1312
1355
  );
1313
1356
  return () => unsubscribe();
1314
1357
  }, []);
1358
+ (0, import_react.useEffect)(() => {
1359
+ const unsubscribe = eventHandler.current.subscribeToModelDownloadProgressEvent(
1360
+ (progress) => {
1361
+ setModelDownloadProgress(progress);
1362
+ }
1363
+ );
1364
+ return () => unsubscribe();
1365
+ }, []);
1315
1366
  (0, import_react.useEffect)(() => {
1316
1367
  if (isFetched && remoteConfig && remoteConfig[tool]) {
1317
1368
  setConfig((prevConfig) => {
@@ -1336,6 +1387,7 @@ function AITranslationInnerProvider(props) {
1336
1387
  locale,
1337
1388
  renderVersion: renderVersionManager.getVersion(),
1338
1389
  translationProgress,
1390
+ modelDownloadProgress,
1339
1391
  tool
1340
1392
  };
1341
1393
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AITranslationContext.Provider, { value: contextValue, children });
@@ -1345,11 +1397,22 @@ function AITranslationProvider(props) {
1345
1397
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_query2.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AITranslationInnerProvider, { ...props }) });
1346
1398
  }
1347
1399
 
1400
+ // src/hooks/useAITranslation.ts
1401
+ function useAITranslation() {
1402
+ const ctx = (0, import_react2.useContext)(AITranslationContext);
1403
+ if (!ctx) {
1404
+ throw new Error(
1405
+ "useAITranslation must be used inside an AITranslationProvider"
1406
+ );
1407
+ }
1408
+ return ctx;
1409
+ }
1410
+
1348
1411
  // src/components/AITranslateText.tsx
1349
- var import_react3 = require("react");
1412
+ var import_react4 = require("react");
1350
1413
 
1351
1414
  // src/components/TranslatedIcon.tsx
1352
- var import_react2 = require("react");
1415
+ var import_react3 = require("react");
1353
1416
  var import_jsx_runtime2 = require("react/jsx-runtime");
1354
1417
  var TranslatedIcon = ({
1355
1418
  width = 14,
@@ -1385,11 +1448,11 @@ var AITranslateText = ({
1385
1448
  showHighlight = false,
1386
1449
  translatedIconProps
1387
1450
  }) => {
1388
- const context = (0, import_react3.useContext)(AITranslationContext);
1389
- const [displayText, setDisplayText] = (0, import_react3.useState)(text ?? "");
1390
- const [showHighlightState, setShowHighlightState] = (0, import_react3.useState)(showHighlight);
1391
- const eventHandlerRef = (0, import_react3.useRef)(null);
1392
- (0, import_react3.useEffect)(() => {
1451
+ const context = (0, import_react4.useContext)(AITranslationContext);
1452
+ const [displayText, setDisplayText] = (0, import_react4.useState)(text ?? "");
1453
+ const [showHighlightState, setShowHighlightState] = (0, import_react4.useState)(showHighlight);
1454
+ const eventHandlerRef = (0, import_react4.useRef)(null);
1455
+ (0, import_react4.useEffect)(() => {
1393
1456
  if (!context) return;
1394
1457
  if (!eventHandlerRef.current) {
1395
1458
  eventHandlerRef.current = new EventHandler(context.tool);
@@ -1406,14 +1469,14 @@ var AITranslateText = ({
1406
1469
  );
1407
1470
  return () => unsubscribe();
1408
1471
  }, [context, text]);
1409
- const reset = (0, import_react3.useCallback)(
1472
+ const reset = (0, import_react4.useCallback)(
1410
1473
  (displayValue = text) => {
1411
1474
  setDisplayText(displayValue);
1412
1475
  setShowHighlightState(false);
1413
1476
  },
1414
1477
  [text]
1415
1478
  );
1416
- (0, import_react3.useEffect)(() => {
1479
+ (0, import_react4.useEffect)(() => {
1417
1480
  if (text == null || text === "") {
1418
1481
  reset(text ?? "");
1419
1482
  return;
@@ -1463,5 +1526,6 @@ var getAITranslationLDId = (domain) => {
1463
1526
  AITranslationProvider,
1464
1527
  AI_TRANSLATION_FEATURE_FLAG_KEY,
1465
1528
  getAITranslationLDId,
1529
+ useAITranslation,
1466
1530
  useConfig
1467
1531
  });
@@ -178,6 +178,9 @@ function useConfig(toolName, options = {}) {
178
178
  });
179
179
  }
180
180
 
181
+ // src/hooks/useAITranslation.ts
182
+ import { useContext } from "react";
183
+
181
184
  // src/Provider.tsx
182
185
  import {
183
186
  createContext,
@@ -583,6 +586,7 @@ import { SystemEvents } from "@procore/web-sdk-events";
583
586
  var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
584
587
  var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
585
588
  var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
589
+ var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
586
590
 
587
591
  // src/utils/eventHandler.ts
588
592
  var EventHandler = class {
@@ -642,6 +646,31 @@ var EventHandler = class {
642
646
  )
643
647
  );
644
648
  }
649
+ /**
650
+ * Publishes a Chrome AI language-model download-progress event.
651
+ *
652
+ * Not tool-scoped — a Chrome language model is shared across the entire
653
+ * browser session, so all Provider instances receive it.
654
+ */
655
+ publishModelDownloadProgressEvent(loaded, total, progress) {
656
+ this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
657
+ loaded,
658
+ total,
659
+ progress,
660
+ isComplete: loaded === total
661
+ });
662
+ }
663
+ subscribeToModelDownloadProgressEvent(callback) {
664
+ return this.aiTranslationEvents.subscribe(
665
+ MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
666
+ (detail) => callback({
667
+ loaded: detail.data?.loaded ?? 0,
668
+ total: detail.data?.total ?? 0,
669
+ progress: detail.data?.progress ?? 0,
670
+ isComplete: detail.data?.isComplete ?? false
671
+ })
672
+ );
673
+ }
645
674
  withEventName(eventName) {
646
675
  return eventName + ":" + this.toolName;
647
676
  }
@@ -931,6 +960,13 @@ var ChromeLanguageDetector = class _ChromeLanguageDetector {
931
960
  };
932
961
 
933
962
  // src/translators/frontend_translators/chrome/Translator.ts
963
+ var _modelDownloadEventHandler = null;
964
+ var getModelDownloadEventHandler = () => {
965
+ if (!_modelDownloadEventHandler) {
966
+ _modelDownloadEventHandler = new EventHandler("__chrome_model__");
967
+ }
968
+ return _modelDownloadEventHandler;
969
+ };
934
970
  var ChromeTranslator = class _ChromeTranslator {
935
971
  translator;
936
972
  constructor(translator) {
@@ -971,7 +1007,12 @@ var ChromeTranslator = class _ChromeTranslator {
971
1007
  "downloadprogress",
972
1008
  (e) => {
973
1009
  isDownloading = true;
974
- console.log(`Progress: ${Math.round(e.loaded / e.total * 100)}%`);
1010
+ const progress = e.total > 0 ? Math.round(e.loaded / e.total * 100) : 0;
1011
+ getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1012
+ e.loaded,
1013
+ e.total,
1014
+ progress
1015
+ );
975
1016
  if (e.loaded === e.total) {
976
1017
  resolveReady();
977
1018
  }
@@ -1250,6 +1291,7 @@ function AITranslationInnerProvider(props) {
1250
1291
  enableAIT = true
1251
1292
  } = props;
1252
1293
  const [translationProgress, setTranslationProgress] = useState(null);
1294
+ const [modelDownloadProgress, setModelDownloadProgress] = useState(null);
1253
1295
  const [config2, setConfig] = useState(new Config());
1254
1296
  const translator = useRef(new Translator2(config2));
1255
1297
  const eventHandler = useRef(new EventHandler(tool));
@@ -1288,6 +1330,14 @@ function AITranslationInnerProvider(props) {
1288
1330
  );
1289
1331
  return () => unsubscribe();
1290
1332
  }, []);
1333
+ useEffect(() => {
1334
+ const unsubscribe = eventHandler.current.subscribeToModelDownloadProgressEvent(
1335
+ (progress) => {
1336
+ setModelDownloadProgress(progress);
1337
+ }
1338
+ );
1339
+ return () => unsubscribe();
1340
+ }, []);
1291
1341
  useEffect(() => {
1292
1342
  if (isFetched && remoteConfig && remoteConfig[tool]) {
1293
1343
  setConfig((prevConfig) => {
@@ -1312,6 +1362,7 @@ function AITranslationInnerProvider(props) {
1312
1362
  locale,
1313
1363
  renderVersion: renderVersionManager.getVersion(),
1314
1364
  translationProgress,
1365
+ modelDownloadProgress,
1315
1366
  tool
1316
1367
  };
1317
1368
  return /* @__PURE__ */ jsx(AITranslationContext.Provider, { value: contextValue, children });
@@ -1321,11 +1372,22 @@ function AITranslationProvider(props) {
1321
1372
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(AITranslationInnerProvider, { ...props }) });
1322
1373
  }
1323
1374
 
1375
+ // src/hooks/useAITranslation.ts
1376
+ function useAITranslation() {
1377
+ const ctx = useContext(AITranslationContext);
1378
+ if (!ctx) {
1379
+ throw new Error(
1380
+ "useAITranslation must be used inside an AITranslationProvider"
1381
+ );
1382
+ }
1383
+ return ctx;
1384
+ }
1385
+
1324
1386
  // src/components/AITranslateText.tsx
1325
1387
  import {
1326
1388
  useState as useState2,
1327
1389
  useEffect as useEffect2,
1328
- useContext,
1390
+ useContext as useContext2,
1329
1391
  useCallback as useCallback2,
1330
1392
  useRef as useRef2
1331
1393
  } from "react";
@@ -1367,7 +1429,7 @@ var AITranslateText = ({
1367
1429
  showHighlight = false,
1368
1430
  translatedIconProps
1369
1431
  }) => {
1370
- const context = useContext(AITranslationContext);
1432
+ const context = useContext2(AITranslationContext);
1371
1433
  const [displayText, setDisplayText] = useState2(text ?? "");
1372
1434
  const [showHighlightState, setShowHighlightState] = useState2(showHighlight);
1373
1435
  const eventHandlerRef = useRef2(null);
@@ -1444,5 +1506,6 @@ export {
1444
1506
  AITranslationProvider,
1445
1507
  AI_TRANSLATION_FEATURE_FLAG_KEY,
1446
1508
  getAITranslationLDId,
1509
+ useAITranslation,
1447
1510
  useConfig
1448
1511
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@procore/ai-translations",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Library that provides a solution to use AI to translate text into a language",
5
5
  "main": "dist/legacy/index.js",
6
6
  "types": "dist/legacy/index.d.ts",
@@ -49,6 +49,8 @@
49
49
  "cypress:run:component": "cypress run --component --browser chrome ",
50
50
  "cypress:run:headed": "cypress run --headed --browser chrome",
51
51
  "test:cypress": "yarn cypress:run",
52
+ "test:integration": "hammer test src/__integration__",
53
+ "cypress:run:integration": "cypress run --component --browser chrome --spec 'cypress/component/integration/**'",
52
54
  "storybook": "storybook dev -p 6006",
53
55
  "build-storybook": "storybook build"
54
56
  },
@@ -87,6 +89,7 @@
87
89
  "@storybook/addon-essentials": "^7.6.20",
88
90
  "@storybook/addon-a11y": "^7.6.20",
89
91
  "@procore/storybook-addon": "^4.5.1",
90
- "eslint-plugin-storybook": "^0.11.1"
92
+ "eslint-plugin-storybook": "^0.11.1",
93
+ "fake-indexeddb": "^6.0.0"
91
94
  }
92
95
  }