@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/modern/index.mjs
CHANGED
|
@@ -627,6 +627,7 @@ var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
|
|
|
627
627
|
var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
|
|
628
628
|
var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
|
|
629
629
|
var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
|
|
630
|
+
var MODEL_CONSENT_REQUIRED_EVENT_NAME = "ai-translations-model-consent-required";
|
|
630
631
|
|
|
631
632
|
// src/utils/eventHandler.ts
|
|
632
633
|
var EventHandler = class {
|
|
@@ -700,12 +701,14 @@ var EventHandler = class {
|
|
|
700
701
|
* Not tool-scoped — a Chrome language model is shared across the entire
|
|
701
702
|
* browser session, so all Provider instances receive it.
|
|
702
703
|
*/
|
|
703
|
-
publishModelDownloadProgressEvent(loaded, total, progress) {
|
|
704
|
+
publishModelDownloadProgressEvent(loaded, total, progress, sourceLanguage, targetLanguage) {
|
|
704
705
|
this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
|
|
705
706
|
loaded,
|
|
706
707
|
total,
|
|
707
708
|
progress,
|
|
708
|
-
isComplete: loaded === total
|
|
709
|
+
isComplete: loaded === total,
|
|
710
|
+
sourceLanguage,
|
|
711
|
+
targetLanguage
|
|
709
712
|
});
|
|
710
713
|
}
|
|
711
714
|
subscribeToModelDownloadProgressEvent(callback) {
|
|
@@ -715,7 +718,29 @@ var EventHandler = class {
|
|
|
715
718
|
loaded: detail.data?.loaded ?? 0,
|
|
716
719
|
total: detail.data?.total ?? 0,
|
|
717
720
|
progress: detail.data?.progress ?? 0,
|
|
718
|
-
isComplete: detail.data?.isComplete ?? false
|
|
721
|
+
isComplete: detail.data?.isComplete ?? false,
|
|
722
|
+
sourceLanguage: detail.data?.sourceLanguage ?? "",
|
|
723
|
+
targetLanguage: detail.data?.targetLanguage ?? ""
|
|
724
|
+
})
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Publishes a request for the user to consent to downloading a specific
|
|
729
|
+
* source -> target language model. Like the download-progress event this is
|
|
730
|
+
* NOT tool-scoped — a language model is shared across the whole session.
|
|
731
|
+
*/
|
|
732
|
+
publishModelConsentRequiredEvent(sourceLanguage, targetLanguage) {
|
|
733
|
+
this.aiTranslationEvents.publish(MODEL_CONSENT_REQUIRED_EVENT_NAME, {
|
|
734
|
+
sourceLanguage,
|
|
735
|
+
targetLanguage
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
subscribeToModelConsentRequiredEvent(callback) {
|
|
739
|
+
return this.aiTranslationEvents.subscribe(
|
|
740
|
+
MODEL_CONSENT_REQUIRED_EVENT_NAME,
|
|
741
|
+
(detail) => callback({
|
|
742
|
+
sourceLanguage: detail.data?.sourceLanguage ?? "",
|
|
743
|
+
targetLanguage: detail.data?.targetLanguage ?? ""
|
|
719
744
|
})
|
|
720
745
|
);
|
|
721
746
|
}
|
|
@@ -724,6 +749,111 @@ var EventHandler = class {
|
|
|
724
749
|
}
|
|
725
750
|
};
|
|
726
751
|
|
|
752
|
+
// src/utils/modelDownloadGate.ts
|
|
753
|
+
var ModelDownloadGateImpl = class _ModelDownloadGateImpl {
|
|
754
|
+
eventHandler = null;
|
|
755
|
+
ready = /* @__PURE__ */ new Set();
|
|
756
|
+
pending = /* @__PURE__ */ new Map();
|
|
757
|
+
resolvers = /* @__PURE__ */ new Map();
|
|
758
|
+
rejecters = /* @__PURE__ */ new Map();
|
|
759
|
+
getEventHandler() {
|
|
760
|
+
if (!this.eventHandler) {
|
|
761
|
+
this.eventHandler = new EventHandler("__chrome_model__");
|
|
762
|
+
}
|
|
763
|
+
return this.eventHandler;
|
|
764
|
+
}
|
|
765
|
+
static key(sourceLanguage, targetLanguage) {
|
|
766
|
+
return `${sourceLanguage}->${targetLanguage}`;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Resolves once the model for `sourceLanguage -> targetLanguage` is downloaded
|
|
770
|
+
* and ready. Pauses (stays pending) until the user confirms the download and
|
|
771
|
+
* it completes. Re-prompts are deduped per model.
|
|
772
|
+
*/
|
|
773
|
+
ensureModelReady(sourceLanguage, targetLanguage) {
|
|
774
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
775
|
+
if (this.ready.has(key)) {
|
|
776
|
+
return Promise.resolve();
|
|
777
|
+
}
|
|
778
|
+
const existing = this.pending.get(key);
|
|
779
|
+
if (existing) {
|
|
780
|
+
return existing;
|
|
781
|
+
}
|
|
782
|
+
const promise = new Promise((resolve, reject) => {
|
|
783
|
+
this.resolvers.set(key, resolve);
|
|
784
|
+
this.rejecters.set(key, reject);
|
|
785
|
+
});
|
|
786
|
+
this.pending.set(key, promise);
|
|
787
|
+
this.getEventHandler().publishModelConsentRequiredEvent(
|
|
788
|
+
sourceLanguage,
|
|
789
|
+
targetLanguage
|
|
790
|
+
);
|
|
791
|
+
return promise;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Marks a model as ready and resolves any waiter. Resolved exclusively when
|
|
795
|
+
* `downloadModel` resolves (wired in the Provider). This guarantees Chrome's
|
|
796
|
+
* `Translator.create()` has returned and the translator instance is cached in
|
|
797
|
+
* `chromeTranslatorRegistry`, so the resumed translation reuses it instead of
|
|
798
|
+
* calling `Translator.create()` again outside the user gesture (which throws
|
|
799
|
+
* `NotAllowedError`). It is deliberately NOT resolved on the intermediate
|
|
800
|
+
* `MODEL_DOWNLOAD_PROGRESS` `isComplete` event, which fires before
|
|
801
|
+
* `Translator.create()` returns.
|
|
802
|
+
*/
|
|
803
|
+
markReady(sourceLanguage, targetLanguage) {
|
|
804
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
805
|
+
this.ready.add(key);
|
|
806
|
+
const resolve = this.resolvers.get(key);
|
|
807
|
+
if (resolve) {
|
|
808
|
+
resolve();
|
|
809
|
+
}
|
|
810
|
+
this.clearPending(key);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Marks a model download as failed and rejects any waiter so the paused
|
|
814
|
+
* translation run can move past this text instead of hanging forever. Called
|
|
815
|
+
* when `downloadModel` rejects (e.g. Chrome returns 404 for a pair it
|
|
816
|
+
* reported as downloadable). The pair is NOT cached as ready, so it can be
|
|
817
|
+
* re-attempted later.
|
|
818
|
+
*/
|
|
819
|
+
markFailed(sourceLanguage, targetLanguage, error) {
|
|
820
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
821
|
+
const reject = this.rejecters.get(key);
|
|
822
|
+
if (reject) {
|
|
823
|
+
reject(
|
|
824
|
+
error instanceof Error ? error : new Error(`Model download failed for ${key}`)
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
this.clearPending(key);
|
|
828
|
+
}
|
|
829
|
+
isReady(sourceLanguage, targetLanguage) {
|
|
830
|
+
return this.ready.has(
|
|
831
|
+
_ModelDownloadGateImpl.key(sourceLanguage, targetLanguage)
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Clears all gate state. Intended for test teardown so cached "ready" models
|
|
836
|
+
* do not leak between cases.
|
|
837
|
+
*/
|
|
838
|
+
reset() {
|
|
839
|
+
this.ready.clear();
|
|
840
|
+
this.pending.clear();
|
|
841
|
+
this.resolvers.clear();
|
|
842
|
+
this.rejecters.clear();
|
|
843
|
+
}
|
|
844
|
+
clearPending(key) {
|
|
845
|
+
this.resolvers.delete(key);
|
|
846
|
+
this.rejecters.delete(key);
|
|
847
|
+
this.pending.delete(key);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
var modelDownloadGate = new ModelDownloadGateImpl();
|
|
851
|
+
|
|
852
|
+
// src/utils/language.ts
|
|
853
|
+
function normalizeLanguage(language) {
|
|
854
|
+
return language.split("-")[0] ?? language;
|
|
855
|
+
}
|
|
856
|
+
|
|
727
857
|
// src/utils/renderVersionManager.ts
|
|
728
858
|
var RenderVersionManager = class {
|
|
729
859
|
version = 0;
|
|
@@ -780,27 +910,30 @@ var TranslationManager = class {
|
|
|
780
910
|
this.translationProgress.total = queueManager.queueSize(
|
|
781
911
|
this.currentTranslatorStrategy
|
|
782
912
|
);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
913
|
+
try {
|
|
914
|
+
do {
|
|
915
|
+
for (const batch of queueManager.generateBatches(
|
|
916
|
+
this.translator.getConfig()
|
|
917
|
+
)) {
|
|
918
|
+
batch.forEach((entry) => this.mfeToBeNotified.add(entry.tool));
|
|
919
|
+
const result = await this.translator.processTranslations(
|
|
920
|
+
batch.map(
|
|
921
|
+
(entry) => this.convertTranslationQueueEntryToTranslationRequest(entry)
|
|
922
|
+
)
|
|
923
|
+
);
|
|
924
|
+
this.setTranslationProgress(batch.length);
|
|
925
|
+
await this.updateDatabaseWithTranslations(result);
|
|
926
|
+
await this.notifyTranslationCompleted();
|
|
927
|
+
}
|
|
928
|
+
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
929
|
+
this.resetTranslationProgress();
|
|
930
|
+
this.publishTranslationProgress();
|
|
931
|
+
} finally {
|
|
932
|
+
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
933
|
+
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
934
|
+
} else {
|
|
935
|
+
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
796
936
|
}
|
|
797
|
-
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
798
|
-
this.resetTranslationProgress();
|
|
799
|
-
this.publishTranslationProgress();
|
|
800
|
-
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
801
|
-
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
802
|
-
} else {
|
|
803
|
-
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
804
937
|
}
|
|
805
938
|
}
|
|
806
939
|
async updateDatabaseWithTranslations(translationResult) {
|
|
@@ -970,6 +1103,16 @@ var Client = class {
|
|
|
970
1103
|
return this.createTranslationErrorResponse(requests, errorMessage);
|
|
971
1104
|
}
|
|
972
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* No-op: backend translation does not manage on-device models, so there is
|
|
1108
|
+
* nothing to download client-side. The parameters are kept to match the
|
|
1109
|
+
* {@link TranslatorClientAdapter.downloadModel} signature and make the no-op
|
|
1110
|
+
* intent explicit to callers.
|
|
1111
|
+
*/
|
|
1112
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1113
|
+
void sourceLanguage;
|
|
1114
|
+
void targetLanguage;
|
|
1115
|
+
}
|
|
973
1116
|
createTranslationErrorResponse(requests, errorMessage) {
|
|
974
1117
|
return {
|
|
975
1118
|
responseBody: requests.map((req) => ({
|
|
@@ -1089,6 +1232,7 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1089
1232
|
}
|
|
1090
1233
|
}
|
|
1091
1234
|
static async init(sourceLanguage, targetLanguage) {
|
|
1235
|
+
this.ensureRegistry();
|
|
1092
1236
|
if (globalThis.chromeTranslatorRegistry.has(
|
|
1093
1237
|
this.generateKey(sourceLanguage, targetLanguage)
|
|
1094
1238
|
)) {
|
|
@@ -1103,7 +1247,30 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1103
1247
|
new _ChromeTranslator(translator)
|
|
1104
1248
|
);
|
|
1105
1249
|
}
|
|
1106
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* Downloads the on-device model for a source -> target language pair. Must be
|
|
1252
|
+
* invoked from a user gesture (e.g. a click handler), since Chrome requires an
|
|
1253
|
+
* active gesture to start a model download.
|
|
1254
|
+
*/
|
|
1255
|
+
static async downloadModel(sourceLanguage, targetLanguage) {
|
|
1256
|
+
await this.init(
|
|
1257
|
+
normalizeLanguage(sourceLanguage),
|
|
1258
|
+
normalizeLanguage(targetLanguage)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Creates a Chrome translator instance for the pair.
|
|
1263
|
+
*
|
|
1264
|
+
* @param publishProgress - When true, `downloadprogress` events are forwarded
|
|
1265
|
+
* as `MODEL_DOWNLOAD_PROGRESS` events (driving the download-progress popup).
|
|
1266
|
+
* This should only be true for an explicit, consent-driven download
|
|
1267
|
+
* (`init`/`downloadModel`). When creating an already-downloaded
|
|
1268
|
+
* (`available`) model in the translate loop, Chrome still emits
|
|
1269
|
+
* `downloadprogress` while loading the pack from disk into memory; forwarding
|
|
1270
|
+
* those would wrongly show a "downloading" popup for a model that is already
|
|
1271
|
+
* downloaded, so the translate-loop path passes `false`.
|
|
1272
|
+
*/
|
|
1273
|
+
static async createTranslator(source, target, publishProgress = true) {
|
|
1107
1274
|
const key = this.generateKey(source, target);
|
|
1108
1275
|
let resolveReady;
|
|
1109
1276
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -1120,11 +1287,15 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1120
1287
|
const loaded = e.loaded ?? 0;
|
|
1121
1288
|
const total = e.total ?? 0;
|
|
1122
1289
|
const progress = total > 0 ? Math.round(loaded / total * 100) : 0;
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1290
|
+
if (publishProgress) {
|
|
1291
|
+
getModelDownloadEventHandler().publishModelDownloadProgressEvent(
|
|
1292
|
+
loaded,
|
|
1293
|
+
total,
|
|
1294
|
+
progress,
|
|
1295
|
+
source,
|
|
1296
|
+
target
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1128
1299
|
if (total > 0 && loaded >= total) {
|
|
1129
1300
|
resolveReady();
|
|
1130
1301
|
}
|
|
@@ -1141,26 +1312,27 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1141
1312
|
}
|
|
1142
1313
|
static async translate(text, targetLanguage) {
|
|
1143
1314
|
this.ensureRegistry();
|
|
1315
|
+
const normalizedTarget = normalizeLanguage(targetLanguage);
|
|
1316
|
+
let sourceLanguage = normalizedTarget;
|
|
1144
1317
|
try {
|
|
1145
1318
|
const detector = await ChromeLanguageDetector.getInstance();
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(targetLanguage)) {
|
|
1319
|
+
sourceLanguage = normalizeLanguage(await detector.detectLanguage(text));
|
|
1320
|
+
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(normalizedTarget)) {
|
|
1149
1321
|
return {
|
|
1150
1322
|
translation: text,
|
|
1151
1323
|
sourceLanguage,
|
|
1152
1324
|
success: false,
|
|
1153
1325
|
retryable: false,
|
|
1154
|
-
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${
|
|
1326
|
+
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${normalizedTarget}`
|
|
1155
1327
|
};
|
|
1156
1328
|
}
|
|
1157
|
-
const
|
|
1158
|
-
if (!
|
|
1159
|
-
const
|
|
1329
|
+
const key = this.generateKey(sourceLanguage, normalizedTarget);
|
|
1330
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1331
|
+
const availability = await Translator.availability({
|
|
1160
1332
|
sourceLanguage,
|
|
1161
|
-
targetLanguage
|
|
1333
|
+
targetLanguage: normalizedTarget
|
|
1162
1334
|
});
|
|
1163
|
-
if (
|
|
1335
|
+
if (availability === "unavailable") {
|
|
1164
1336
|
return {
|
|
1165
1337
|
translation: text,
|
|
1166
1338
|
sourceLanguage,
|
|
@@ -1169,47 +1341,94 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1169
1341
|
errorMessage: "Translation not supported"
|
|
1170
1342
|
};
|
|
1171
1343
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1344
|
+
if (availability !== "available") {
|
|
1345
|
+
const isUserActivationActive = typeof navigator !== "undefined" && (navigator.userActivation?.isActive ?? false);
|
|
1346
|
+
if (isUserActivationActive) {
|
|
1347
|
+
await _ChromeTranslator.downloadModel(
|
|
1348
|
+
sourceLanguage,
|
|
1349
|
+
normalizedTarget
|
|
1350
|
+
);
|
|
1351
|
+
modelDownloadGate.markReady(sourceLanguage, normalizedTarget);
|
|
1352
|
+
} else {
|
|
1353
|
+
await modelDownloadGate.ensureModelReady(
|
|
1354
|
+
sourceLanguage,
|
|
1355
|
+
normalizedTarget
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1181
1358
|
}
|
|
1182
1359
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
this.translatorReadyPromises.delete(key);
|
|
1186
|
-
const translation = await instance.translator?.translate(text);
|
|
1187
|
-
if (!translation) {
|
|
1188
|
-
return {
|
|
1189
|
-
translation: text,
|
|
1190
|
-
sourceLanguage,
|
|
1191
|
-
success: false,
|
|
1192
|
-
errorMessage: "Translation failed",
|
|
1193
|
-
retryable: false
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
return {
|
|
1197
|
-
translation,
|
|
1360
|
+
return await this.createInstanceAndTranslate(
|
|
1361
|
+
text,
|
|
1198
1362
|
sourceLanguage,
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
};
|
|
1363
|
+
normalizedTarget
|
|
1364
|
+
);
|
|
1202
1365
|
} catch (error) {
|
|
1366
|
+
const isGestureRequired = error instanceof DOMException && error.name === "NotAllowedError";
|
|
1367
|
+
if (isGestureRequired) {
|
|
1368
|
+
try {
|
|
1369
|
+
await modelDownloadGate.ensureModelReady(
|
|
1370
|
+
sourceLanguage,
|
|
1371
|
+
normalizedTarget
|
|
1372
|
+
);
|
|
1373
|
+
return await this.createInstanceAndTranslate(
|
|
1374
|
+
text,
|
|
1375
|
+
sourceLanguage,
|
|
1376
|
+
normalizedTarget
|
|
1377
|
+
);
|
|
1378
|
+
} catch (retryError) {
|
|
1379
|
+
void retryError;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1203
1382
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1204
1383
|
return {
|
|
1205
1384
|
translation: text,
|
|
1206
|
-
sourceLanguage
|
|
1385
|
+
sourceLanguage,
|
|
1207
1386
|
success: false,
|
|
1208
1387
|
errorMessage: message,
|
|
1209
|
-
retryable:
|
|
1388
|
+
retryable: false,
|
|
1389
|
+
requiresGesture: isGestureRequired
|
|
1210
1390
|
};
|
|
1211
1391
|
}
|
|
1212
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Creates the translator instance for the pair if it is not already cached,
|
|
1395
|
+
* waits for the model to be ready, and translates the text. The model is
|
|
1396
|
+
* expected to already be downloaded (or downloadable without a gesture) by
|
|
1397
|
+
* the time this is called.
|
|
1398
|
+
*/
|
|
1399
|
+
static async createInstanceAndTranslate(text, sourceLanguage, targetLanguage) {
|
|
1400
|
+
const key = this.generateKey(sourceLanguage, targetLanguage);
|
|
1401
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1402
|
+
const chromeTranslator = await this.createTranslator(
|
|
1403
|
+
sourceLanguage,
|
|
1404
|
+
targetLanguage,
|
|
1405
|
+
false
|
|
1406
|
+
);
|
|
1407
|
+
globalThis.chromeTranslatorRegistry.set(
|
|
1408
|
+
key,
|
|
1409
|
+
new _ChromeTranslator(chromeTranslator)
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const instance = globalThis.chromeTranslatorRegistry.get(key);
|
|
1413
|
+
await this.waitForReady(key);
|
|
1414
|
+
this.translatorReadyPromises.delete(key);
|
|
1415
|
+
const translation = await instance.translator?.translate(text);
|
|
1416
|
+
if (!translation) {
|
|
1417
|
+
return {
|
|
1418
|
+
translation: text,
|
|
1419
|
+
sourceLanguage,
|
|
1420
|
+
success: false,
|
|
1421
|
+
errorMessage: "Translation failed",
|
|
1422
|
+
retryable: false
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
translation,
|
|
1427
|
+
sourceLanguage,
|
|
1428
|
+
success: true,
|
|
1429
|
+
retryable: true
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1213
1432
|
static generateKey(sourceLanguage, targetLanguage) {
|
|
1214
1433
|
return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
|
|
1215
1434
|
}
|
|
@@ -1256,7 +1475,8 @@ var Client2 = class {
|
|
|
1256
1475
|
translation: result.translation,
|
|
1257
1476
|
sourceLanguage: result.sourceLanguage,
|
|
1258
1477
|
isTranslated: result.success,
|
|
1259
|
-
retryable: result.retryable
|
|
1478
|
+
retryable: result.retryable,
|
|
1479
|
+
requiresGesture: result.requiresGesture ?? false
|
|
1260
1480
|
}
|
|
1261
1481
|
],
|
|
1262
1482
|
targetLanguage: request.targetLanguage,
|
|
@@ -1271,6 +1491,9 @@ var Client2 = class {
|
|
|
1271
1491
|
errorMessage
|
|
1272
1492
|
};
|
|
1273
1493
|
}
|
|
1494
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1495
|
+
await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
|
|
1496
|
+
}
|
|
1274
1497
|
getConfig() {
|
|
1275
1498
|
return this.config;
|
|
1276
1499
|
}
|
|
@@ -1306,6 +1529,10 @@ var Client3 = class {
|
|
|
1306
1529
|
const translator = this.getFrontendTranslator();
|
|
1307
1530
|
return await translator.translateBatch(...args);
|
|
1308
1531
|
}
|
|
1532
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1533
|
+
const translator = this.getFrontendTranslator();
|
|
1534
|
+
await translator.downloadModel(sourceLanguage, targetLanguage);
|
|
1535
|
+
}
|
|
1309
1536
|
};
|
|
1310
1537
|
|
|
1311
1538
|
// src/translators/translator.ts
|
|
@@ -1320,6 +1547,17 @@ var Translator2 = class {
|
|
|
1320
1547
|
const translatorClient = this.getTranslatorClient();
|
|
1321
1548
|
return await translatorClient.translateBatch(translationRequests);
|
|
1322
1549
|
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Downloads the on-device model for a source -> target language pair via the
|
|
1552
|
+
* active translator client. Must be invoked from a user gesture for clients
|
|
1553
|
+
* (e.g. Chrome) that require one to start a model download.
|
|
1554
|
+
*/
|
|
1555
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1556
|
+
await this.getTranslatorClient().downloadModel(
|
|
1557
|
+
sourceLanguage,
|
|
1558
|
+
targetLanguage
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1323
1561
|
setTranslatorClient(config2) {
|
|
1324
1562
|
if (config2.getToolConfig().strategy === "frontend_translations") {
|
|
1325
1563
|
this.translatorClient = new Client3(config2);
|
|
@@ -1438,6 +1676,9 @@ function AITranslationInnerProvider(props) {
|
|
|
1438
1676
|
} = props;
|
|
1439
1677
|
const [translationProgress, setTranslationProgress] = useState(null);
|
|
1440
1678
|
const [modelDownloadProgress, setModelDownloadProgress] = useState(null);
|
|
1679
|
+
const [modelConsent, setModelConsent] = useState(
|
|
1680
|
+
null
|
|
1681
|
+
);
|
|
1441
1682
|
const [config2, setConfig] = useState(new Config());
|
|
1442
1683
|
const translator = useRef(new Translator2(config2));
|
|
1443
1684
|
const eventHandler = useRef(new EventHandler(tool));
|
|
@@ -1489,6 +1730,14 @@ function AITranslationInnerProvider(props) {
|
|
|
1489
1730
|
);
|
|
1490
1731
|
return () => unsubscribe();
|
|
1491
1732
|
}, []);
|
|
1733
|
+
useEffect(() => {
|
|
1734
|
+
const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
|
|
1735
|
+
(request) => {
|
|
1736
|
+
setModelConsent(request);
|
|
1737
|
+
}
|
|
1738
|
+
);
|
|
1739
|
+
return () => unsubscribe();
|
|
1740
|
+
}, []);
|
|
1492
1741
|
useEffect(() => {
|
|
1493
1742
|
if (!isFetched || !remoteConfig) return;
|
|
1494
1743
|
const toolConfig = remoteConfig[tool] ?? remoteConfig;
|
|
@@ -1505,9 +1754,23 @@ function AITranslationInnerProvider(props) {
|
|
|
1505
1754
|
config2.setCompanyId(companyId);
|
|
1506
1755
|
config2.setProjectId(projectId);
|
|
1507
1756
|
const ait = useCallback(
|
|
1508
|
-
async (text) =>
|
|
1757
|
+
async (text) => {
|
|
1758
|
+
if (!enableAIT) {
|
|
1759
|
+
return text;
|
|
1760
|
+
}
|
|
1761
|
+
return aitFunction(text, translationRegistry.current, config2, tool);
|
|
1762
|
+
},
|
|
1509
1763
|
[tool, config2, enableAIT]
|
|
1510
1764
|
);
|
|
1765
|
+
const confirmModelDownload = useCallback(() => {
|
|
1766
|
+
if (!modelConsent) return;
|
|
1767
|
+
if (config2.getToolConfig().strategy !== "frontend_translations") return;
|
|
1768
|
+
const { sourceLanguage, targetLanguage } = modelConsent;
|
|
1769
|
+
setModelConsent(null);
|
|
1770
|
+
translator.current.downloadModel(sourceLanguage, targetLanguage).then(() => modelDownloadGate.markReady(sourceLanguage, targetLanguage)).catch(
|
|
1771
|
+
(error) => modelDownloadGate.markFailed(sourceLanguage, targetLanguage, error)
|
|
1772
|
+
);
|
|
1773
|
+
}, [modelConsent, config2]);
|
|
1511
1774
|
const contextValue = {
|
|
1512
1775
|
ait,
|
|
1513
1776
|
locale,
|
|
@@ -1516,6 +1779,8 @@ function AITranslationInnerProvider(props) {
|
|
|
1516
1779
|
renderVersion: renderVersionManager.getVersion(),
|
|
1517
1780
|
translationProgress,
|
|
1518
1781
|
modelDownloadProgress,
|
|
1782
|
+
modelConsent,
|
|
1783
|
+
confirmModelDownload,
|
|
1519
1784
|
tool,
|
|
1520
1785
|
onTrackAnalyticsEvent,
|
|
1521
1786
|
page,
|
|
@@ -1550,8 +1815,8 @@ var BUTTON_TYPE = {
|
|
|
1550
1815
|
var ACTION = {
|
|
1551
1816
|
CLICKED: "clicked"
|
|
1552
1817
|
};
|
|
1553
|
-
function buildObject(
|
|
1554
|
-
return `${
|
|
1818
|
+
function buildObject(buttonType) {
|
|
1819
|
+
return `${buttonType}_button`;
|
|
1555
1820
|
}
|
|
1556
1821
|
function buildEventKey(parts) {
|
|
1557
1822
|
const { scope, tool, object, action } = parts;
|
|
@@ -1568,7 +1833,7 @@ function buildAnalyticEvent(params) {
|
|
|
1568
1833
|
additionalProperties
|
|
1569
1834
|
} = params;
|
|
1570
1835
|
const resolvedAction = action ?? ACTION.CLICKED;
|
|
1571
|
-
const object = buildObject(
|
|
1836
|
+
const object = buildObject(buttonType);
|
|
1572
1837
|
const key = buildEventKey({
|
|
1573
1838
|
scope,
|
|
1574
1839
|
tool,
|
|
@@ -1577,6 +1842,7 @@ function buildAnalyticEvent(params) {
|
|
|
1577
1842
|
});
|
|
1578
1843
|
const properties = {
|
|
1579
1844
|
...baseProperties,
|
|
1845
|
+
[`${buttonType}_button_location`]: pageContext,
|
|
1580
1846
|
...additionalProperties
|
|
1581
1847
|
};
|
|
1582
1848
|
return { key, properties };
|
|
@@ -1862,7 +2128,7 @@ var CustomizableAITranslateText = ({
|
|
|
1862
2128
|
|
|
1863
2129
|
// src/utils/featureFlag.ts
|
|
1864
2130
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1865
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
2131
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1866
2132
|
var getAITranslationLDId = (domain) => {
|
|
1867
2133
|
const { environment, zone } = getProcoreZone(domain);
|
|
1868
2134
|
if (isFederalZone(zone)) {
|
package/package.json
CHANGED