@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.d.ts
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}`
|
|
@@ -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 };
|
package/dist/modern/index.js
CHANGED
|
@@ -670,6 +670,7 @@ var TRANSLATION_COMPLETE_EVENT_NAME = "ai-translation-completed";
|
|
|
670
670
|
var RERENDER_EVENT_NAME = "ai-translations-component-rerender";
|
|
671
671
|
var TRANSLATION_PROGRESS_EVENT_NAME = "ai-translations-progress";
|
|
672
672
|
var MODEL_DOWNLOAD_PROGRESS_EVENT_NAME = "ai-translations-model-download-progress";
|
|
673
|
+
var MODEL_CONSENT_REQUIRED_EVENT_NAME = "ai-translations-model-consent-required";
|
|
673
674
|
|
|
674
675
|
// src/utils/eventHandler.ts
|
|
675
676
|
var EventHandler = class {
|
|
@@ -743,12 +744,14 @@ var EventHandler = class {
|
|
|
743
744
|
* Not tool-scoped — a Chrome language model is shared across the entire
|
|
744
745
|
* browser session, so all Provider instances receive it.
|
|
745
746
|
*/
|
|
746
|
-
publishModelDownloadProgressEvent(loaded, total, progress) {
|
|
747
|
+
publishModelDownloadProgressEvent(loaded, total, progress, sourceLanguage, targetLanguage) {
|
|
747
748
|
this.aiTranslationEvents.publish(MODEL_DOWNLOAD_PROGRESS_EVENT_NAME, {
|
|
748
749
|
loaded,
|
|
749
750
|
total,
|
|
750
751
|
progress,
|
|
751
|
-
isComplete: loaded === total
|
|
752
|
+
isComplete: loaded === total,
|
|
753
|
+
sourceLanguage,
|
|
754
|
+
targetLanguage
|
|
752
755
|
});
|
|
753
756
|
}
|
|
754
757
|
subscribeToModelDownloadProgressEvent(callback) {
|
|
@@ -758,7 +761,29 @@ var EventHandler = class {
|
|
|
758
761
|
loaded: detail.data?.loaded ?? 0,
|
|
759
762
|
total: detail.data?.total ?? 0,
|
|
760
763
|
progress: detail.data?.progress ?? 0,
|
|
761
|
-
isComplete: detail.data?.isComplete ?? false
|
|
764
|
+
isComplete: detail.data?.isComplete ?? false,
|
|
765
|
+
sourceLanguage: detail.data?.sourceLanguage ?? "",
|
|
766
|
+
targetLanguage: detail.data?.targetLanguage ?? ""
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Publishes a request for the user to consent to downloading a specific
|
|
772
|
+
* source -> target language model. Like the download-progress event this is
|
|
773
|
+
* NOT tool-scoped — a language model is shared across the whole session.
|
|
774
|
+
*/
|
|
775
|
+
publishModelConsentRequiredEvent(sourceLanguage, targetLanguage) {
|
|
776
|
+
this.aiTranslationEvents.publish(MODEL_CONSENT_REQUIRED_EVENT_NAME, {
|
|
777
|
+
sourceLanguage,
|
|
778
|
+
targetLanguage
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
subscribeToModelConsentRequiredEvent(callback) {
|
|
782
|
+
return this.aiTranslationEvents.subscribe(
|
|
783
|
+
MODEL_CONSENT_REQUIRED_EVENT_NAME,
|
|
784
|
+
(detail) => callback({
|
|
785
|
+
sourceLanguage: detail.data?.sourceLanguage ?? "",
|
|
786
|
+
targetLanguage: detail.data?.targetLanguage ?? ""
|
|
762
787
|
})
|
|
763
788
|
);
|
|
764
789
|
}
|
|
@@ -767,6 +792,111 @@ var EventHandler = class {
|
|
|
767
792
|
}
|
|
768
793
|
};
|
|
769
794
|
|
|
795
|
+
// src/utils/modelDownloadGate.ts
|
|
796
|
+
var ModelDownloadGateImpl = class _ModelDownloadGateImpl {
|
|
797
|
+
eventHandler = null;
|
|
798
|
+
ready = /* @__PURE__ */ new Set();
|
|
799
|
+
pending = /* @__PURE__ */ new Map();
|
|
800
|
+
resolvers = /* @__PURE__ */ new Map();
|
|
801
|
+
rejecters = /* @__PURE__ */ new Map();
|
|
802
|
+
getEventHandler() {
|
|
803
|
+
if (!this.eventHandler) {
|
|
804
|
+
this.eventHandler = new EventHandler("__chrome_model__");
|
|
805
|
+
}
|
|
806
|
+
return this.eventHandler;
|
|
807
|
+
}
|
|
808
|
+
static key(sourceLanguage, targetLanguage) {
|
|
809
|
+
return `${sourceLanguage}->${targetLanguage}`;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Resolves once the model for `sourceLanguage -> targetLanguage` is downloaded
|
|
813
|
+
* and ready. Pauses (stays pending) until the user confirms the download and
|
|
814
|
+
* it completes. Re-prompts are deduped per model.
|
|
815
|
+
*/
|
|
816
|
+
ensureModelReady(sourceLanguage, targetLanguage) {
|
|
817
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
818
|
+
if (this.ready.has(key)) {
|
|
819
|
+
return Promise.resolve();
|
|
820
|
+
}
|
|
821
|
+
const existing = this.pending.get(key);
|
|
822
|
+
if (existing) {
|
|
823
|
+
return existing;
|
|
824
|
+
}
|
|
825
|
+
const promise = new Promise((resolve, reject) => {
|
|
826
|
+
this.resolvers.set(key, resolve);
|
|
827
|
+
this.rejecters.set(key, reject);
|
|
828
|
+
});
|
|
829
|
+
this.pending.set(key, promise);
|
|
830
|
+
this.getEventHandler().publishModelConsentRequiredEvent(
|
|
831
|
+
sourceLanguage,
|
|
832
|
+
targetLanguage
|
|
833
|
+
);
|
|
834
|
+
return promise;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Marks a model as ready and resolves any waiter. Resolved exclusively when
|
|
838
|
+
* `downloadModel` resolves (wired in the Provider). This guarantees Chrome's
|
|
839
|
+
* `Translator.create()` has returned and the translator instance is cached in
|
|
840
|
+
* `chromeTranslatorRegistry`, so the resumed translation reuses it instead of
|
|
841
|
+
* calling `Translator.create()` again outside the user gesture (which throws
|
|
842
|
+
* `NotAllowedError`). It is deliberately NOT resolved on the intermediate
|
|
843
|
+
* `MODEL_DOWNLOAD_PROGRESS` `isComplete` event, which fires before
|
|
844
|
+
* `Translator.create()` returns.
|
|
845
|
+
*/
|
|
846
|
+
markReady(sourceLanguage, targetLanguage) {
|
|
847
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
848
|
+
this.ready.add(key);
|
|
849
|
+
const resolve = this.resolvers.get(key);
|
|
850
|
+
if (resolve) {
|
|
851
|
+
resolve();
|
|
852
|
+
}
|
|
853
|
+
this.clearPending(key);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Marks a model download as failed and rejects any waiter so the paused
|
|
857
|
+
* translation run can move past this text instead of hanging forever. Called
|
|
858
|
+
* when `downloadModel` rejects (e.g. Chrome returns 404 for a pair it
|
|
859
|
+
* reported as downloadable). The pair is NOT cached as ready, so it can be
|
|
860
|
+
* re-attempted later.
|
|
861
|
+
*/
|
|
862
|
+
markFailed(sourceLanguage, targetLanguage, error) {
|
|
863
|
+
const key = _ModelDownloadGateImpl.key(sourceLanguage, targetLanguage);
|
|
864
|
+
const reject = this.rejecters.get(key);
|
|
865
|
+
if (reject) {
|
|
866
|
+
reject(
|
|
867
|
+
error instanceof Error ? error : new Error(`Model download failed for ${key}`)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
this.clearPending(key);
|
|
871
|
+
}
|
|
872
|
+
isReady(sourceLanguage, targetLanguage) {
|
|
873
|
+
return this.ready.has(
|
|
874
|
+
_ModelDownloadGateImpl.key(sourceLanguage, targetLanguage)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Clears all gate state. Intended for test teardown so cached "ready" models
|
|
879
|
+
* do not leak between cases.
|
|
880
|
+
*/
|
|
881
|
+
reset() {
|
|
882
|
+
this.ready.clear();
|
|
883
|
+
this.pending.clear();
|
|
884
|
+
this.resolvers.clear();
|
|
885
|
+
this.rejecters.clear();
|
|
886
|
+
}
|
|
887
|
+
clearPending(key) {
|
|
888
|
+
this.resolvers.delete(key);
|
|
889
|
+
this.rejecters.delete(key);
|
|
890
|
+
this.pending.delete(key);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var modelDownloadGate = new ModelDownloadGateImpl();
|
|
894
|
+
|
|
895
|
+
// src/utils/language.ts
|
|
896
|
+
function normalizeLanguage(language) {
|
|
897
|
+
return language.split("-")[0] ?? language;
|
|
898
|
+
}
|
|
899
|
+
|
|
770
900
|
// src/utils/renderVersionManager.ts
|
|
771
901
|
var RenderVersionManager = class {
|
|
772
902
|
version = 0;
|
|
@@ -823,27 +953,30 @@ var TranslationManager = class {
|
|
|
823
953
|
this.translationProgress.total = queueManager.queueSize(
|
|
824
954
|
this.currentTranslatorStrategy
|
|
825
955
|
);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
956
|
+
try {
|
|
957
|
+
do {
|
|
958
|
+
for (const batch of queueManager.generateBatches(
|
|
959
|
+
this.translator.getConfig()
|
|
960
|
+
)) {
|
|
961
|
+
batch.forEach((entry) => this.mfeToBeNotified.add(entry.tool));
|
|
962
|
+
const result = await this.translator.processTranslations(
|
|
963
|
+
batch.map(
|
|
964
|
+
(entry) => this.convertTranslationQueueEntryToTranslationRequest(entry)
|
|
965
|
+
)
|
|
966
|
+
);
|
|
967
|
+
this.setTranslationProgress(batch.length);
|
|
968
|
+
await this.updateDatabaseWithTranslations(result);
|
|
969
|
+
await this.notifyTranslationCompleted();
|
|
970
|
+
}
|
|
971
|
+
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
972
|
+
this.resetTranslationProgress();
|
|
973
|
+
this.publishTranslationProgress();
|
|
974
|
+
} finally {
|
|
975
|
+
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
976
|
+
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
977
|
+
} else {
|
|
978
|
+
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
839
979
|
}
|
|
840
|
-
} while (queueManager.queueSize(this.currentTranslatorStrategy) > 0);
|
|
841
|
-
this.resetTranslationProgress();
|
|
842
|
-
this.publishTranslationProgress();
|
|
843
|
-
if (this.currentTranslatorStrategy === "frontend_translations") {
|
|
844
|
-
globalThis._FRONTEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
845
|
-
} else {
|
|
846
|
-
globalThis._BACKEND_AI_TRANSLATION_IN_PROGRESS_ = false;
|
|
847
980
|
}
|
|
848
981
|
}
|
|
849
982
|
async updateDatabaseWithTranslations(translationResult) {
|
|
@@ -1013,6 +1146,16 @@ var Client = class {
|
|
|
1013
1146
|
return this.createTranslationErrorResponse(requests, errorMessage);
|
|
1014
1147
|
}
|
|
1015
1148
|
}
|
|
1149
|
+
/**
|
|
1150
|
+
* No-op: backend translation does not manage on-device models, so there is
|
|
1151
|
+
* nothing to download client-side. The parameters are kept to match the
|
|
1152
|
+
* {@link TranslatorClientAdapter.downloadModel} signature and make the no-op
|
|
1153
|
+
* intent explicit to callers.
|
|
1154
|
+
*/
|
|
1155
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1156
|
+
void sourceLanguage;
|
|
1157
|
+
void targetLanguage;
|
|
1158
|
+
}
|
|
1016
1159
|
createTranslationErrorResponse(requests, errorMessage) {
|
|
1017
1160
|
return {
|
|
1018
1161
|
responseBody: requests.map((req) => ({
|
|
@@ -1132,6 +1275,7 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1132
1275
|
}
|
|
1133
1276
|
}
|
|
1134
1277
|
static async init(sourceLanguage, targetLanguage) {
|
|
1278
|
+
this.ensureRegistry();
|
|
1135
1279
|
if (globalThis.chromeTranslatorRegistry.has(
|
|
1136
1280
|
this.generateKey(sourceLanguage, targetLanguage)
|
|
1137
1281
|
)) {
|
|
@@ -1146,7 +1290,30 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1146
1290
|
new _ChromeTranslator(translator)
|
|
1147
1291
|
);
|
|
1148
1292
|
}
|
|
1149
|
-
|
|
1293
|
+
/**
|
|
1294
|
+
* Downloads the on-device model for a source -> target language pair. Must be
|
|
1295
|
+
* invoked from a user gesture (e.g. a click handler), since Chrome requires an
|
|
1296
|
+
* active gesture to start a model download.
|
|
1297
|
+
*/
|
|
1298
|
+
static async downloadModel(sourceLanguage, targetLanguage) {
|
|
1299
|
+
await this.init(
|
|
1300
|
+
normalizeLanguage(sourceLanguage),
|
|
1301
|
+
normalizeLanguage(targetLanguage)
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Creates a Chrome translator instance for the pair.
|
|
1306
|
+
*
|
|
1307
|
+
* @param publishProgress - When true, `downloadprogress` events are forwarded
|
|
1308
|
+
* as `MODEL_DOWNLOAD_PROGRESS` events (driving the download-progress popup).
|
|
1309
|
+
* This should only be true for an explicit, consent-driven download
|
|
1310
|
+
* (`init`/`downloadModel`). When creating an already-downloaded
|
|
1311
|
+
* (`available`) model in the translate loop, Chrome still emits
|
|
1312
|
+
* `downloadprogress` while loading the pack from disk into memory; forwarding
|
|
1313
|
+
* those would wrongly show a "downloading" popup for a model that is already
|
|
1314
|
+
* downloaded, so the translate-loop path passes `false`.
|
|
1315
|
+
*/
|
|
1316
|
+
static async createTranslator(source, target, publishProgress = true) {
|
|
1150
1317
|
const key = this.generateKey(source, target);
|
|
1151
1318
|
let resolveReady;
|
|
1152
1319
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -1163,11 +1330,15 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1163
1330
|
const loaded = e.loaded ?? 0;
|
|
1164
1331
|
const total = e.total ?? 0;
|
|
1165
1332
|
const progress = total > 0 ? Math.round(loaded / total * 100) : 0;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1333
|
+
if (publishProgress) {
|
|
1334
|
+
getModelDownloadEventHandler().publishModelDownloadProgressEvent(
|
|
1335
|
+
loaded,
|
|
1336
|
+
total,
|
|
1337
|
+
progress,
|
|
1338
|
+
source,
|
|
1339
|
+
target
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1171
1342
|
if (total > 0 && loaded >= total) {
|
|
1172
1343
|
resolveReady();
|
|
1173
1344
|
}
|
|
@@ -1184,26 +1355,27 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1184
1355
|
}
|
|
1185
1356
|
static async translate(text, targetLanguage) {
|
|
1186
1357
|
this.ensureRegistry();
|
|
1358
|
+
const normalizedTarget = normalizeLanguage(targetLanguage);
|
|
1359
|
+
let sourceLanguage = normalizedTarget;
|
|
1187
1360
|
try {
|
|
1188
1361
|
const detector = await ChromeLanguageDetector.getInstance();
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(targetLanguage)) {
|
|
1362
|
+
sourceLanguage = normalizeLanguage(await detector.detectLanguage(text));
|
|
1363
|
+
if (!this.isSupportedLanguage(sourceLanguage) || !this.isSupportedLanguage(normalizedTarget)) {
|
|
1192
1364
|
return {
|
|
1193
1365
|
translation: text,
|
|
1194
1366
|
sourceLanguage,
|
|
1195
1367
|
success: false,
|
|
1196
1368
|
retryable: false,
|
|
1197
|
-
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${
|
|
1369
|
+
errorMessage: `Unsupported language pair: ${sourceLanguage} \u2192 ${normalizedTarget}`
|
|
1198
1370
|
};
|
|
1199
1371
|
}
|
|
1200
|
-
const
|
|
1201
|
-
if (!
|
|
1202
|
-
const
|
|
1372
|
+
const key = this.generateKey(sourceLanguage, normalizedTarget);
|
|
1373
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1374
|
+
const availability = await Translator.availability({
|
|
1203
1375
|
sourceLanguage,
|
|
1204
|
-
targetLanguage
|
|
1376
|
+
targetLanguage: normalizedTarget
|
|
1205
1377
|
});
|
|
1206
|
-
if (
|
|
1378
|
+
if (availability === "unavailable") {
|
|
1207
1379
|
return {
|
|
1208
1380
|
translation: text,
|
|
1209
1381
|
sourceLanguage,
|
|
@@ -1212,47 +1384,93 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1212
1384
|
errorMessage: "Translation not supported"
|
|
1213
1385
|
};
|
|
1214
1386
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1387
|
+
if (availability !== "available") {
|
|
1388
|
+
const isUserActivationActive = typeof navigator !== "undefined" && (navigator.userActivation?.isActive ?? false);
|
|
1389
|
+
if (isUserActivationActive) {
|
|
1390
|
+
await _ChromeTranslator.downloadModel(
|
|
1391
|
+
sourceLanguage,
|
|
1392
|
+
normalizedTarget
|
|
1393
|
+
);
|
|
1394
|
+
modelDownloadGate.markReady(sourceLanguage, normalizedTarget);
|
|
1395
|
+
} else {
|
|
1396
|
+
await modelDownloadGate.ensureModelReady(
|
|
1397
|
+
sourceLanguage,
|
|
1398
|
+
normalizedTarget
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1224
1401
|
}
|
|
1225
1402
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
this.translatorReadyPromises.delete(key);
|
|
1229
|
-
const translation = await instance.translator?.translate(text);
|
|
1230
|
-
if (!translation) {
|
|
1231
|
-
return {
|
|
1232
|
-
translation: text,
|
|
1233
|
-
sourceLanguage,
|
|
1234
|
-
success: false,
|
|
1235
|
-
errorMessage: "Translation failed",
|
|
1236
|
-
retryable: false
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
return {
|
|
1240
|
-
translation,
|
|
1403
|
+
return await this.createInstanceAndTranslate(
|
|
1404
|
+
text,
|
|
1241
1405
|
sourceLanguage,
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
};
|
|
1406
|
+
normalizedTarget
|
|
1407
|
+
);
|
|
1245
1408
|
} catch (error) {
|
|
1409
|
+
const isGestureRequired = error instanceof DOMException && error.name === "NotAllowedError";
|
|
1410
|
+
if (isGestureRequired) {
|
|
1411
|
+
try {
|
|
1412
|
+
await modelDownloadGate.ensureModelReady(
|
|
1413
|
+
sourceLanguage,
|
|
1414
|
+
normalizedTarget
|
|
1415
|
+
);
|
|
1416
|
+
return await this.createInstanceAndTranslate(
|
|
1417
|
+
text,
|
|
1418
|
+
sourceLanguage,
|
|
1419
|
+
normalizedTarget
|
|
1420
|
+
);
|
|
1421
|
+
} catch (retryError) {
|
|
1422
|
+
void retryError;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1246
1425
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1247
|
-
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1248
1426
|
return {
|
|
1249
1427
|
translation: text,
|
|
1250
|
-
sourceLanguage
|
|
1428
|
+
sourceLanguage,
|
|
1251
1429
|
success: false,
|
|
1252
1430
|
errorMessage: message,
|
|
1253
|
-
retryable:
|
|
1431
|
+
retryable: false,
|
|
1432
|
+
requiresGesture: isGestureRequired
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Creates the translator instance for the pair if it is not already cached,
|
|
1438
|
+
* waits for the model to be ready, and translates the text. The model is
|
|
1439
|
+
* expected to already be downloaded (or downloadable without a gesture) by
|
|
1440
|
+
* the time this is called.
|
|
1441
|
+
*/
|
|
1442
|
+
static async createInstanceAndTranslate(text, sourceLanguage, targetLanguage) {
|
|
1443
|
+
const key = this.generateKey(sourceLanguage, targetLanguage);
|
|
1444
|
+
if (!globalThis.chromeTranslatorRegistry.has(key)) {
|
|
1445
|
+
const chromeTranslator = await this.createTranslator(
|
|
1446
|
+
sourceLanguage,
|
|
1447
|
+
targetLanguage,
|
|
1448
|
+
false
|
|
1449
|
+
);
|
|
1450
|
+
globalThis.chromeTranslatorRegistry.set(
|
|
1451
|
+
key,
|
|
1452
|
+
new _ChromeTranslator(chromeTranslator)
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
const instance = globalThis.chromeTranslatorRegistry.get(key);
|
|
1456
|
+
await this.waitForReady(key);
|
|
1457
|
+
this.translatorReadyPromises.delete(key);
|
|
1458
|
+
const translation = await instance.translator?.translate(text);
|
|
1459
|
+
if (!translation) {
|
|
1460
|
+
return {
|
|
1461
|
+
translation: text,
|
|
1462
|
+
sourceLanguage,
|
|
1463
|
+
success: false,
|
|
1464
|
+
errorMessage: "Translation failed",
|
|
1465
|
+
retryable: false
|
|
1254
1466
|
};
|
|
1255
1467
|
}
|
|
1468
|
+
return {
|
|
1469
|
+
translation,
|
|
1470
|
+
sourceLanguage,
|
|
1471
|
+
success: true,
|
|
1472
|
+
retryable: true
|
|
1473
|
+
};
|
|
1256
1474
|
}
|
|
1257
1475
|
static generateKey(sourceLanguage, targetLanguage) {
|
|
1258
1476
|
return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
|
|
@@ -1300,7 +1518,8 @@ var Client2 = class {
|
|
|
1300
1518
|
translation: result.translation,
|
|
1301
1519
|
sourceLanguage: result.sourceLanguage,
|
|
1302
1520
|
isTranslated: result.success,
|
|
1303
|
-
retryable: result.retryable
|
|
1521
|
+
retryable: result.retryable,
|
|
1522
|
+
requiresGesture: result.requiresGesture ?? false
|
|
1304
1523
|
}
|
|
1305
1524
|
],
|
|
1306
1525
|
targetLanguage: request.targetLanguage,
|
|
@@ -1315,6 +1534,9 @@ var Client2 = class {
|
|
|
1315
1534
|
errorMessage
|
|
1316
1535
|
};
|
|
1317
1536
|
}
|
|
1537
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1538
|
+
await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
|
|
1539
|
+
}
|
|
1318
1540
|
getConfig() {
|
|
1319
1541
|
return this.config;
|
|
1320
1542
|
}
|
|
@@ -1350,6 +1572,10 @@ var Client3 = class {
|
|
|
1350
1572
|
const translator = this.getFrontendTranslator();
|
|
1351
1573
|
return await translator.translateBatch(...args);
|
|
1352
1574
|
}
|
|
1575
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1576
|
+
const translator = this.getFrontendTranslator();
|
|
1577
|
+
await translator.downloadModel(sourceLanguage, targetLanguage);
|
|
1578
|
+
}
|
|
1353
1579
|
};
|
|
1354
1580
|
|
|
1355
1581
|
// src/translators/translator.ts
|
|
@@ -1364,6 +1590,17 @@ var Translator2 = class {
|
|
|
1364
1590
|
const translatorClient = this.getTranslatorClient();
|
|
1365
1591
|
return await translatorClient.translateBatch(translationRequests);
|
|
1366
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Downloads the on-device model for a source -> target language pair via the
|
|
1595
|
+
* active translator client. Must be invoked from a user gesture for clients
|
|
1596
|
+
* (e.g. Chrome) that require one to start a model download.
|
|
1597
|
+
*/
|
|
1598
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1599
|
+
await this.getTranslatorClient().downloadModel(
|
|
1600
|
+
sourceLanguage,
|
|
1601
|
+
targetLanguage
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1367
1604
|
setTranslatorClient(config2) {
|
|
1368
1605
|
if (config2.getToolConfig().strategy === "frontend_translations") {
|
|
1369
1606
|
this.translatorClient = new Client3(config2);
|
|
@@ -1482,6 +1719,9 @@ function AITranslationInnerProvider(props) {
|
|
|
1482
1719
|
} = props;
|
|
1483
1720
|
const [translationProgress, setTranslationProgress] = (0, import_react.useState)(null);
|
|
1484
1721
|
const [modelDownloadProgress, setModelDownloadProgress] = (0, import_react.useState)(null);
|
|
1722
|
+
const [modelConsent, setModelConsent] = (0, import_react.useState)(
|
|
1723
|
+
null
|
|
1724
|
+
);
|
|
1485
1725
|
const [config2, setConfig] = (0, import_react.useState)(new Config());
|
|
1486
1726
|
const translator = (0, import_react.useRef)(new Translator2(config2));
|
|
1487
1727
|
const eventHandler = (0, import_react.useRef)(new EventHandler(tool));
|
|
@@ -1533,6 +1773,14 @@ function AITranslationInnerProvider(props) {
|
|
|
1533
1773
|
);
|
|
1534
1774
|
return () => unsubscribe();
|
|
1535
1775
|
}, []);
|
|
1776
|
+
(0, import_react.useEffect)(() => {
|
|
1777
|
+
const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
|
|
1778
|
+
(request) => {
|
|
1779
|
+
setModelConsent(request);
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
return () => unsubscribe();
|
|
1783
|
+
}, []);
|
|
1536
1784
|
(0, import_react.useEffect)(() => {
|
|
1537
1785
|
if (!isFetched || !remoteConfig) return;
|
|
1538
1786
|
const toolConfig = remoteConfig[tool] ?? remoteConfig;
|
|
@@ -1549,9 +1797,23 @@ function AITranslationInnerProvider(props) {
|
|
|
1549
1797
|
config2.setCompanyId(companyId);
|
|
1550
1798
|
config2.setProjectId(projectId);
|
|
1551
1799
|
const ait = (0, import_react.useCallback)(
|
|
1552
|
-
async (text) =>
|
|
1800
|
+
async (text) => {
|
|
1801
|
+
if (!enableAIT) {
|
|
1802
|
+
return text;
|
|
1803
|
+
}
|
|
1804
|
+
return aitFunction(text, translationRegistry.current, config2, tool);
|
|
1805
|
+
},
|
|
1553
1806
|
[tool, config2, enableAIT]
|
|
1554
1807
|
);
|
|
1808
|
+
const confirmModelDownload = (0, import_react.useCallback)(() => {
|
|
1809
|
+
if (!modelConsent) return;
|
|
1810
|
+
if (config2.getToolConfig().strategy !== "frontend_translations") return;
|
|
1811
|
+
const { sourceLanguage, targetLanguage } = modelConsent;
|
|
1812
|
+
setModelConsent(null);
|
|
1813
|
+
translator.current.downloadModel(sourceLanguage, targetLanguage).then(() => modelDownloadGate.markReady(sourceLanguage, targetLanguage)).catch(
|
|
1814
|
+
(error) => modelDownloadGate.markFailed(sourceLanguage, targetLanguage, error)
|
|
1815
|
+
);
|
|
1816
|
+
}, [modelConsent, config2]);
|
|
1555
1817
|
const contextValue = {
|
|
1556
1818
|
ait,
|
|
1557
1819
|
locale,
|
|
@@ -1560,6 +1822,8 @@ function AITranslationInnerProvider(props) {
|
|
|
1560
1822
|
renderVersion: renderVersionManager.getVersion(),
|
|
1561
1823
|
translationProgress,
|
|
1562
1824
|
modelDownloadProgress,
|
|
1825
|
+
modelConsent,
|
|
1826
|
+
confirmModelDownload,
|
|
1563
1827
|
tool,
|
|
1564
1828
|
onTrackAnalyticsEvent,
|
|
1565
1829
|
page,
|
|
@@ -1594,8 +1858,8 @@ var BUTTON_TYPE = {
|
|
|
1594
1858
|
var ACTION = {
|
|
1595
1859
|
CLICKED: "clicked"
|
|
1596
1860
|
};
|
|
1597
|
-
function buildObject(
|
|
1598
|
-
return `${
|
|
1861
|
+
function buildObject(buttonType) {
|
|
1862
|
+
return `${buttonType}_button`;
|
|
1599
1863
|
}
|
|
1600
1864
|
function buildEventKey(parts) {
|
|
1601
1865
|
const { scope, tool, object, action } = parts;
|
|
@@ -1612,7 +1876,7 @@ function buildAnalyticEvent(params) {
|
|
|
1612
1876
|
additionalProperties
|
|
1613
1877
|
} = params;
|
|
1614
1878
|
const resolvedAction = action ?? ACTION.CLICKED;
|
|
1615
|
-
const object = buildObject(
|
|
1879
|
+
const object = buildObject(buttonType);
|
|
1616
1880
|
const key = buildEventKey({
|
|
1617
1881
|
scope,
|
|
1618
1882
|
tool,
|
|
@@ -1621,6 +1885,7 @@ function buildAnalyticEvent(params) {
|
|
|
1621
1885
|
});
|
|
1622
1886
|
const properties = {
|
|
1623
1887
|
...baseProperties,
|
|
1888
|
+
[`${buttonType}_button_location`]: pageContext,
|
|
1624
1889
|
...additionalProperties
|
|
1625
1890
|
};
|
|
1626
1891
|
return { key, properties };
|