@procore/ai-translations 0.8.1 → 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 +27 -2
- package/dist/legacy/index.d.ts +27 -2
- package/dist/legacy/index.js +345 -74
- package/dist/legacy/index.mjs +345 -74
- package/dist/modern/index.d.mts +27 -2
- package/dist/modern/index.d.ts +27 -2
- package/dist/modern/index.js +338 -73
- package/dist/modern/index.mjs +338 -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,93 @@ 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
|
-
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1205
1383
|
return {
|
|
1206
1384
|
translation: text,
|
|
1207
|
-
sourceLanguage
|
|
1385
|
+
sourceLanguage,
|
|
1208
1386
|
success: false,
|
|
1209
1387
|
errorMessage: message,
|
|
1210
|
-
retryable:
|
|
1388
|
+
retryable: false,
|
|
1389
|
+
requiresGesture: isGestureRequired
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
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
|
|
1211
1423
|
};
|
|
1212
1424
|
}
|
|
1425
|
+
return {
|
|
1426
|
+
translation,
|
|
1427
|
+
sourceLanguage,
|
|
1428
|
+
success: true,
|
|
1429
|
+
retryable: true
|
|
1430
|
+
};
|
|
1213
1431
|
}
|
|
1214
1432
|
static generateKey(sourceLanguage, targetLanguage) {
|
|
1215
1433
|
return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
|
|
@@ -1257,7 +1475,8 @@ var Client2 = class {
|
|
|
1257
1475
|
translation: result.translation,
|
|
1258
1476
|
sourceLanguage: result.sourceLanguage,
|
|
1259
1477
|
isTranslated: result.success,
|
|
1260
|
-
retryable: result.retryable
|
|
1478
|
+
retryable: result.retryable,
|
|
1479
|
+
requiresGesture: result.requiresGesture ?? false
|
|
1261
1480
|
}
|
|
1262
1481
|
],
|
|
1263
1482
|
targetLanguage: request.targetLanguage,
|
|
@@ -1272,6 +1491,9 @@ var Client2 = class {
|
|
|
1272
1491
|
errorMessage
|
|
1273
1492
|
};
|
|
1274
1493
|
}
|
|
1494
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1495
|
+
await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
|
|
1496
|
+
}
|
|
1275
1497
|
getConfig() {
|
|
1276
1498
|
return this.config;
|
|
1277
1499
|
}
|
|
@@ -1307,6 +1529,10 @@ var Client3 = class {
|
|
|
1307
1529
|
const translator = this.getFrontendTranslator();
|
|
1308
1530
|
return await translator.translateBatch(...args);
|
|
1309
1531
|
}
|
|
1532
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1533
|
+
const translator = this.getFrontendTranslator();
|
|
1534
|
+
await translator.downloadModel(sourceLanguage, targetLanguage);
|
|
1535
|
+
}
|
|
1310
1536
|
};
|
|
1311
1537
|
|
|
1312
1538
|
// src/translators/translator.ts
|
|
@@ -1321,6 +1547,17 @@ var Translator2 = class {
|
|
|
1321
1547
|
const translatorClient = this.getTranslatorClient();
|
|
1322
1548
|
return await translatorClient.translateBatch(translationRequests);
|
|
1323
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
|
+
}
|
|
1324
1561
|
setTranslatorClient(config2) {
|
|
1325
1562
|
if (config2.getToolConfig().strategy === "frontend_translations") {
|
|
1326
1563
|
this.translatorClient = new Client3(config2);
|
|
@@ -1439,6 +1676,9 @@ function AITranslationInnerProvider(props) {
|
|
|
1439
1676
|
} = props;
|
|
1440
1677
|
const [translationProgress, setTranslationProgress] = useState(null);
|
|
1441
1678
|
const [modelDownloadProgress, setModelDownloadProgress] = useState(null);
|
|
1679
|
+
const [modelConsent, setModelConsent] = useState(
|
|
1680
|
+
null
|
|
1681
|
+
);
|
|
1442
1682
|
const [config2, setConfig] = useState(new Config());
|
|
1443
1683
|
const translator = useRef(new Translator2(config2));
|
|
1444
1684
|
const eventHandler = useRef(new EventHandler(tool));
|
|
@@ -1490,6 +1730,14 @@ function AITranslationInnerProvider(props) {
|
|
|
1490
1730
|
);
|
|
1491
1731
|
return () => unsubscribe();
|
|
1492
1732
|
}, []);
|
|
1733
|
+
useEffect(() => {
|
|
1734
|
+
const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
|
|
1735
|
+
(request) => {
|
|
1736
|
+
setModelConsent(request);
|
|
1737
|
+
}
|
|
1738
|
+
);
|
|
1739
|
+
return () => unsubscribe();
|
|
1740
|
+
}, []);
|
|
1493
1741
|
useEffect(() => {
|
|
1494
1742
|
if (!isFetched || !remoteConfig) return;
|
|
1495
1743
|
const toolConfig = remoteConfig[tool] ?? remoteConfig;
|
|
@@ -1506,9 +1754,23 @@ function AITranslationInnerProvider(props) {
|
|
|
1506
1754
|
config2.setCompanyId(companyId);
|
|
1507
1755
|
config2.setProjectId(projectId);
|
|
1508
1756
|
const ait = useCallback(
|
|
1509
|
-
async (text) =>
|
|
1757
|
+
async (text) => {
|
|
1758
|
+
if (!enableAIT) {
|
|
1759
|
+
return text;
|
|
1760
|
+
}
|
|
1761
|
+
return aitFunction(text, translationRegistry.current, config2, tool);
|
|
1762
|
+
},
|
|
1510
1763
|
[tool, config2, enableAIT]
|
|
1511
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]);
|
|
1512
1774
|
const contextValue = {
|
|
1513
1775
|
ait,
|
|
1514
1776
|
locale,
|
|
@@ -1517,6 +1779,8 @@ function AITranslationInnerProvider(props) {
|
|
|
1517
1779
|
renderVersion: renderVersionManager.getVersion(),
|
|
1518
1780
|
translationProgress,
|
|
1519
1781
|
modelDownloadProgress,
|
|
1782
|
+
modelConsent,
|
|
1783
|
+
confirmModelDownload,
|
|
1520
1784
|
tool,
|
|
1521
1785
|
onTrackAnalyticsEvent,
|
|
1522
1786
|
page,
|
|
@@ -1551,8 +1815,8 @@ var BUTTON_TYPE = {
|
|
|
1551
1815
|
var ACTION = {
|
|
1552
1816
|
CLICKED: "clicked"
|
|
1553
1817
|
};
|
|
1554
|
-
function buildObject(
|
|
1555
|
-
return `${
|
|
1818
|
+
function buildObject(buttonType) {
|
|
1819
|
+
return `${buttonType}_button`;
|
|
1556
1820
|
}
|
|
1557
1821
|
function buildEventKey(parts) {
|
|
1558
1822
|
const { scope, tool, object, action } = parts;
|
|
@@ -1569,7 +1833,7 @@ function buildAnalyticEvent(params) {
|
|
|
1569
1833
|
additionalProperties
|
|
1570
1834
|
} = params;
|
|
1571
1835
|
const resolvedAction = action ?? ACTION.CLICKED;
|
|
1572
|
-
const object = buildObject(
|
|
1836
|
+
const object = buildObject(buttonType);
|
|
1573
1837
|
const key = buildEventKey({
|
|
1574
1838
|
scope,
|
|
1575
1839
|
tool,
|
|
@@ -1578,6 +1842,7 @@ function buildAnalyticEvent(params) {
|
|
|
1578
1842
|
});
|
|
1579
1843
|
const properties = {
|
|
1580
1844
|
...baseProperties,
|
|
1845
|
+
[`${buttonType}_button_location`]: pageContext,
|
|
1581
1846
|
...additionalProperties
|
|
1582
1847
|
};
|
|
1583
1848
|
return { key, properties };
|
package/package.json
CHANGED