@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.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}`
|
|
@@ -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 };
|
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,94 @@ 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
1426
|
return {
|
|
1248
1427
|
translation: text,
|
|
1249
|
-
sourceLanguage
|
|
1428
|
+
sourceLanguage,
|
|
1250
1429
|
success: false,
|
|
1251
1430
|
errorMessage: message,
|
|
1252
|
-
retryable:
|
|
1431
|
+
retryable: false,
|
|
1432
|
+
requiresGesture: isGestureRequired
|
|
1253
1433
|
};
|
|
1254
1434
|
}
|
|
1255
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
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
translation,
|
|
1470
|
+
sourceLanguage,
|
|
1471
|
+
success: true,
|
|
1472
|
+
retryable: true
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1256
1475
|
static generateKey(sourceLanguage, targetLanguage) {
|
|
1257
1476
|
return `${sourceLanguage.toUpperCase()}-${targetLanguage.toUpperCase()}`;
|
|
1258
1477
|
}
|
|
@@ -1299,7 +1518,8 @@ var Client2 = class {
|
|
|
1299
1518
|
translation: result.translation,
|
|
1300
1519
|
sourceLanguage: result.sourceLanguage,
|
|
1301
1520
|
isTranslated: result.success,
|
|
1302
|
-
retryable: result.retryable
|
|
1521
|
+
retryable: result.retryable,
|
|
1522
|
+
requiresGesture: result.requiresGesture ?? false
|
|
1303
1523
|
}
|
|
1304
1524
|
],
|
|
1305
1525
|
targetLanguage: request.targetLanguage,
|
|
@@ -1314,6 +1534,9 @@ var Client2 = class {
|
|
|
1314
1534
|
errorMessage
|
|
1315
1535
|
};
|
|
1316
1536
|
}
|
|
1537
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1538
|
+
await ChromeTranslator.downloadModel(sourceLanguage, targetLanguage);
|
|
1539
|
+
}
|
|
1317
1540
|
getConfig() {
|
|
1318
1541
|
return this.config;
|
|
1319
1542
|
}
|
|
@@ -1349,6 +1572,10 @@ var Client3 = class {
|
|
|
1349
1572
|
const translator = this.getFrontendTranslator();
|
|
1350
1573
|
return await translator.translateBatch(...args);
|
|
1351
1574
|
}
|
|
1575
|
+
async downloadModel(sourceLanguage, targetLanguage) {
|
|
1576
|
+
const translator = this.getFrontendTranslator();
|
|
1577
|
+
await translator.downloadModel(sourceLanguage, targetLanguage);
|
|
1578
|
+
}
|
|
1352
1579
|
};
|
|
1353
1580
|
|
|
1354
1581
|
// src/translators/translator.ts
|
|
@@ -1363,6 +1590,17 @@ var Translator2 = class {
|
|
|
1363
1590
|
const translatorClient = this.getTranslatorClient();
|
|
1364
1591
|
return await translatorClient.translateBatch(translationRequests);
|
|
1365
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
|
+
}
|
|
1366
1604
|
setTranslatorClient(config2) {
|
|
1367
1605
|
if (config2.getToolConfig().strategy === "frontend_translations") {
|
|
1368
1606
|
this.translatorClient = new Client3(config2);
|
|
@@ -1481,6 +1719,9 @@ function AITranslationInnerProvider(props) {
|
|
|
1481
1719
|
} = props;
|
|
1482
1720
|
const [translationProgress, setTranslationProgress] = (0, import_react.useState)(null);
|
|
1483
1721
|
const [modelDownloadProgress, setModelDownloadProgress] = (0, import_react.useState)(null);
|
|
1722
|
+
const [modelConsent, setModelConsent] = (0, import_react.useState)(
|
|
1723
|
+
null
|
|
1724
|
+
);
|
|
1484
1725
|
const [config2, setConfig] = (0, import_react.useState)(new Config());
|
|
1485
1726
|
const translator = (0, import_react.useRef)(new Translator2(config2));
|
|
1486
1727
|
const eventHandler = (0, import_react.useRef)(new EventHandler(tool));
|
|
@@ -1532,6 +1773,14 @@ function AITranslationInnerProvider(props) {
|
|
|
1532
1773
|
);
|
|
1533
1774
|
return () => unsubscribe();
|
|
1534
1775
|
}, []);
|
|
1776
|
+
(0, import_react.useEffect)(() => {
|
|
1777
|
+
const unsubscribe = eventHandler.current.subscribeToModelConsentRequiredEvent(
|
|
1778
|
+
(request) => {
|
|
1779
|
+
setModelConsent(request);
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
return () => unsubscribe();
|
|
1783
|
+
}, []);
|
|
1535
1784
|
(0, import_react.useEffect)(() => {
|
|
1536
1785
|
if (!isFetched || !remoteConfig) return;
|
|
1537
1786
|
const toolConfig = remoteConfig[tool] ?? remoteConfig;
|
|
@@ -1548,9 +1797,23 @@ function AITranslationInnerProvider(props) {
|
|
|
1548
1797
|
config2.setCompanyId(companyId);
|
|
1549
1798
|
config2.setProjectId(projectId);
|
|
1550
1799
|
const ait = (0, import_react.useCallback)(
|
|
1551
|
-
async (text) =>
|
|
1800
|
+
async (text) => {
|
|
1801
|
+
if (!enableAIT) {
|
|
1802
|
+
return text;
|
|
1803
|
+
}
|
|
1804
|
+
return aitFunction(text, translationRegistry.current, config2, tool);
|
|
1805
|
+
},
|
|
1552
1806
|
[tool, config2, enableAIT]
|
|
1553
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]);
|
|
1554
1817
|
const contextValue = {
|
|
1555
1818
|
ait,
|
|
1556
1819
|
locale,
|
|
@@ -1559,6 +1822,8 @@ function AITranslationInnerProvider(props) {
|
|
|
1559
1822
|
renderVersion: renderVersionManager.getVersion(),
|
|
1560
1823
|
translationProgress,
|
|
1561
1824
|
modelDownloadProgress,
|
|
1825
|
+
modelConsent,
|
|
1826
|
+
confirmModelDownload,
|
|
1562
1827
|
tool,
|
|
1563
1828
|
onTrackAnalyticsEvent,
|
|
1564
1829
|
page,
|
|
@@ -1593,8 +1858,8 @@ var BUTTON_TYPE = {
|
|
|
1593
1858
|
var ACTION = {
|
|
1594
1859
|
CLICKED: "clicked"
|
|
1595
1860
|
};
|
|
1596
|
-
function buildObject(
|
|
1597
|
-
return `${
|
|
1861
|
+
function buildObject(buttonType) {
|
|
1862
|
+
return `${buttonType}_button`;
|
|
1598
1863
|
}
|
|
1599
1864
|
function buildEventKey(parts) {
|
|
1600
1865
|
const { scope, tool, object, action } = parts;
|
|
@@ -1611,7 +1876,7 @@ function buildAnalyticEvent(params) {
|
|
|
1611
1876
|
additionalProperties
|
|
1612
1877
|
} = params;
|
|
1613
1878
|
const resolvedAction = action ?? ACTION.CLICKED;
|
|
1614
|
-
const object = buildObject(
|
|
1879
|
+
const object = buildObject(buttonType);
|
|
1615
1880
|
const key = buildEventKey({
|
|
1616
1881
|
scope,
|
|
1617
1882
|
tool,
|
|
@@ -1620,6 +1885,7 @@ function buildAnalyticEvent(params) {
|
|
|
1620
1885
|
});
|
|
1621
1886
|
const properties = {
|
|
1622
1887
|
...baseProperties,
|
|
1888
|
+
[`${buttonType}_button_location`]: pageContext,
|
|
1623
1889
|
...additionalProperties
|
|
1624
1890
|
};
|
|
1625
1891
|
return { key, properties };
|
|
@@ -1899,7 +2165,7 @@ var CustomizableAITranslateText = ({
|
|
|
1899
2165
|
|
|
1900
2166
|
// src/utils/featureFlag.ts
|
|
1901
2167
|
var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
|
|
1902
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
2168
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1903
2169
|
var getAITranslationLDId = (domain) => {
|
|
1904
2170
|
const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
|
|
1905
2171
|
if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {
|