@procore/ai-translations 0.8.0 → 0.9.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.
@@ -678,6 +678,7 @@ var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
678
678
  var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
679
679
  var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
680
680
  var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
681
+ var MODEL_CONSENT_REQUIRED_EVENT_NAME = "ai-translations-model-consent-required";
681
682
 
682
683
  // src/utils/eventHandler.ts
683
684
  var EventHandler = class {
@@ -758,24 +759,51 @@ var EventHandler = class {
758
759
  * Not tool-scoped — a Chrome language model is shared across the entire
759
760
  * browser session, so all Provider instances receive it.
760
761
  */
761
- publishModelDownloadProgressEvent(loaded, total, progress) {
762
+ publishModelDownloadProgressEvent(loaded, total, progress, sourceLanguage, targetLanguage) {
762
763
  this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
763
764
  loaded,
764
765
  total,
765
766
  progress,
766
- isComplete: loaded === total
767
+ isComplete: loaded === total,
768
+ sourceLanguage,
769
+ targetLanguage
767
770
  });
768
771
  }
769
772
  subscribeToModelDownloadProgressEvent(callback) {
770
773
  return this.aiTranslationEvents.subscribe(
771
774
  MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
772
775
  (detail) => {
773
- var _a, _b, _c, _d;
776
+ var _a, _b, _c, _d, _e, _f;
774
777
  return callback({
775
778
  loaded: ((_a = detail.data) == null ? void 0 : _a.loaded) ?? 0,
776
779
  total: ((_b = detail.data) == null ? void 0 : _b.total) ?? 0,
777
780
  progress: ((_c = detail.data) == null ? void 0 : _c.progress) ?? 0,
778
- isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false
781
+ isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false,
782
+ sourceLanguage: ((_e = detail.data) == null ? void 0 : _e.sourceLanguage) ?? "",
783
+ targetLanguage: ((_f = detail.data) == null ? void 0 : _f.targetLanguage) ?? ""
784
+ });
785
+ }
786
+ );
787
+ }
788
+ /**
789
+ * Publishes a request for the user to consent to downloading a specific
790
+ * source -> target language model. Like the download-progress event this is
791
+ * NOT tool-scoped — a language model is shared across the whole session.
792
+ */
793
+ publishModelConsentRequiredEvent(sourceLanguage, targetLanguage) {
794
+ this.aiTranslationEvents.publish(MODEL_CONSENT_REQUIRED_EVENT_NAME, {
795
+ sourceLanguage,
796
+ targetLanguage
797
+ });
798
+ }
799
+ subscribeToModelConsentRequiredEvent(callback) {
800
+ return this.aiTranslationEvents.subscribe(
801
+ MODEL_CONSENT_REQUIRED_EVENT_NAME,
802
+ (detail) => {
803
+ var _a, _b;
804
+ return callback({
805
+ sourceLanguage: ((_a = detail.data) == null ? void 0 : _a.sourceLanguage) ?? "",
806
+ targetLanguage: ((_b = detail.data) == null ? void 0 : _b.targetLanguage) ?? ""
779
807
  });
780
808
  }
781
809
  );
@@ -785,6 +813,113 @@ var EventHandler = class {
785
813
  }
786
814
  };
787
815
 
816
+ // src/utils/modelDownloadGate.ts
817
+ var ModelDownloadGateImpl = class _ModelDownloadGateImpl {
818
+ constructor() {
819
+ __publicField(this, "eventHandler", null);
820
+ __publicField(this, "ready", /* @__PURE__ */ new Set());
821
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
822
+ __publicField(this, "resolvers", /* @__PURE__ */ new Map());
823
+ __publicField(this, "rejecters", /* @__PURE__ */ new Map());
824
+ }
825
+ getEventHandler() {
826
+ if (!this.eventHandler) {
827
+ this.eventHandler = new EventHandler("__chrome_model__");
828
+ }
829
+ return this.eventHandler;
830
+ }
831
+ static key(sourceLanguage, targetLanguage) {
832
+ return `${sourceLanguage}->${targetLanguage}`;
833
+ }
834
+ /**
835
+ * Resolves once the model for `sourceLanguage -> targetLanguage` is downloaded
836
+ * and ready. Pauses (stays pending) until the user confirms the download and
837
+ * it completes. Re-prompts are deduped per model.
838
+ */
839
+ ensureModelReady(sourceLanguage, targetLanguage) {
840
+ const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
841
+ if (this.ready.has(key)) {
842
+ return Promise.resolve();
843
+ }
844
+ const existing = this.pending.get(key);
845
+ if (existing) {
846
+ return existing;
847
+ }
848
+ const promise = new Promise((resolve, reject) => {
849
+ this.resolvers.set(key, resolve);
850
+ this.rejecters.set(key, reject);
851
+ });
852
+ this.pending.set(key, promise);
853
+ this.getEventHandler().publishModelConsentRequiredEvent(
854
+ sourceLanguage,
855
+ targetLanguage
856
+ );
857
+ return promise;
858
+ }
859
+ /**
860
+ * Marks a model as ready and resolves any waiter. Resolved exclusively when
861
+ * `downloadModel` resolves (wired in the Provider). This guarantees Chrome's
862
+ * `Translator.create()` has returned and the translator instance is cached in
863
+ * `chromeTranslatorRegistry`, so the resumed translation reuses it instead of
864
+ * calling `Translator.create()` again outside the user gesture (which throws
865
+ * `NotAllowedError`). It is deliberately NOT resolved on the intermediate
866
+ * `MODEL_DOWNLOAD_PROGRESS` `isComplete` event, which fires before
867
+ * `Translator.create()` returns.
868
+ */
869
+ markReady(sourceLanguage, targetLanguage) {
870
+ const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
871
+ this.ready.add(key);
872
+ const resolve = this.resolvers.get(key);
873
+ if (resolve) {
874
+ resolve();
875
+ }
876
+ this.clearPending(key);
877
+ }
878
+ /**
879
+ * Marks a model download as failed and rejects any waiter so the paused
880
+ * translation run can move past this text instead of hanging forever. Called
881
+ * when `downloadModel` rejects (e.g. Chrome returns 404 for a pair it
882
+ * reported as downloadable). The pair is NOT cached as ready, so it can be
883
+ * re-attempted later.
884
+ */
885
+ markFailed(sourceLanguage, targetLanguage, error) {
886
+ const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
887
+ const reject = this.rejecters.get(key);
888
+ if (reject) {
889
+ reject(
890
+ error instanceof Error ? error : new Error(`Model download failed for ${key}`)
891
+ );
892
+ }
893
+ this.clearPending(key);
894
+ }
895
+ isReady(sourceLanguage, targetLanguage) {
896
+ return this.ready.has(
897
+ _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage)
898
+ );
899
+ }
900
+ /**
901
+ * Clears all gate state. Intended for test teardown so cached "ready" models
902
+ * do not leak between cases.
903
+ */
904
+ reset() {
905
+ this.ready.clear();
906
+ this.pending.clear();
907
+ this.resolvers.clear();
908
+ this.rejecters.clear();
909
+ }
910
+ clearPending(key) {
911
+ this.resolvers.delete(key);
912
+ this.rejecters.delete(key);
913
+ this.pending.delete(key);
914
+ }
915
+ };
916
+ var modelDownloadGate = new ModelDownloadGateImpl();
917
+
918
+ // src/utils/language.ts
919
+ function normalizeLanguage(language) {
920
+ return language.split("-")[0] ?? language;
921
+ }
922
+
788
923
  // src/utils/renderVersionManager.ts
789
924
  var RenderVersionManager = class {
790
925
  constructor() {
@@ -843,27 +978,30 @@ var TranslationManager = class {
843
978
  this.translationProgress.total = queueManager.queueSize(
844
979
  this.currentTranslatorStrategy
845
980
  );
846
- do {
847
- for (const batch of queueManager.generateBatches(
848
- this.translator.getConfig()
849
- )) {
850
- batch.forEach((entry) => this.mfeToBeNotified.add(entry.tool));
851
- const result = await this.translator.processTranslations(
852
- batch.map(
853
- (entry) => this.convertTranslationQueueEntryToTranslationRequest(entry)
854
- )
855
- );
856
- this.setTranslationProgress(batch.length);
857
- await this.updateDatabaseWithTranslations(result);
858
- await this.notifyTranslationCompleted();
981
+ try {
982
+ do {
983
+ for (const batch of queueManager.generateBatches(
984
+ this.translator.getConfig()
985
+ )) {
986
+ batch.forEach((entry) => this.mfeToBeNotified.add(entry.tool));
987
+ const result = await this.translator.processTranslations(
988
+ batch.map(
989
+ (entry) => this.convertTranslationQueueEntryToTranslationRequest(entry)
990
+ )
991
+ );
992
+ this.setTranslationProgress(batch.length);
993
+ await this.updateDatabaseWithTranslations(result);
994
+ await this.notifyTranslationCompleted();
995
+ }
996
+ } while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
997
+ this.resetTranslationProgress();
998
+ this.publishTranslationProgress();
999
+ } finally {
1000
+ if (this.currentTranslatorStrategy === "frontend_translations") {
1001
+ globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
1002
+ } else {
1003
+ globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
859
1004
  }
860
- } while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
861
- this.resetTranslationProgress();
862
- this.publishTranslationProgress();
863
- if (this.currentTranslatorStrategy === "frontend_translations") {
864
- globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
865
- } else {
866
- globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
867
1005
  }
868
1006
  }
869
1007
  async updateDatabaseWithTranslations(translationResult) {
@@ -1033,6 +1171,16 @@ var Client = class {
1033
1171
  return this.createTranslationErrorResponse(requests, errorMessage);
1034
1172
  }
1035
1173
  }
1174
+ /**
1175
+ * No-op: backend translation does not manage on-device models, so there is
1176
+ * nothing to download client-side. The parameters are kept to match the
1177
+ * {@link TranslatorClientAdapter.downloadModel} signature and make the no-op
1178
+ * intent explicit to callers.
1179
+ */
1180
+ async downloadModel(sourceLanguage, targetLanguage) {
1181
+ void sourceLanguage;
1182
+ void targetLanguage;
1183
+ }
1036
1184
  createTranslationErrorResponse(requests, errorMessage) {
1037
1185
  return {
1038
1186
  responseBody: requests.map((req) => ({
@@ -1152,6 +1300,7 @@ var _ChromeTranslator = class _ChromeTranslator {
1152
1300
  }
1153
1301
  }
1154
1302
  static async init(sourceLanguage, targetLanguage) {
1303
+ this.ensureRegistry();
1155
1304
  if (globalThis.chromeTranslatorRegistry.has(
1156
1305
  this.generateKey(sourceLanguage, targetLanguage)
1157
1306
  )) {
@@ -1166,7 +1315,30 @@ var _ChromeTranslator = class _ChromeTranslator {
1166
1315
  new _ChromeTranslator(translator)
1167
1316
  );
1168
1317
  }
1169
- static async createTranslator(source, target) {
1318
+ /**
1319
+ * Downloads the on-device model for a source -> target language pair. Must be
1320
+ * invoked from a user gesture (e.g. a click handler), since Chrome requires an
1321
+ * active gesture to start a model download.
1322
+ */
1323
+ static async downloadModel(sourceLanguage, targetLanguage) {
1324
+ await this.init(
1325
+ normalizeLanguage(sourceLanguage),
1326
+ normalizeLanguage(targetLanguage)
1327
+ );
1328
+ }
1329
+ /**
1330
+ * Creates a Chrome translator instance for the pair.
1331
+ *
1332
+ * @param publishProgress - When true, `downloadprogress` events are forwarded
1333
+ * as `MODEL_DOWNLOAD_PROGRESS` events (driving the download-progress popup).
1334
+ * This should only be true for an explicit, consent-driven download
1335
+ * (`init`/`downloadModel`). When creating an already-downloaded
1336
+ * (`available`) model in the translate loop, Chrome still emits
1337
+ * `downloadprogress` while loading the pack from disk into memory; forwarding
1338
+ * those would wrongly show a "downloading" popup for a model that is already
1339
+ * downloaded, so the translate-loop path passes `false`.
1340
+ */
1341
+ static async createTranslator(source, target, publishProgress = true) {
1170
1342
  const key = this.generateKey(source, target);
1171
1343
  let resolveReady;
1172
1344
  const readyPromise = new Promise((resolve) => {
@@ -1183,11 +1355,15 @@ var _ChromeTranslator = class _ChromeTranslator {
1183
1355
  const loaded = e.loaded ?? 0;
1184
1356
  const total = e.total ?? 0;
1185
1357
  const progress = total > 0 ? Math.round(loaded / total * 100) : 0;
1186
- getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1187
- loaded,
1188
- total,
1189
- progress
1190
- );
1358
+ if (publishProgress) {
1359
+ getModelDownloadEventHandler().publishModelDownloadProgressEvent(
1360
+ loaded,
1361
+ total,
1362
+ progress,
1363
+ source,
1364
+ target
1365
+ );
1366
+ }
1191
1367
  if (total > 0 && loaded >= total) {
1192
1368
  resolveReady();
1193
1369
  }
@@ -1205,26 +1381,27 @@ var _ChromeTranslator = class _ChromeTranslator {
1205
1381
  static async translate(text, targetLanguage) {
1206
1382
  var _a;
1207
1383
  this.ensureRegistry();
1384
+ const normalizedTarget = normalizeLanguage(targetLanguage);
1385
+ let sourceLanguage = normalizedTarget;
1208
1386
  try {
1209
1387
  const detector = await ChromeLanguageDetector.getInstance();
1210
- const sourceLanguage = await detector.detectLanguage(text);
1211
- const key = this.generateKey(sourceLanguage, targetLanguage);
1212
- if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(targetLanguage)) {
1388
+ sourceLanguage = normalizeLanguage(await detector.detectLanguage(text));
1389
+ if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(normalizedTarget)) {
1213
1390
  return {
1214
1391
  translation: text,
1215
1392
  sourceLanguage,
1216
1393
  success: false,
1217
1394
  retryable: false,
1218
- errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${targetLanguage}`
1395
+ errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${normalizedTarget}`
1219
1396
  };
1220
1397
  }
1221
- const existingInstance = globalThis.chromeTranslatorRegistry.get(key);
1222
- if (!existingInstance) {
1223
- const translatorCapabilities = await Translator.availability({
1398
+ const key = this.generateKey(sourceLanguage, normalizedTarget);
1399
+ if (!globalThis.chromeTranslatorRegistry.has(key)) {
1400
+ const availability = await Translator.availability({
1224
1401
  sourceLanguage,
1225
- targetLanguage
1402
+ targetLanguage: normalizedTarget
1226
1403
  });
1227
- if (translatorCapabilities === "unavailable") {
1404
+ if (availability === "unavailable") {
1228
1405
  return {
1229
1406
  translation: text,
1230
1407
  sourceLanguage,
@@ -1233,47 +1410,95 @@ var _ChromeTranslator = class _ChromeTranslator {
1233
1410
  errorMessage: "Translation not supported"
1234
1411
  };
1235
1412
  }
1236
- const chromeTranslator = await this.createTranslator(
1237
- sourceLanguage,
1238
- targetLanguage
1239
- );
1240
- if (!globalThis.chromeTranslatorRegistry.has(key)) {
1241
- globalThis.chromeTranslatorRegistry.set(
1242
- key,
1243
- new _ChromeTranslator(chromeTranslator)
1244
- );
1413
+ if (availability !== "available") {
1414
+ const isUserActivationActive = typeof navigator !== "undefined" && (((_a = navigator.userActivation) == null ? void 0 : _a.isActive) ?? false);
1415
+ if (isUserActivationActive) {
1416
+ await _ChromeTranslator.downloadModel(
1417
+ sourceLanguage,
1418
+ normalizedTarget
1419
+ );
1420
+ modelDownloadGate.markReady(sourceLanguage, normalizedTarget);
1421
+ } else {
1422
+ await modelDownloadGate.ensureModelReady(
1423
+ sourceLanguage,
1424
+ normalizedTarget
1425
+ );
1426
+ }
1245
1427
  }
1246
1428
  }
1247
- const instance = globalThis.chromeTranslatorRegistry.get(key);
1248
- await this.waitForReady(key);
1249
- this.translatorReadyPromises.delete(key);
1250
- const translation = await ((_a = instance.translator) == null ? void 0 : _a.translate(text));
1251
- if (!translation) {
1252
- return {
1253
- translation: text,
1254
- sourceLanguage,
1255
- success: false,
1256
- errorMessage: "Translation failed",
1257
- retryable: false
1258
- };
1259
- }
1260
- return {
1261
- translation,
1429
+ return await this.createInstanceAndTranslate(
1430
+ text,
1262
1431
  sourceLanguage,
1263
- success: true,
1264
- retryable: true
1265
- };
1432
+ normalizedTarget
1433
+ );
1266
1434
  } catch (error) {
1435
+ const isGestureRequired = error instanceof DOMException && error.name === "NotAllowedError";
1436
+ if (isGestureRequired) {
1437
+ try {
1438
+ await modelDownloadGate.ensureModelReady(
1439
+ sourceLanguage,
1440
+ normalizedTarget
1441
+ );
1442
+ return await this.createInstanceAndTranslate(
1443
+ text,
1444
+ sourceLanguage,
1445
+ normalizedTarget
1446
+ );
1447
+ } catch (retryError) {
1448
+ void retryError;
1449
+ }
1450
+ }
1267
1451
  const message = error instanceof Error ? error.message : "Unknown error";
1268
1452
  return {
1269
1453
  translation: text,
1270
- sourceLanguage: targetLanguage,
1454
+ sourceLanguage,
1271
1455
  success: false,
1272
1456
  errorMessage: message,
1273
- retryable: true
1457
+ retryable: false,
1458
+ requiresGesture: isGestureRequired
1274
1459
  };
1275
1460
  }
1276
1461
  }
1462
+ /**
1463
+ * Creates the translator instance for the pair if it is not already cached,
1464
+ * waits for the model to be ready, and translates the text. The model is
1465
+ * expected to already be downloaded (or downloadable without a gesture) by
1466
+ * the time this is called.
1467
+ */
1468
+ static async createInstanceAndTranslate(text, sourceLanguage, targetLanguage) {
1469
+ var _a;
1470
+ const key = this.generateKey(sourceLanguage, targetLanguage);
1471
+ if (!globalThis.chromeTranslatorRegistry.has(key)) {
1472
+ const chromeTranslator = await this.createTranslator(
1473
+ sourceLanguage,
1474
+ targetLanguage,
1475
+ false
1476
+ );
1477
+ globalThis.chromeTranslatorRegistry.set(
1478
+ key,
1479
+ new _ChromeTranslator(chromeTranslator)
1480
+ );
1481
+ }
1482
+ const instance = globalThis.chromeTranslatorRegistry.get(key);
1483
+ await this.waitForReady(key);
1484
+ this.translatorReadyPromises.delete(key);
1485
+ const translation = await ((_a = instance.translator) == null ? void 0 : _a.translate(text));
1486
+ if (!translation) {
1487
+ return {
1488
+ translation: text,
1489
+ sourceLanguage,
1490
+ success: false,
1491
+ errorMessage: "Translation failed",
1492
+ retryable: false
1493
+ };
1494
+ }
1495
+ return {
1496
+ translation,
1497
+ sourceLanguage,
1498
+ success: true,
1499
+ retryable: true
1500
+ };
1501
+ }
1277
1502
  static generateKey(sourceLanguage, targetLanguage) {
1278
1503
  return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
1279
1504
  }
@@ -1317,7 +1542,8 @@ var Client2 = class {
1317
1542
  translation: result.translation,
1318
1543
  sourceLanguage: result.sourceLanguage,
1319
1544
  isTranslated: result.success,
1320
- retryable: result.retryable
1545
+ retryable: result.retryable,
1546
+ requiresGesture: result.requiresGesture ?? false
1321
1547
  }
1322
1548
  ],
1323
1549
  targetLanguage: request.targetLanguage,
@@ -1332,6 +1558,9 @@ var Client2 = class {
1332
1558
  errorMessage
1333
1559
  };
1334
1560
  }
1561
+ async downloadModel(sourceLanguage, targetLanguage) {
1562
+ await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
1563
+ }
1335
1564
  getConfig() {
1336
1565
  return this.config;
1337
1566
  }
@@ -1372,6 +1601,10 @@ var Client3 = class {
1372
1601
  const translator = this.getFrontendTranslator();
1373
1602
  return await translator.translateBatch(...args);
1374
1603
  }
1604
+ async downloadModel(sourceLanguage, targetLanguage) {
1605
+ const translator = this.getFrontendTranslator();
1606
+ await translator.downloadModel(sourceLanguage, targetLanguage);
1607
+ }
1375
1608
  };
1376
1609
 
1377
1610
  // src/translators/translator.ts
@@ -1386,6 +1619,17 @@ var Translator2 = class {
1386
1619
  const translatorClient = this.getTranslatorClient();
1387
1620
  return await translatorClient.translateBatch(translationRequests);
1388
1621
  }
1622
+ /**
1623
+ * Downloads the on-device model for a source -> target language pair via the
1624
+ * active translator client. Must be invoked from a user gesture for clients
1625
+ * (e.g. Chrome) that require one to start a model download.
1626
+ */
1627
+ async downloadModel(sourceLanguage, targetLanguage) {
1628
+ await this.getTranslatorClient().downloadModel(
1629
+ sourceLanguage,
1630
+ targetLanguage
1631
+ );
1632
+ }
1389
1633
  setTranslatorClient(config2) {
1390
1634
  if (config2.getToolConfig().strategy === "frontend_translations") {
1391
1635
  this.translatorClient = new Client3(config2);
@@ -1504,6 +1748,9 @@ function AITranslationInnerProvider(props) {
1504
1748
  } = props;
1505
1749
  const [translationProgress, setTranslationProgress] = (0, import_react.useState)(null);
1506
1750
  const [modelDownloadProgress, setModelDownloadProgress] = (0, import_react.useState)(null);
1751
+ const [modelConsent, setModelConsent] = (0, import_react.useState)(
1752
+ null
1753
+ );
1507
1754
  const [config2, setConfig] = (0, import_react.useState)(new Config());
1508
1755
  const translator = (0, import_react.useRef)(new Translator2(config2));
1509
1756
  const eventHandler = (0, import_react.useRef)(new EventHandler(tool));
@@ -1555,6 +1802,14 @@ function AITranslationInnerProvider(props) {
1555
1802
  );
1556
1803
  return () => unsubscribe();
1557
1804
  }, []);
1805
+ (0, import_react.useEffect)(() => {
1806
+ const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
1807
+ (request) => {
1808
+ setModelConsent(request);
1809
+ }
1810
+ );
1811
+ return () => unsubscribe();
1812
+ }, []);
1558
1813
  (0, import_react.useEffect)(() => {
1559
1814
  if (!isFetched || !remoteConfig) return;
1560
1815
  const toolConfig = remoteConfig[tool] ?? remoteConfig;
@@ -1571,9 +1826,23 @@ function AITranslationInnerProvider(props) {
1571
1826
  config2.setCompanyId(companyId);
1572
1827
  config2.setProjectId(projectId);
1573
1828
  const ait = (0, import_react.useCallback)(
1574
- async (text) => enableAIT ? await aitFunction(text, translationRegistry.current, config2, tool) : await Promise.resolve(text),
1829
+ async (text) => {
1830
+ if (!enableAIT) {
1831
+ return text;
1832
+ }
1833
+ return aitFunction(text, translationRegistry.current, config2, tool);
1834
+ },
1575
1835
  [tool, config2, enableAIT]
1576
1836
  );
1837
+ const confirmModelDownload = (0, import_react.useCallback)(() => {
1838
+ if (!modelConsent) return;
1839
+ if (config2.getToolConfig().strategy !== "frontend_translations") return;
1840
+ const { sourceLanguage, targetLanguage } = modelConsent;
1841
+ setModelConsent(null);
1842
+ translator.current.downloadModel(sourceLanguage, targetLanguage).then(() => modelDownloadGate.markReady(sourceLanguage, targetLanguage)).catch(
1843
+ (error) => modelDownloadGate.markFailed(sourceLanguage, targetLanguage, error)
1844
+ );
1845
+ }, [modelConsent, config2]);
1577
1846
  const contextValue = {
1578
1847
  ait,
1579
1848
  locale,
@@ -1582,6 +1851,8 @@ function AITranslationInnerProvider(props) {
1582
1851
  renderVersion: renderVersionManager.getVersion(),
1583
1852
  translationProgress,
1584
1853
  modelDownloadProgress,
1854
+ modelConsent,
1855
+ confirmModelDownload,
1585
1856
  tool,
1586
1857
  onTrackAnalyticsEvent,
1587
1858
  page,
@@ -1616,8 +1887,8 @@ var BUTTON_TYPE = {
1616
1887
  var ACTION = {
1617
1888
  CLICKED: "clicked"
1618
1889
  };
1619
- function buildObject(pageContext, buttonType) {
1620
- return `${pageContext}_${buttonType}_button`;
1890
+ function buildObject(buttonType) {
1891
+ return `${buttonType}_button`;
1621
1892
  }
1622
1893
  function buildEventKey(parts) {
1623
1894
  const { scope, tool, object, action } = parts;
@@ -1634,7 +1905,7 @@ function buildAnalyticEvent(params) {
1634
1905
  additionalProperties
1635
1906
  } = params;
1636
1907
  const resolvedAction = action ?? ACTION.CLICKED;
1637
- const object = buildObject(pageContext, buttonType);
1908
+ const object = buildObject(buttonType);
1638
1909
  const key = buildEventKey({
1639
1910
  scope,
1640
1911
  tool,
@@ -1643,6 +1914,7 @@ function buildAnalyticEvent(params) {
1643
1914
  });
1644
1915
  const properties = {
1645
1916
  ...baseProperties,
1917
+ [`${buttonType}_button_location`]: pageContext,
1646
1918
  ...additionalProperties
1647
1919
  };
1648
1920
  return { key, properties };
@@ -1922,7 +2194,7 @@ var CustomizableAITranslateText = ({
1922
2194
 
1923
2195
  // src/utils/featureFlag.ts
1924
2196
  var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
1925
- var AI_TRANSLATION_FEATURE_FLAG_KEY = "pe_ai_translations_web_program_visible";
2197
+ var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
1926
2198
  var getAITranslationLDId = (domain) => {
1927
2199
  const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
1928
2200
  if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {