@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.
- package/README.md +39 -5
- package/dist/legacy/index.d.mts +28 -3
- package/dist/legacy/index.d.ts +28 -3
- package/dist/legacy/index.js +346 -74
- package/dist/legacy/index.mjs +346 -74
- package/dist/modern/index.d.mts +28 -3
- package/dist/modern/index.d.ts +28 -3
- package/dist/modern/index.js +339 -73
- package/dist/modern/index.mjs +339 -73
- package/package.json +1 -1
package/dist/legacy/index.mjs
CHANGED
|
@@ -637,6 +637,7 @@ var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
|
|
|
637
637
|
var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
|
|
638
638
|
var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
|
|
639
639
|
var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
|
|
640
|
+
var MODEL_CONSENT_REQUIRED_EVENT_NAME = "ai-translations-model-consent-required";
|
|
640
641
|
|
|
641
642
|
// src/utils/eventHandler.ts
|
|
642
643
|
var EventHandler = class {
|
|
@@ -717,24 +718,51 @@ var EventHandler = class {
|
|
|
717
718
|
* Not tool-scoped — a Chrome language model is shared across the entire
|
|
718
719
|
* browser session, so all Provider instances receive it.
|
|
719
720
|
*/
|
|
720
|
-
publishModelDownloadProgressEvent(loaded, total, progress) {
|
|
721
|
+
publishModelDownloadProgressEvent(loaded, total, progress, sourceLanguage, targetLanguage) {
|
|
721
722
|
this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
|
|
722
723
|
loaded,
|
|
723
724
|
total,
|
|
724
725
|
progress,
|
|
725
|
-
isComplete: loaded === total
|
|
726
|
+
isComplete: loaded === total,
|
|
727
|
+
sourceLanguage,
|
|
728
|
+
targetLanguage
|
|
726
729
|
});
|
|
727
730
|
}
|
|
728
731
|
subscribeToModelDownloadProgressEvent(callback) {
|
|
729
732
|
return this.aiTranslationEvents.subscribe(
|
|
730
733
|
MODEL_DOWNLOAD_PROGRESS_EVENT_NAME,
|
|
731
734
|
(detail) => {
|
|
732
|
-
var _a, _b, _c, _d;
|
|
735
|
+
var _a, _b, _c, _d, _e, _f;
|
|
733
736
|
return callback({
|
|
734
737
|
loaded: ((_a = detail.data) == null ? void 0 : _a.loaded) ?? 0,
|
|
735
738
|
total: ((_b = detail.data) == null ? void 0 : _b.total) ?? 0,
|
|
736
739
|
progress: ((_c = detail.data) == null ? void 0 : _c.progress) ?? 0,
|
|
737
|
-
isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false
|
|
740
|
+
isComplete: ((_d = detail.data) == null ? void 0 : _d.isComplete) ?? false,
|
|
741
|
+
sourceLanguage: ((_e = detail.data) == null ? void 0 : _e.sourceLanguage) ?? "",
|
|
742
|
+
targetLanguage: ((_f = detail.data) == null ? void 0 : _f.targetLanguage) ?? ""
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Publishes a request for the user to consent to downloading a specific
|
|
749
|
+
* source -> target language model. Like the download-progress event this is
|
|
750
|
+
* NOT tool-scoped — a language model is shared across the whole session.
|
|
751
|
+
*/
|
|
752
|
+
publishModelConsentRequiredEvent(sourceLanguage, targetLanguage) {
|
|
753
|
+
this.aiTranslationEvents.publish(MODEL_CONSENT_REQUIRED_EVENT_NAME, {
|
|
754
|
+
sourceLanguage,
|
|
755
|
+
targetLanguage
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
subscribeToModelConsentRequiredEvent(callback) {
|
|
759
|
+
return this.aiTranslationEvents.subscribe(
|
|
760
|
+
MODEL_CONSENT_REQUIRED_EVENT_NAME,
|
|
761
|
+
(detail) => {
|
|
762
|
+
var _a, _b;
|
|
763
|
+
return callback({
|
|
764
|
+
sourceLanguage: ((_a = detail.data) == null ? void 0 : _a.sourceLanguage) ?? "",
|
|
765
|
+
targetLanguage: ((_b = detail.data) == null ? void 0 : _b.targetLanguage) ?? ""
|
|
738
766
|
});
|
|
739
767
|
}
|
|
740
768
|
);
|
|
@@ -744,6 +772,113 @@ var EventHandler = class {
|
|
|
744
772
|
}
|
|
745
773
|
};
|
|
746
774
|
|
|
775
|
+
// src/utils/modelDownloadGate.ts
|
|
776
|
+
var ModelDownloadGateImpl = class _ModelDownloadGateImpl {
|
|
777
|
+
constructor() {
|
|
778
|
+
__publicField(this, "eventHandler", null);
|
|
779
|
+
__publicField(this, "ready", /* @__PURE__ */ new Set());
|
|
780
|
+
__publicField(this, "pending", /* @__PURE__ */ new Map());
|
|
781
|
+
__publicField(this, "resolvers", /* @__PURE__ */ new Map());
|
|
782
|
+
__publicField(this, "rejecters", /* @__PURE__ */ new Map());
|
|
783
|
+
}
|
|
784
|
+
getEventHandler() {
|
|
785
|
+
if (!this.eventHandler) {
|
|
786
|
+
this.eventHandler = new EventHandler("__chrome_model__");
|
|
787
|
+
}
|
|
788
|
+
return this.eventHandler;
|
|
789
|
+
}
|
|
790
|
+
static key(sourceLanguage, targetLanguage) {
|
|
791
|
+
return `${sourceLanguage}->${targetLanguage}`;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Resolves once the model for `sourceLanguage -> targetLanguage` is downloaded
|
|
795
|
+
* and ready. Pauses (stays pending) until the user confirms the download and
|
|
796
|
+
* it completes. Re-prompts are deduped per model.
|
|
797
|
+
*/
|
|
798
|
+
ensureModelReady(sourceLanguage, targetLanguage) {
|
|
799
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
800
|
+
if (this.ready.has(key)) {
|
|
801
|
+
return Promise.resolve();
|
|
802
|
+
}
|
|
803
|
+
const existing = this.pending.get(key);
|
|
804
|
+
if (existing) {
|
|
805
|
+
return existing;
|
|
806
|
+
}
|
|
807
|
+
const promise = new Promise((resolve, reject) => {
|
|
808
|
+
this.resolvers.set(key, resolve);
|
|
809
|
+
this.rejecters.set(key, reject);
|
|
810
|
+
});
|
|
811
|
+
this.pending.set(key, promise);
|
|
812
|
+
this.getEventHandler().publishModelConsentRequiredEvent(
|
|
813
|
+
sourceLanguage,
|
|
814
|
+
targetLanguage
|
|
815
|
+
);
|
|
816
|
+
return promise;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Marks a model as ready and resolves any waiter. Resolved exclusively when
|
|
820
|
+
* `downloadModel` resolves (wired in the Provider). This guarantees Chrome's
|
|
821
|
+
* `Translator.create()` has returned and the translator instance is cached in
|
|
822
|
+
* `chromeTranslatorRegistry`, so the resumed translation reuses it instead of
|
|
823
|
+
* calling `Translator.create()` again outside the user gesture (which throws
|
|
824
|
+
* `NotAllowedError`). It is deliberately NOT resolved on the intermediate
|
|
825
|
+
* `MODEL_DOWNLOAD_PROGRESS` `isComplete` event, which fires before
|
|
826
|
+
* `Translator.create()` returns.
|
|
827
|
+
*/
|
|
828
|
+
markReady(sourceLanguage, targetLanguage) {
|
|
829
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
830
|
+
this.ready.add(key);
|
|
831
|
+
const resolve = this.resolvers.get(key);
|
|
832
|
+
if (resolve) {
|
|
833
|
+
resolve();
|
|
834
|
+
}
|
|
835
|
+
this.clearPending(key);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Marks a model download as failed and rejects any waiter so the paused
|
|
839
|
+
* translation run can move past this text instead of hanging forever. Called
|
|
840
|
+
* when `downloadModel` rejects (e.g. Chrome returns 404 for a pair it
|
|
841
|
+
* reported as downloadable). The pair is NOT cached as ready, so it can be
|
|
842
|
+
* re-attempted later.
|
|
843
|
+
*/
|
|
844
|
+
markFailed(sourceLanguage, targetLanguage, error) {
|
|
845
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
846
|
+
const reject = this.rejecters.get(key);
|
|
847
|
+
if (reject) {
|
|
848
|
+
reject(
|
|
849
|
+
error instanceof Error ? error : new Error(`Model download failed for ${key}`)
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
this.clearPending(key);
|
|
853
|
+
}
|
|
854
|
+
isReady(sourceLanguage, targetLanguage) {
|
|
855
|
+
return this.ready.has(
|
|
856
|
+
_ModelDownloadGateImpl.key(sourceLanguage, targetLanguage)
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Clears all gate state. Intended for test teardown so cached "ready" models
|
|
861
|
+
* do not leak between cases.
|
|
862
|
+
*/
|
|
863
|
+
reset() {
|
|
864
|
+
this.ready.clear();
|
|
865
|
+
this.pending.clear();
|
|
866
|
+
this.resolvers.clear();
|
|
867
|
+
this.rejecters.clear();
|
|
868
|
+
}
|
|
869
|
+
clearPending(key) {
|
|
870
|
+
this.resolvers.delete(key);
|
|
871
|
+
this.rejecters.delete(key);
|
|
872
|
+
this.pending.delete(key);
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var modelDownloadGate = new ModelDownloadGateImpl();
|
|
876
|
+
|
|
877
|
+
// src/utils/language.ts
|
|
878
|
+
function normalizeLanguage(language) {
|
|
879
|
+
return language.split("-")[0] ?? language;
|
|
880
|
+
}
|
|
881
|
+
|
|
747
882
|
// src/utils/renderVersionManager.ts
|
|
748
883
|
var RenderVersionManager = class {
|
|
749
884
|
constructor() {
|
|
@@ -802,27 +937,30 @@ var TranslationManager = class {
|
|
|
802
937
|
this.translationProgress.total = queueManager.queueSize(
|
|
803
938
|
this.currentTranslatorStrategy
|
|
804
939
|
);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
940
|
+
try {
|
|
941
|
+
do {
|
|
942
|
+
for (const batch of queueManager.generateBatches(
|
|
943
|
+
this.translator.getConfig()
|
|
944
|
+
)) {
|
|
945
|
+
batch.forEach((entry) => this.mfeToBeNotified.add(entry.tool));
|
|
946
|
+
const result = await this.translator.processTranslations(
|
|
947
|
+
batch.map(
|
|
948
|
+
(entry) => this.convertTranslationQueueEntryToTranslationRequest(entry)
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
this.setTranslationProgress(batch.length);
|
|
952
|
+
await this.updateDatabaseWithTranslations(result);
|
|
953
|
+
await this.notifyTranslationCompleted();
|
|
954
|
+
}
|
|
955
|
+
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
956
|
+
this.resetTranslationProgress();
|
|
957
|
+
this.publishTranslationProgress();
|
|
958
|
+
} finally {
|
|
959
|
+
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
960
|
+
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
961
|
+
} else {
|
|
962
|
+
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
818
963
|
}
|
|
819
|
-
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
820
|
-
this.resetTranslationProgress();
|
|
821
|
-
this.publishTranslationProgress();
|
|
822
|
-
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
823
|
-
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
824
|
-
} else {
|
|
825
|
-
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
826
964
|
}
|
|
827
965
|
}
|
|
828
966
|
async updateDatabaseWithTranslations(translationResult) {
|
|
@@ -992,6 +1130,16 @@ var Client = class {
|
|
|
992
1130
|
return this.createTranslationErrorResponse(requests, errorMessage);
|
|
993
1131
|
}
|
|
994
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* No-op: backend translation does not manage on-device models, so there is
|
|
1135
|
+
* nothing to download client-side. The parameters are kept to match the
|
|
1136
|
+
* {@link TranslatorClientAdapter.downloadModel} signature and make the no-op
|
|
1137
|
+
* intent explicit to callers.
|
|
1138
|
+
*/
|
|
1139
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1140
|
+
void sourceLanguage;
|
|
1141
|
+
void targetLanguage;
|
|
1142
|
+
}
|
|
995
1143
|
createTranslationErrorResponse(requests, errorMessage) {
|
|
996
1144
|
return {
|
|
997
1145
|
responseBody: requests.map((req) => ({
|
|
@@ -1111,6 +1259,7 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1111
1259
|
}
|
|
1112
1260
|
}
|
|
1113
1261
|
static async init(sourceLanguage, targetLanguage) {
|
|
1262
|
+
this.ensureRegistry();
|
|
1114
1263
|
if (globalThis.chromeTranslatorRegistry.has(
|
|
1115
1264
|
this.generateKey(sourceLanguage, targetLanguage)
|
|
1116
1265
|
)) {
|
|
@@ -1125,7 +1274,30 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1125
1274
|
new _ChromeTranslator(translator)
|
|
1126
1275
|
);
|
|
1127
1276
|
}
|
|
1128
|
-
|
|
1277
|
+
/**
|
|
1278
|
+
* Downloads the on-device model for a source -> target language pair. Must be
|
|
1279
|
+
* invoked from a user gesture (e.g. a click handler), since Chrome requires an
|
|
1280
|
+
* active gesture to start a model download.
|
|
1281
|
+
*/
|
|
1282
|
+
static async downloadModel(sourceLanguage, targetLanguage) {
|
|
1283
|
+
await this.init(
|
|
1284
|
+
normalizeLanguage(sourceLanguage),
|
|
1285
|
+
normalizeLanguage(targetLanguage)
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Creates a Chrome translator instance for the pair.
|
|
1290
|
+
*
|
|
1291
|
+
* @param publishProgress - When true, `downloadprogress` events are forwarded
|
|
1292
|
+
* as `MODEL_DOWNLOAD_PROGRESS` events (driving the download-progress popup).
|
|
1293
|
+
* This should only be true for an explicit, consent-driven download
|
|
1294
|
+
* (`init`/`downloadModel`). When creating an already-downloaded
|
|
1295
|
+
* (`available`) model in the translate loop, Chrome still emits
|
|
1296
|
+
* `downloadprogress` while loading the pack from disk into memory; forwarding
|
|
1297
|
+
* those would wrongly show a "downloading" popup for a model that is already
|
|
1298
|
+
* downloaded, so the translate-loop path passes `false`.
|
|
1299
|
+
*/
|
|
1300
|
+
static async createTranslator(source, target, publishProgress = true) {
|
|
1129
1301
|
const key = this.generateKey(source, target);
|
|
1130
1302
|
let resolveReady;
|
|
1131
1303
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -1142,11 +1314,15 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1142
1314
|
const loaded = e.loaded ?? 0;
|
|
1143
1315
|
const total = e.total ?? 0;
|
|
1144
1316
|
const progress = total > 0 ? Math.round(loaded / total * 100) : 0;
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1317
|
+
if (publishProgress) {
|
|
1318
|
+
getModelDownloadEventHandler().publishModelDownloadProgressEvent(
|
|
1319
|
+
loaded,
|
|
1320
|
+
total,
|
|
1321
|
+
progress,
|
|
1322
|
+
source,
|
|
1323
|
+
target
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1150
1326
|
if (total > 0 && loaded >= total) {
|
|
1151
1327
|
resolveReady();
|
|
1152
1328
|
}
|
|
@@ -1164,26 +1340,27 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1164
1340
|
static async translate(text, targetLanguage) {
|
|
1165
1341
|
var _a;
|
|
1166
1342
|
this.ensureRegistry();
|
|
1343
|
+
const normalizedTarget = normalizeLanguage(targetLanguage);
|
|
1344
|
+
let sourceLanguage = normalizedTarget;
|
|
1167
1345
|
try {
|
|
1168
1346
|
const detector = await ChromeLanguageDetector.getInstance();
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(targetLanguage)) {
|
|
1347
|
+
sourceLanguage = normalizeLanguage(await detector.detectLanguage(text));
|
|
1348
|
+
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(normalizedTarget)) {
|
|
1172
1349
|
return {
|
|
1173
1350
|
translation: text,
|
|
1174
1351
|
sourceLanguage,
|
|
1175
1352
|
success: false,
|
|
1176
1353
|
retryable: false,
|
|
1177
|
-
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${
|
|
1354
|
+
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${normalizedTarget}`
|
|
1178
1355
|
};
|
|
1179
1356
|
}
|
|
1180
|
-
const
|
|
1181
|
-
if (!
|
|
1182
|
-
const
|
|
1357
|
+
const key = this.generateKey(sourceLanguage, normalizedTarget);
|
|
1358
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1359
|
+
const availability = await Translator.availability({
|
|
1183
1360
|
sourceLanguage,
|
|
1184
|
-
targetLanguage
|
|
1361
|
+
targetLanguage: normalizedTarget
|
|
1185
1362
|
});
|
|
1186
|
-
if (
|
|
1363
|
+
if (availability === "unavailable") {
|
|
1187
1364
|
return {
|
|
1188
1365
|
translation: text,
|
|
1189
1366
|
sourceLanguage,
|
|
@@ -1192,47 +1369,95 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1192
1369
|
errorMessage: "Translation not supported"
|
|
1193
1370
|
};
|
|
1194
1371
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1372
|
+
if (availability !== "available") {
|
|
1373
|
+
const isUserActivationActive = typeof navigator !== "undefined" && (((_a = navigator.userActivation) == null ? void 0 : _a.isActive) ?? false);
|
|
1374
|
+
if (isUserActivationActive) {
|
|
1375
|
+
await _ChromeTranslator.downloadModel(
|
|
1376
|
+
sourceLanguage,
|
|
1377
|
+
normalizedTarget
|
|
1378
|
+
);
|
|
1379
|
+
modelDownloadGate.markReady(sourceLanguage, normalizedTarget);
|
|
1380
|
+
} else {
|
|
1381
|
+
await modelDownloadGate.ensureModelReady(
|
|
1382
|
+
sourceLanguage,
|
|
1383
|
+
normalizedTarget
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1204
1386
|
}
|
|
1205
1387
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
this.translatorReadyPromises.delete(key);
|
|
1209
|
-
const translation = await ((_a = instance.translator) == null ? void 0 : _a.translate(text));
|
|
1210
|
-
if (!translation) {
|
|
1211
|
-
return {
|
|
1212
|
-
translation: text,
|
|
1213
|
-
sourceLanguage,
|
|
1214
|
-
success: false,
|
|
1215
|
-
errorMessage: "Translation failed",
|
|
1216
|
-
retryable: false
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
return {
|
|
1220
|
-
translation,
|
|
1388
|
+
return await this.createInstanceAndTranslate(
|
|
1389
|
+
text,
|
|
1221
1390
|
sourceLanguage,
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
};
|
|
1391
|
+
normalizedTarget
|
|
1392
|
+
);
|
|
1225
1393
|
} catch (error) {
|
|
1394
|
+
const isGestureRequired = error instanceof DOMException && error.name === "NotAllowedError";
|
|
1395
|
+
if (isGestureRequired) {
|
|
1396
|
+
try {
|
|
1397
|
+
await modelDownloadGate.ensureModelReady(
|
|
1398
|
+
sourceLanguage,
|
|
1399
|
+
normalizedTarget
|
|
1400
|
+
);
|
|
1401
|
+
return await this.createInstanceAndTranslate(
|
|
1402
|
+
text,
|
|
1403
|
+
sourceLanguage,
|
|
1404
|
+
normalizedTarget
|
|
1405
|
+
);
|
|
1406
|
+
} catch (retryError) {
|
|
1407
|
+
void retryError;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1226
1410
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1227
1411
|
return {
|
|
1228
1412
|
translation: text,
|
|
1229
|
-
sourceLanguage
|
|
1413
|
+
sourceLanguage,
|
|
1230
1414
|
success: false,
|
|
1231
1415
|
errorMessage: message,
|
|
1232
|
-
retryable:
|
|
1416
|
+
retryable: false,
|
|
1417
|
+
requiresGesture: isGestureRequired
|
|
1233
1418
|
};
|
|
1234
1419
|
}
|
|
1235
1420
|
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Creates the translator instance for the pair if it is not already cached,
|
|
1423
|
+
* waits for the model to be ready, and translates the text. The model is
|
|
1424
|
+
* expected to already be downloaded (or downloadable without a gesture) by
|
|
1425
|
+
* the time this is called.
|
|
1426
|
+
*/
|
|
1427
|
+
static async createInstanceAndTranslate(text, sourceLanguage, targetLanguage) {
|
|
1428
|
+
var _a;
|
|
1429
|
+
const key = this.generateKey(sourceLanguage, targetLanguage);
|
|
1430
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1431
|
+
const chromeTranslator = await this.createTranslator(
|
|
1432
|
+
sourceLanguage,
|
|
1433
|
+
targetLanguage,
|
|
1434
|
+
false
|
|
1435
|
+
);
|
|
1436
|
+
globalThis.chromeTranslatorRegistry.set(
|
|
1437
|
+
key,
|
|
1438
|
+
new _ChromeTranslator(chromeTranslator)
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const instance = globalThis.chromeTranslatorRegistry.get(key);
|
|
1442
|
+
await this.waitForReady(key);
|
|
1443
|
+
this.translatorReadyPromises.delete(key);
|
|
1444
|
+
const translation = await ((_a = instance.translator) == null ? void 0 : _a.translate(text));
|
|
1445
|
+
if (!translation) {
|
|
1446
|
+
return {
|
|
1447
|
+
translation: text,
|
|
1448
|
+
sourceLanguage,
|
|
1449
|
+
success: false,
|
|
1450
|
+
errorMessage: "Translation failed",
|
|
1451
|
+
retryable: false
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
translation,
|
|
1456
|
+
sourceLanguage,
|
|
1457
|
+
success: true,
|
|
1458
|
+
retryable: true
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1236
1461
|
static generateKey(sourceLanguage, targetLanguage) {
|
|
1237
1462
|
return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
|
|
1238
1463
|
}
|
|
@@ -1276,7 +1501,8 @@ var Client2 = class {
|
|
|
1276
1501
|
translation: result.translation,
|
|
1277
1502
|
sourceLanguage: result.sourceLanguage,
|
|
1278
1503
|
isTranslated: result.success,
|
|
1279
|
-
retryable: result.retryable
|
|
1504
|
+
retryable: result.retryable,
|
|
1505
|
+
requiresGesture: result.requiresGesture ?? false
|
|
1280
1506
|
}
|
|
1281
1507
|
],
|
|
1282
1508
|
targetLanguage: request.targetLanguage,
|
|
@@ -1291,6 +1517,9 @@ var Client2 = class {
|
|
|
1291
1517
|
errorMessage
|
|
1292
1518
|
};
|
|
1293
1519
|
}
|
|
1520
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1521
|
+
await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
|
|
1522
|
+
}
|
|
1294
1523
|
getConfig() {
|
|
1295
1524
|
return this.config;
|
|
1296
1525
|
}
|
|
@@ -1331,6 +1560,10 @@ var Client3 = class {
|
|
|
1331
1560
|
const translator = this.getFrontendTranslator();
|
|
1332
1561
|
return await translator.translateBatch(...args);
|
|
1333
1562
|
}
|
|
1563
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1564
|
+
const translator = this.getFrontendTranslator();
|
|
1565
|
+
await translator.downloadModel(sourceLanguage, targetLanguage);
|
|
1566
|
+
}
|
|
1334
1567
|
};
|
|
1335
1568
|
|
|
1336
1569
|
// src/translators/translator.ts
|
|
@@ -1345,6 +1578,17 @@ var Translator2 = class {
|
|
|
1345
1578
|
const translatorClient = this.getTranslatorClient();
|
|
1346
1579
|
return await translatorClient.translateBatch(translationRequests);
|
|
1347
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Downloads the on-device model for a source -> target language pair via the
|
|
1583
|
+
* active translator client. Must be invoked from a user gesture for clients
|
|
1584
|
+
* (e.g. Chrome) that require one to start a model download.
|
|
1585
|
+
*/
|
|
1586
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1587
|
+
await this.getTranslatorClient().downloadModel(
|
|
1588
|
+
sourceLanguage,
|
|
1589
|
+
targetLanguage
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1348
1592
|
setTranslatorClient(config2) {
|
|
1349
1593
|
if (config2.getToolConfig().strategy === "frontend_translations") {
|
|
1350
1594
|
this.translatorClient = new Client3(config2);
|
|
@@ -1463,6 +1707,9 @@ function AITranslationInnerProvider(props) {
|
|
|
1463
1707
|
} = props;
|
|
1464
1708
|
const [translationProgress, setTranslationProgress] = useState(null);
|
|
1465
1709
|
const [modelDownloadProgress, setModelDownloadProgress] = useState(null);
|
|
1710
|
+
const [modelConsent, setModelConsent] = useState(
|
|
1711
|
+
null
|
|
1712
|
+
);
|
|
1466
1713
|
const [config2, setConfig] = useState(new Config());
|
|
1467
1714
|
const translator = useRef(new Translator2(config2));
|
|
1468
1715
|
const eventHandler = useRef(new EventHandler(tool));
|
|
@@ -1514,6 +1761,14 @@ function AITranslationInnerProvider(props) {
|
|
|
1514
1761
|
);
|
|
1515
1762
|
return () => unsubscribe();
|
|
1516
1763
|
}, []);
|
|
1764
|
+
useEffect(() => {
|
|
1765
|
+
const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
|
|
1766
|
+
(request) => {
|
|
1767
|
+
setModelConsent(request);
|
|
1768
|
+
}
|
|
1769
|
+
);
|
|
1770
|
+
return () => unsubscribe();
|
|
1771
|
+
}, []);
|
|
1517
1772
|
useEffect(() => {
|
|
1518
1773
|
if (!isFetched || !remoteConfig) return;
|
|
1519
1774
|
const toolConfig = remoteConfig[tool] ?? remoteConfig;
|
|
@@ -1530,9 +1785,23 @@ function AITranslationInnerProvider(props) {
|
|
|
1530
1785
|
config2.setCompanyId(companyId);
|
|
1531
1786
|
config2.setProjectId(projectId);
|
|
1532
1787
|
const ait = useCallback(
|
|
1533
|
-
async (text) =>
|
|
1788
|
+
async (text) => {
|
|
1789
|
+
if (!enableAIT) {
|
|
1790
|
+
return text;
|
|
1791
|
+
}
|
|
1792
|
+
return aitFunction(text, translationRegistry.current, config2, tool);
|
|
1793
|
+
},
|
|
1534
1794
|
[tool, config2, enableAIT]
|
|
1535
1795
|
);
|
|
1796
|
+
const confirmModelDownload = useCallback(() => {
|
|
1797
|
+
if (!modelConsent) return;
|
|
1798
|
+
if (config2.getToolConfig().strategy !== "frontend_translations") return;
|
|
1799
|
+
const { sourceLanguage, targetLanguage } = modelConsent;
|
|
1800
|
+
setModelConsent(null);
|
|
1801
|
+
translator.current.downloadModel(sourceLanguage, targetLanguage).then(() => modelDownloadGate.markReady(sourceLanguage, targetLanguage)).catch(
|
|
1802
|
+
(error) => modelDownloadGate.markFailed(sourceLanguage, targetLanguage, error)
|
|
1803
|
+
);
|
|
1804
|
+
}, [modelConsent, config2]);
|
|
1536
1805
|
const contextValue = {
|
|
1537
1806
|
ait,
|
|
1538
1807
|
locale,
|
|
@@ -1541,6 +1810,8 @@ function AITranslationInnerProvider(props) {
|
|
|
1541
1810
|
renderVersion: renderVersionManager.getVersion(),
|
|
1542
1811
|
translationProgress,
|
|
1543
1812
|
modelDownloadProgress,
|
|
1813
|
+
modelConsent,
|
|
1814
|
+
confirmModelDownload,
|
|
1544
1815
|
tool,
|
|
1545
1816
|
onTrackAnalyticsEvent,
|
|
1546
1817
|
page,
|
|
@@ -1575,8 +1846,8 @@ var BUTTON_TYPE = {
|
|
|
1575
1846
|
var ACTION = {
|
|
1576
1847
|
CLICKED: "clicked"
|
|
1577
1848
|
};
|
|
1578
|
-
function buildObject(
|
|
1579
|
-
return `${
|
|
1849
|
+
function buildObject(buttonType) {
|
|
1850
|
+
return `${buttonType}_button`;
|
|
1580
1851
|
}
|
|
1581
1852
|
function buildEventKey(parts) {
|
|
1582
1853
|
const { scope, tool, object, action } = parts;
|
|
@@ -1593,7 +1864,7 @@ function buildAnalyticEvent(params) {
|
|
|
1593
1864
|
additionalProperties
|
|
1594
1865
|
} = params;
|
|
1595
1866
|
const resolvedAction = action ?? ACTION.CLICKED;
|
|
1596
|
-
const object = buildObject(
|
|
1867
|
+
const object = buildObject(buttonType);
|
|
1597
1868
|
const key = buildEventKey({
|
|
1598
1869
|
scope,
|
|
1599
1870
|
tool,
|
|
@@ -1602,6 +1873,7 @@ function buildAnalyticEvent(params) {
|
|
|
1602
1873
|
});
|
|
1603
1874
|
const properties = {
|
|
1604
1875
|
...baseProperties,
|
|
1876
|
+
[`${buttonType}_button_location`]: pageContext,
|
|
1605
1877
|
...additionalProperties
|
|
1606
1878
|
};
|
|
1607
1879
|
return { key, properties };
|
|
@@ -1887,7 +2159,7 @@ var CustomizableAITranslateText = ({
|
|
|
1887
2159
|
|
|
1888
2160
|
// src/utils/featureFlag.ts
|
|
1889
2161
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1890
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
2162
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1891
2163
|
var getAITranslationLDId = (domain) => {
|
|
1892
2164
|
const { environment, zone } = getProcoreZone(domain);
|
|
1893
2165
|
if (isFederalZone(zone)) {
|
package/dist/modern/index.d.mts
CHANGED
|
@@ -64,6 +64,20 @@ interface ModelDownloadProgress {
|
|
|
64
64
|
progress: number;
|
|
65
65
|
/** True once the model has finished downloading */
|
|
66
66
|
isComplete: boolean;
|
|
67
|
+
/** Source language of the model being downloaded (e.g. `"fr"`) */
|
|
68
|
+
sourceLanguage: string;
|
|
69
|
+
/** Target language of the model being downloaded (e.g. `"en"`) */
|
|
70
|
+
targetLanguage: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A pending request for the user to consent to downloading a specific
|
|
74
|
+
* source -> target language model before translation can continue.
|
|
75
|
+
*/
|
|
76
|
+
interface ModelConsentRequest {
|
|
77
|
+
/** Source language of the model that needs downloading (e.g. `"fr"`). */
|
|
78
|
+
sourceLanguage: string;
|
|
79
|
+
/** Target language of the model that needs downloading (e.g. `"en"`). */
|
|
80
|
+
targetLanguage: string;
|
|
67
81
|
}
|
|
68
82
|
interface AITranslationContextValue {
|
|
69
83
|
ait: (text: string) => Promise<string>;
|
|
@@ -77,6 +91,17 @@ interface AITranslationContextValue {
|
|
|
77
91
|
translationProgress: TranslationProgress | null;
|
|
78
92
|
/** Chrome AI model download progress; `null` until a download begins. */
|
|
79
93
|
modelDownloadProgress: ModelDownloadProgress | null;
|
|
94
|
+
/**
|
|
95
|
+
* Pending model the translation run is paused on, awaiting the user's consent
|
|
96
|
+
* to download; `null` when no consent is required.
|
|
97
|
+
*/
|
|
98
|
+
modelConsent: ModelConsentRequest | null;
|
|
99
|
+
/**
|
|
100
|
+
* Confirms the pending {@link modelConsent} download. Must be called from a
|
|
101
|
+
* user gesture (e.g. a click handler) since Chrome requires a gesture to
|
|
102
|
+
* start a model download.
|
|
103
|
+
*/
|
|
104
|
+
confirmModelDownload: () => void;
|
|
80
105
|
tool: string;
|
|
81
106
|
/** Called by `useAIAnalytics` with a fully assembled event. The MFE is responsible for sending it. */
|
|
82
107
|
onTrackAnalyticsEvent?: AIAnalyticsTracker;
|
|
@@ -171,7 +196,7 @@ type Action = (typeof ACTION)[keyof typeof ACTION];
|
|
|
171
196
|
* Builds the `object` segment: `{pageContext}_{buttonType}_button`
|
|
172
197
|
* e.g. `buildObject('view', 'translate')` → `'view_translate_button'`
|
|
173
198
|
*/
|
|
174
|
-
declare function buildObject(
|
|
199
|
+
declare function buildObject(buttonType: ButtonType): string;
|
|
175
200
|
/**
|
|
176
201
|
* Builds a fully-qualified event key:
|
|
177
202
|
* `ux.web.feature.{scope}.{tool}.{object}.{action}`
|
|
@@ -320,7 +345,7 @@ declare const CustomizableAITranslateText: React.FC<CustomizableAITranslateTextP
|
|
|
320
345
|
/**
|
|
321
346
|
* The key used to store/retrieve the feature flag in local storage.
|
|
322
347
|
*/
|
|
323
|
-
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
348
|
+
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
324
349
|
/**
|
|
325
350
|
* Retrieves the LD ID for the AI translation feature flag based on the domain.
|
|
326
351
|
* @param domain - The domain to determine the LD ID for
|
|
@@ -335,4 +360,4 @@ declare global {
|
|
|
335
360
|
var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
|
|
336
361
|
}
|
|
337
362
|
|
|
338
|
-
export { ACTION, type AIAnalyticsEventProperties, type AIAnalyticsTracker, AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type Action, type AnalyticEvent, BUTTON_TYPE, type BuildAnalyticEventParams, type ButtonType, CustomizableAITranslateText, type CustomizableAITranslateTextProps, type EventKeyParts, type Scope, type TextSegmenter, type TranslatableSegment, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|
|
363
|
+
export { ACTION, type AIAnalyticsEventProperties, type AIAnalyticsTracker, AITranslateText, type AITranslateTextProps, AITranslationProvider, AI_TRANSLATION_FEATURE_FLAG_KEY, type Action, type AnalyticEvent, BUTTON_TYPE, type BuildAnalyticEventParams, type ButtonType, CustomizableAITranslateText, type CustomizableAITranslateTextProps, type EventKeyParts, type ModelConsentRequest, type ModelDownloadProgress, type Scope, type TextSegmenter, type TranslatableSegment, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|