@procore/ai-translations 0.7.0 → 0.8.1
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 +98 -0
- package/dist/legacy/index.d.mts +56 -2
- package/dist/legacy/index.d.ts +56 -2
- package/dist/legacy/index.js +99 -2
- package/dist/legacy/index.mjs +88 -2
- package/dist/modern/index.d.mts +56 -2
- package/dist/modern/index.d.ts +56 -2
- package/dist/modern/index.js +99 -2
- package/dist/modern/index.mjs +88 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,104 @@ Props:
|
|
|
81
81
|
- `showHighlight?: boolean` (default `false`)
|
|
82
82
|
- `translatedIconProps?: TranslatedIconProps`
|
|
83
83
|
|
|
84
|
+
### `CustomizableAITranslateText`
|
|
85
|
+
|
|
86
|
+
Translates only chosen substrings of a string. Use anywhere a value mixes translatable and non-translatable text — descriptions, status fields, breadcrumbs, tags, cards, forms, modals — not just data tables.
|
|
87
|
+
|
|
88
|
+
Supply a `segmenter` function that splits the full text into an ordered list of `{ text, translate }` segments. Only segments with `translate: true` are sent through the AI pipeline; the rest are rendered verbatim.
|
|
89
|
+
|
|
90
|
+
Props:
|
|
91
|
+
|
|
92
|
+
- `text: string` — the full text value, passed to `segmenter`
|
|
93
|
+
- `shouldTranslate: boolean` — master switch; when `false` all segments render as plain text
|
|
94
|
+
- `showHighlight?: boolean` (default `false`) — whether to show the `TranslatedIcon`
|
|
95
|
+
- `highlightMode?: 'segment' | 'cell'` (default `'segment'`) — see below
|
|
96
|
+
- `segmenter?: (text: string) => TranslatableSegment[]` — split rule; when omitted the whole text is one translatable segment
|
|
97
|
+
- `translatedIconProps?: TranslatedIconProps`
|
|
98
|
+
|
|
99
|
+
#### Segmenter examples
|
|
100
|
+
|
|
101
|
+
**`"{code} - {label}"`** — keep the code prefix, translate only the label:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { CustomizableAITranslateText } from '@procore/ai-translations';
|
|
105
|
+
|
|
106
|
+
<CustomizableAITranslateText
|
|
107
|
+
text="INS-001 - Safety Inspection"
|
|
108
|
+
shouldTranslate={true}
|
|
109
|
+
segmenter={(text) => {
|
|
110
|
+
const idx = text.indexOf(' - ');
|
|
111
|
+
if (idx === -1) return [{ text, translate: true }];
|
|
112
|
+
return [
|
|
113
|
+
{ text: text.slice(0, idx + 3), translate: false }, // "INS-001 - "
|
|
114
|
+
{ text: text.slice(idx + 3), translate: true }, // "Safety Inspection"
|
|
115
|
+
];
|
|
116
|
+
}}
|
|
117
|
+
/>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`"Key: Value"`** — keep the field name, translate only the value:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<CustomizableAITranslateText
|
|
124
|
+
text="Status: Awaiting Review"
|
|
125
|
+
shouldTranslate={true}
|
|
126
|
+
segmenter={(text) => {
|
|
127
|
+
const idx = text.indexOf(': ');
|
|
128
|
+
if (idx === -1) return [{ text, translate: true }];
|
|
129
|
+
return [
|
|
130
|
+
{ text: text.slice(0, idx + 2), translate: false }, // "Status: "
|
|
131
|
+
{ text: text.slice(idx + 2), translate: true }, // "Awaiting Review"
|
|
132
|
+
];
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**`"A | B"`** — translate both halves, keep the divider:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<CustomizableAITranslateText
|
|
141
|
+
text="Safety | Critical"
|
|
142
|
+
shouldTranslate={true}
|
|
143
|
+
segmenter={(text) => {
|
|
144
|
+
const idx = text.indexOf(' | ');
|
|
145
|
+
if (idx === -1) return [{ text, translate: true }];
|
|
146
|
+
return [
|
|
147
|
+
{ text: text.slice(0, idx), translate: true }, // "Safety"
|
|
148
|
+
{ text: ' | ', translate: false }, // " | "
|
|
149
|
+
{ text: text.slice(idx + 3), translate: true }, // "Critical"
|
|
150
|
+
];
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**`"A > B > C"` breadcrumb** — translate every crumb independently:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<CustomizableAITranslateText
|
|
159
|
+
text="Projects > Building A > Floor 3"
|
|
160
|
+
shouldTranslate={true}
|
|
161
|
+
segmenter={(text) => {
|
|
162
|
+
const parts = text.split(' > ');
|
|
163
|
+
return parts.flatMap((part, i) => [
|
|
164
|
+
{ text: part, translate: true },
|
|
165
|
+
...(i < parts.length - 1 ? [{ text: ' > ', translate: false }] : []),
|
|
166
|
+
]);
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `highlightMode`
|
|
172
|
+
|
|
173
|
+
Controls where `TranslatedIcon` appears when a translation actually changes the text:
|
|
174
|
+
|
|
175
|
+
| Mode | Behavior |
|
|
176
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
177
|
+
| `'segment'` (default) | One icon next to each segment whose translated output differs from its source. Precisely marks which substrings are AI-generated. |
|
|
178
|
+
| `'cell'` | A single icon at the very start of the value, only when at least one segment's output actually changed. Cleaner for values with many translated segments. |
|
|
179
|
+
|
|
180
|
+
`showHighlight={false}` suppresses all icons regardless of mode.
|
|
181
|
+
|
|
84
182
|
### `useAITranslation()`
|
|
85
183
|
|
|
86
184
|
Returns:
|
package/dist/legacy/index.d.mts
CHANGED
|
@@ -263,10 +263,64 @@ interface AITranslateTextProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare const AITranslateText: React.FC<AITranslateTextProps>;
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A single piece of a text value. When `translate` is `true`, the segment is
|
|
268
|
+
* sent through the AI translation pipeline; otherwise it is rendered as plain
|
|
269
|
+
* text and never reaches the registry.
|
|
270
|
+
*/
|
|
271
|
+
interface TranslatableSegment {
|
|
272
|
+
text: string;
|
|
273
|
+
translate: boolean;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pure function that turns a string into an ordered list of translatable /
|
|
277
|
+
* non-translatable segments. Called on every render, so it should be
|
|
278
|
+
* inexpensive and free of side effects.
|
|
279
|
+
*/
|
|
280
|
+
type TextSegmenter = (text: string) => TranslatableSegment[];
|
|
281
|
+
interface CustomizableAITranslateTextProps {
|
|
282
|
+
/** The full text value. Fed to the segmenter or — when no segmenter is
|
|
283
|
+
* supplied — translated as a single piece. */
|
|
284
|
+
text: string;
|
|
285
|
+
shouldTranslate: boolean;
|
|
286
|
+
/** Master switch for the highlight icon. Defaults to `false`. When `true`,
|
|
287
|
+
* a single icon is shown at the start of the value if at least one segment's
|
|
288
|
+
* translation actually changed the source text. */
|
|
289
|
+
showHighlight?: boolean;
|
|
290
|
+
segmenter?: TextSegmenter;
|
|
291
|
+
translatedIconProps?: TranslatedIconProps;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Generic component for translating chosen substrings of a string. Owns its
|
|
295
|
+
* translation lifecycle directly (does not compose `AITranslateText`), so it
|
|
296
|
+
* can decide whether a cell-level highlight icon is appropriate based on
|
|
297
|
+
* whether any segment's translation actually changed the source.
|
|
298
|
+
*
|
|
299
|
+
* When `showHighlight` is `true`, a single icon is rendered at the start of
|
|
300
|
+
* the value if at least one segment changed — i.e. always cell-level mode.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* // Translate only the label half of "{code} - {label}"
|
|
304
|
+
* <CustomizableAITranslateText
|
|
305
|
+
* text="INS-001 - Safety Inspection"
|
|
306
|
+
* shouldTranslate={true}
|
|
307
|
+
* showHighlight={true}
|
|
308
|
+
* segmenter={(t) => {
|
|
309
|
+
* const idx = t.indexOf(' - ');
|
|
310
|
+
* if (idx === -1) return [{ text: t, translate: true }];
|
|
311
|
+
* return [
|
|
312
|
+
* { text: t.slice(0, idx + 3), translate: false },
|
|
313
|
+
* { text: t.slice(idx + 3), translate: true },
|
|
314
|
+
* ];
|
|
315
|
+
* }}
|
|
316
|
+
* />
|
|
317
|
+
*/
|
|
318
|
+
declare const CustomizableAITranslateText: React.FC<CustomizableAITranslateTextProps>;
|
|
319
|
+
|
|
266
320
|
/**
|
|
267
321
|
* The key used to store/retrieve the feature flag in local storage.
|
|
268
322
|
*/
|
|
269
|
-
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
323
|
+
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
270
324
|
/**
|
|
271
325
|
* Retrieves the LD ID for the AI translation feature flag based on the domain.
|
|
272
326
|
* @param domain - The domain to determine the LD ID for
|
|
@@ -281,4 +335,4 @@ declare global {
|
|
|
281
335
|
var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
|
|
282
336
|
}
|
|
283
337
|
|
|
284
|
-
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, type EventKeyParts, type Scope, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|
|
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 };
|
package/dist/legacy/index.d.ts
CHANGED
|
@@ -263,10 +263,64 @@ interface AITranslateTextProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare const AITranslateText: React.FC<AITranslateTextProps>;
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A single piece of a text value. When `translate` is `true`, the segment is
|
|
268
|
+
* sent through the AI translation pipeline; otherwise it is rendered as plain
|
|
269
|
+
* text and never reaches the registry.
|
|
270
|
+
*/
|
|
271
|
+
interface TranslatableSegment {
|
|
272
|
+
text: string;
|
|
273
|
+
translate: boolean;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pure function that turns a string into an ordered list of translatable /
|
|
277
|
+
* non-translatable segments. Called on every render, so it should be
|
|
278
|
+
* inexpensive and free of side effects.
|
|
279
|
+
*/
|
|
280
|
+
type TextSegmenter = (text: string) => TranslatableSegment[];
|
|
281
|
+
interface CustomizableAITranslateTextProps {
|
|
282
|
+
/** The full text value. Fed to the segmenter or — when no segmenter is
|
|
283
|
+
* supplied — translated as a single piece. */
|
|
284
|
+
text: string;
|
|
285
|
+
shouldTranslate: boolean;
|
|
286
|
+
/** Master switch for the highlight icon. Defaults to `false`. When `true`,
|
|
287
|
+
* a single icon is shown at the start of the value if at least one segment's
|
|
288
|
+
* translation actually changed the source text. */
|
|
289
|
+
showHighlight?: boolean;
|
|
290
|
+
segmenter?: TextSegmenter;
|
|
291
|
+
translatedIconProps?: TranslatedIconProps;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Generic component for translating chosen substrings of a string. Owns its
|
|
295
|
+
* translation lifecycle directly (does not compose `AITranslateText`), so it
|
|
296
|
+
* can decide whether a cell-level highlight icon is appropriate based on
|
|
297
|
+
* whether any segment's translation actually changed the source.
|
|
298
|
+
*
|
|
299
|
+
* When `showHighlight` is `true`, a single icon is rendered at the start of
|
|
300
|
+
* the value if at least one segment changed — i.e. always cell-level mode.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* // Translate only the label half of "{code} - {label}"
|
|
304
|
+
* <CustomizableAITranslateText
|
|
305
|
+
* text="INS-001 - Safety Inspection"
|
|
306
|
+
* shouldTranslate={true}
|
|
307
|
+
* showHighlight={true}
|
|
308
|
+
* segmenter={(t) => {
|
|
309
|
+
* const idx = t.indexOf(' - ');
|
|
310
|
+
* if (idx === -1) return [{ text: t, translate: true }];
|
|
311
|
+
* return [
|
|
312
|
+
* { text: t.slice(0, idx + 3), translate: false },
|
|
313
|
+
* { text: t.slice(idx + 3), translate: true },
|
|
314
|
+
* ];
|
|
315
|
+
* }}
|
|
316
|
+
* />
|
|
317
|
+
*/
|
|
318
|
+
declare const CustomizableAITranslateText: React.FC<CustomizableAITranslateTextProps>;
|
|
319
|
+
|
|
266
320
|
/**
|
|
267
321
|
* The key used to store/retrieve the feature flag in local storage.
|
|
268
322
|
*/
|
|
269
|
-
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
323
|
+
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
270
324
|
/**
|
|
271
325
|
* Retrieves the LD ID for the AI translation feature flag based on the domain.
|
|
272
326
|
* @param domain - The domain to determine the LD ID for
|
|
@@ -281,4 +335,4 @@ declare global {
|
|
|
281
335
|
var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
|
|
282
336
|
}
|
|
283
337
|
|
|
284
|
-
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, type EventKeyParts, type Scope, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|
|
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 };
|
package/dist/legacy/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
9
|
var __export = (target, all) => {
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
31
|
|
|
@@ -27,6 +37,7 @@ __export(index_exports, {
|
|
|
27
37
|
AITranslationProvider: () => AITranslationProvider,
|
|
28
38
|
AI_TRANSLATION_FEATURE_FLAG_KEY: () => AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
29
39
|
BUTTON_TYPE: () => BUTTON_TYPE,
|
|
40
|
+
CustomizableAITranslateText: () => CustomizableAITranslateText,
|
|
30
41
|
buildAnalyticEvent: () => buildAnalyticEvent,
|
|
31
42
|
buildEventKey: () => buildEventKey,
|
|
32
43
|
buildObject: () => buildObject,
|
|
@@ -1254,12 +1265,13 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1254
1265
|
};
|
|
1255
1266
|
} catch (error) {
|
|
1256
1267
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1268
|
+
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1257
1269
|
return {
|
|
1258
1270
|
translation: text,
|
|
1259
1271
|
sourceLanguage: targetLanguage,
|
|
1260
1272
|
success: false,
|
|
1261
1273
|
errorMessage: message,
|
|
1262
|
-
retryable:
|
|
1274
|
+
retryable: !isPermanentFailure
|
|
1263
1275
|
};
|
|
1264
1276
|
}
|
|
1265
1277
|
}
|
|
@@ -1825,9 +1837,93 @@ var AITranslateText = ({
|
|
|
1825
1837
|
] });
|
|
1826
1838
|
};
|
|
1827
1839
|
|
|
1840
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1841
|
+
var import_react6 = __toESM(require("react"));
|
|
1842
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1843
|
+
var CustomizableAITranslateText = ({
|
|
1844
|
+
text,
|
|
1845
|
+
shouldTranslate,
|
|
1846
|
+
showHighlight = false,
|
|
1847
|
+
segmenter,
|
|
1848
|
+
translatedIconProps
|
|
1849
|
+
}) => {
|
|
1850
|
+
const context = (0, import_react6.useContext)(AITranslationContext);
|
|
1851
|
+
const ait = context == null ? void 0 : context.ait;
|
|
1852
|
+
const segments = (0, import_react6.useMemo)(
|
|
1853
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1854
|
+
[segmenter, text, shouldTranslate]
|
|
1855
|
+
);
|
|
1856
|
+
const [displayTexts, setDisplayTexts] = (0, import_react6.useState)(
|
|
1857
|
+
() => segments.map((s) => s.text)
|
|
1858
|
+
);
|
|
1859
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1860
|
+
(0, import_react6.useEffect)(() => {
|
|
1861
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1862
|
+
}, [segmentsKey]);
|
|
1863
|
+
(0, import_react6.useEffect)(() => {
|
|
1864
|
+
if (!ait || !shouldTranslate) {
|
|
1865
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
let cancelled = false;
|
|
1869
|
+
Promise.all(
|
|
1870
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1871
|
+
).then((resolved) => {
|
|
1872
|
+
if (cancelled) return;
|
|
1873
|
+
setDisplayTexts(resolved);
|
|
1874
|
+
}).catch(() => {
|
|
1875
|
+
if (cancelled) return;
|
|
1876
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1877
|
+
});
|
|
1878
|
+
return () => {
|
|
1879
|
+
cancelled = true;
|
|
1880
|
+
};
|
|
1881
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1882
|
+
const eventHandlerRef = (0, import_react6.useRef)(null);
|
|
1883
|
+
(0, import_react6.useEffect)(() => {
|
|
1884
|
+
if (!context) return;
|
|
1885
|
+
if (!eventHandlerRef.current) {
|
|
1886
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1887
|
+
}
|
|
1888
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1889
|
+
async (sourceTexts) => {
|
|
1890
|
+
if (!shouldTranslate) return;
|
|
1891
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1892
|
+
const sourceSet = new Set(sourceTexts);
|
|
1893
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1894
|
+
if (targets.length === 0) return;
|
|
1895
|
+
const fresh = await Promise.all(
|
|
1896
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1897
|
+
);
|
|
1898
|
+
setDisplayTexts((prev) => {
|
|
1899
|
+
const next = [...prev];
|
|
1900
|
+
targets.forEach(({ i }, j) => {
|
|
1901
|
+
const value = fresh[j];
|
|
1902
|
+
if (value !== void 0) next[i] = value;
|
|
1903
|
+
});
|
|
1904
|
+
return next;
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
);
|
|
1908
|
+
return () => unsubscribe();
|
|
1909
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1910
|
+
const changedFlags = segments.map(
|
|
1911
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1912
|
+
);
|
|
1913
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1914
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1915
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1916
|
+
showCellIcon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TranslatedIcon, { ...translatedIconProps }),
|
|
1917
|
+
segments.map((seg, i) => {
|
|
1918
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1919
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react6.default.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1920
|
+
})
|
|
1921
|
+
] });
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1828
1924
|
// src/utils/featureFlag.ts
|
|
1829
1925
|
var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
|
|
1830
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1926
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1831
1927
|
var getAITranslationLDId = (domain) => {
|
|
1832
1928
|
const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
|
|
1833
1929
|
if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {
|
|
@@ -1850,6 +1946,7 @@ var getAITranslationLDId = (domain) => {
|
|
|
1850
1946
|
AITranslationProvider,
|
|
1851
1947
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1852
1948
|
BUTTON_TYPE,
|
|
1949
|
+
CustomizableAITranslateText,
|
|
1853
1950
|
buildAnalyticEvent,
|
|
1854
1951
|
buildEventKey,
|
|
1855
1952
|
buildObject,
|
package/dist/legacy/index.mjs
CHANGED
|
@@ -1224,12 +1224,13 @@ var _ChromeTranslator = class _ChromeTranslator {
|
|
|
1224
1224
|
};
|
|
1225
1225
|
} catch (error) {
|
|
1226
1226
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1227
|
+
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1227
1228
|
return {
|
|
1228
1229
|
translation: text,
|
|
1229
1230
|
sourceLanguage: targetLanguage,
|
|
1230
1231
|
success: false,
|
|
1231
1232
|
errorMessage: message,
|
|
1232
|
-
retryable:
|
|
1233
|
+
retryable: !isPermanentFailure
|
|
1233
1234
|
};
|
|
1234
1235
|
}
|
|
1235
1236
|
}
|
|
@@ -1801,9 +1802,93 @@ var AITranslateText = ({
|
|
|
1801
1802
|
] });
|
|
1802
1803
|
};
|
|
1803
1804
|
|
|
1805
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1806
|
+
import React4, { useContext as useContext4, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1807
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1808
|
+
var CustomizableAITranslateText = ({
|
|
1809
|
+
text,
|
|
1810
|
+
shouldTranslate,
|
|
1811
|
+
showHighlight = false,
|
|
1812
|
+
segmenter,
|
|
1813
|
+
translatedIconProps
|
|
1814
|
+
}) => {
|
|
1815
|
+
const context = useContext4(AITranslationContext);
|
|
1816
|
+
const ait = context == null ? void 0 : context.ait;
|
|
1817
|
+
const segments = useMemo(
|
|
1818
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1819
|
+
[segmenter, text, shouldTranslate]
|
|
1820
|
+
);
|
|
1821
|
+
const [displayTexts, setDisplayTexts] = useState3(
|
|
1822
|
+
() => segments.map((s) => s.text)
|
|
1823
|
+
);
|
|
1824
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1825
|
+
useEffect3(() => {
|
|
1826
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1827
|
+
}, [segmentsKey]);
|
|
1828
|
+
useEffect3(() => {
|
|
1829
|
+
if (!ait || !shouldTranslate) {
|
|
1830
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
let cancelled = false;
|
|
1834
|
+
Promise.all(
|
|
1835
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1836
|
+
).then((resolved) => {
|
|
1837
|
+
if (cancelled) return;
|
|
1838
|
+
setDisplayTexts(resolved);
|
|
1839
|
+
}).catch(() => {
|
|
1840
|
+
if (cancelled) return;
|
|
1841
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1842
|
+
});
|
|
1843
|
+
return () => {
|
|
1844
|
+
cancelled = true;
|
|
1845
|
+
};
|
|
1846
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1847
|
+
const eventHandlerRef = useRef3(null);
|
|
1848
|
+
useEffect3(() => {
|
|
1849
|
+
if (!context) return;
|
|
1850
|
+
if (!eventHandlerRef.current) {
|
|
1851
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1852
|
+
}
|
|
1853
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1854
|
+
async (sourceTexts) => {
|
|
1855
|
+
if (!shouldTranslate) return;
|
|
1856
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1857
|
+
const sourceSet = new Set(sourceTexts);
|
|
1858
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1859
|
+
if (targets.length === 0) return;
|
|
1860
|
+
const fresh = await Promise.all(
|
|
1861
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1862
|
+
);
|
|
1863
|
+
setDisplayTexts((prev) => {
|
|
1864
|
+
const next = [...prev];
|
|
1865
|
+
targets.forEach(({ i }, j) => {
|
|
1866
|
+
const value = fresh[j];
|
|
1867
|
+
if (value !== void 0) next[i] = value;
|
|
1868
|
+
});
|
|
1869
|
+
return next;
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
);
|
|
1873
|
+
return () => unsubscribe();
|
|
1874
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1875
|
+
const changedFlags = segments.map(
|
|
1876
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1877
|
+
);
|
|
1878
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1879
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1880
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1881
|
+
showCellIcon && /* @__PURE__ */ jsx4(TranslatedIcon, { ...translatedIconProps }),
|
|
1882
|
+
segments.map((seg, i) => {
|
|
1883
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1884
|
+
return /* @__PURE__ */ jsx4(React4.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1885
|
+
})
|
|
1886
|
+
] });
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1804
1889
|
// src/utils/featureFlag.ts
|
|
1805
1890
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1806
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1891
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1807
1892
|
var getAITranslationLDId = (domain) => {
|
|
1808
1893
|
const { environment, zone } = getProcoreZone(domain);
|
|
1809
1894
|
if (isFederalZone(zone)) {
|
|
@@ -1825,6 +1910,7 @@ export {
|
|
|
1825
1910
|
AITranslationProvider,
|
|
1826
1911
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1827
1912
|
BUTTON_TYPE,
|
|
1913
|
+
CustomizableAITranslateText,
|
|
1828
1914
|
buildAnalyticEvent,
|
|
1829
1915
|
buildEventKey,
|
|
1830
1916
|
buildObject,
|
package/dist/modern/index.d.mts
CHANGED
|
@@ -263,10 +263,64 @@ interface AITranslateTextProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare const AITranslateText: React.FC<AITranslateTextProps>;
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A single piece of a text value. When `translate` is `true`, the segment is
|
|
268
|
+
* sent through the AI translation pipeline; otherwise it is rendered as plain
|
|
269
|
+
* text and never reaches the registry.
|
|
270
|
+
*/
|
|
271
|
+
interface TranslatableSegment {
|
|
272
|
+
text: string;
|
|
273
|
+
translate: boolean;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pure function that turns a string into an ordered list of translatable /
|
|
277
|
+
* non-translatable segments. Called on every render, so it should be
|
|
278
|
+
* inexpensive and free of side effects.
|
|
279
|
+
*/
|
|
280
|
+
type TextSegmenter = (text: string) => TranslatableSegment[];
|
|
281
|
+
interface CustomizableAITranslateTextProps {
|
|
282
|
+
/** The full text value. Fed to the segmenter or — when no segmenter is
|
|
283
|
+
* supplied — translated as a single piece. */
|
|
284
|
+
text: string;
|
|
285
|
+
shouldTranslate: boolean;
|
|
286
|
+
/** Master switch for the highlight icon. Defaults to `false`. When `true`,
|
|
287
|
+
* a single icon is shown at the start of the value if at least one segment's
|
|
288
|
+
* translation actually changed the source text. */
|
|
289
|
+
showHighlight?: boolean;
|
|
290
|
+
segmenter?: TextSegmenter;
|
|
291
|
+
translatedIconProps?: TranslatedIconProps;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Generic component for translating chosen substrings of a string. Owns its
|
|
295
|
+
* translation lifecycle directly (does not compose `AITranslateText`), so it
|
|
296
|
+
* can decide whether a cell-level highlight icon is appropriate based on
|
|
297
|
+
* whether any segment's translation actually changed the source.
|
|
298
|
+
*
|
|
299
|
+
* When `showHighlight` is `true`, a single icon is rendered at the start of
|
|
300
|
+
* the value if at least one segment changed — i.e. always cell-level mode.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* // Translate only the label half of "{code} - {label}"
|
|
304
|
+
* <CustomizableAITranslateText
|
|
305
|
+
* text="INS-001 - Safety Inspection"
|
|
306
|
+
* shouldTranslate={true}
|
|
307
|
+
* showHighlight={true}
|
|
308
|
+
* segmenter={(t) => {
|
|
309
|
+
* const idx = t.indexOf(' - ');
|
|
310
|
+
* if (idx === -1) return [{ text: t, translate: true }];
|
|
311
|
+
* return [
|
|
312
|
+
* { text: t.slice(0, idx + 3), translate: false },
|
|
313
|
+
* { text: t.slice(idx + 3), translate: true },
|
|
314
|
+
* ];
|
|
315
|
+
* }}
|
|
316
|
+
* />
|
|
317
|
+
*/
|
|
318
|
+
declare const CustomizableAITranslateText: React.FC<CustomizableAITranslateTextProps>;
|
|
319
|
+
|
|
266
320
|
/**
|
|
267
321
|
* The key used to store/retrieve the feature flag in local storage.
|
|
268
322
|
*/
|
|
269
|
-
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
323
|
+
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
270
324
|
/**
|
|
271
325
|
* Retrieves the LD ID for the AI translation feature flag based on the domain.
|
|
272
326
|
* @param domain - The domain to determine the LD ID for
|
|
@@ -281,4 +335,4 @@ declare global {
|
|
|
281
335
|
var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
|
|
282
336
|
}
|
|
283
337
|
|
|
284
|
-
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, type EventKeyParts, type Scope, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|
|
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 };
|
package/dist/modern/index.d.ts
CHANGED
|
@@ -263,10 +263,64 @@ interface AITranslateTextProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare const AITranslateText: React.FC<AITranslateTextProps>;
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A single piece of a text value. When `translate` is `true`, the segment is
|
|
268
|
+
* sent through the AI translation pipeline; otherwise it is rendered as plain
|
|
269
|
+
* text and never reaches the registry.
|
|
270
|
+
*/
|
|
271
|
+
interface TranslatableSegment {
|
|
272
|
+
text: string;
|
|
273
|
+
translate: boolean;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pure function that turns a string into an ordered list of translatable /
|
|
277
|
+
* non-translatable segments. Called on every render, so it should be
|
|
278
|
+
* inexpensive and free of side effects.
|
|
279
|
+
*/
|
|
280
|
+
type TextSegmenter = (text: string) => TranslatableSegment[];
|
|
281
|
+
interface CustomizableAITranslateTextProps {
|
|
282
|
+
/** The full text value. Fed to the segmenter or — when no segmenter is
|
|
283
|
+
* supplied — translated as a single piece. */
|
|
284
|
+
text: string;
|
|
285
|
+
shouldTranslate: boolean;
|
|
286
|
+
/** Master switch for the highlight icon. Defaults to `false`. When `true`,
|
|
287
|
+
* a single icon is shown at the start of the value if at least one segment's
|
|
288
|
+
* translation actually changed the source text. */
|
|
289
|
+
showHighlight?: boolean;
|
|
290
|
+
segmenter?: TextSegmenter;
|
|
291
|
+
translatedIconProps?: TranslatedIconProps;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Generic component for translating chosen substrings of a string. Owns its
|
|
295
|
+
* translation lifecycle directly (does not compose `AITranslateText`), so it
|
|
296
|
+
* can decide whether a cell-level highlight icon is appropriate based on
|
|
297
|
+
* whether any segment's translation actually changed the source.
|
|
298
|
+
*
|
|
299
|
+
* When `showHighlight` is `true`, a single icon is rendered at the start of
|
|
300
|
+
* the value if at least one segment changed — i.e. always cell-level mode.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* // Translate only the label half of "{code} - {label}"
|
|
304
|
+
* <CustomizableAITranslateText
|
|
305
|
+
* text="INS-001 - Safety Inspection"
|
|
306
|
+
* shouldTranslate={true}
|
|
307
|
+
* showHighlight={true}
|
|
308
|
+
* segmenter={(t) => {
|
|
309
|
+
* const idx = t.indexOf(' - ');
|
|
310
|
+
* if (idx === -1) return [{ text: t, translate: true }];
|
|
311
|
+
* return [
|
|
312
|
+
* { text: t.slice(0, idx + 3), translate: false },
|
|
313
|
+
* { text: t.slice(idx + 3), translate: true },
|
|
314
|
+
* ];
|
|
315
|
+
* }}
|
|
316
|
+
* />
|
|
317
|
+
*/
|
|
318
|
+
declare const CustomizableAITranslateText: React.FC<CustomizableAITranslateTextProps>;
|
|
319
|
+
|
|
266
320
|
/**
|
|
267
321
|
* The key used to store/retrieve the feature flag in local storage.
|
|
268
322
|
*/
|
|
269
|
-
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
323
|
+
declare const AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
270
324
|
/**
|
|
271
325
|
* Retrieves the LD ID for the AI translation feature flag based on the domain.
|
|
272
326
|
* @param domain - The domain to determine the LD ID for
|
|
@@ -281,4 +335,4 @@ declare global {
|
|
|
281
335
|
var _BACKEND_AI_TRANSLATION_IN_PROGRESS_: boolean;
|
|
282
336
|
}
|
|
283
337
|
|
|
284
|
-
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, type EventKeyParts, type Scope, type TranslatedIconProps, type UseAIAnalyticsReturn, type UseConfigOptions, buildAnalyticEvent, buildEventKey, buildObject, getAITranslationLDId, isSupportedBrowser, useAIAnalytics, useAITranslation, useConfig };
|
|
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 };
|
package/dist/modern/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -25,6 +35,7 @@ __export(index_exports, {
|
|
|
25
35
|
AITranslationProvider: () => AITranslationProvider,
|
|
26
36
|
AI_TRANSLATION_FEATURE_FLAG_KEY: () => AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
27
37
|
BUTTON_TYPE: () => BUTTON_TYPE,
|
|
38
|
+
CustomizableAITranslateText: () => CustomizableAITranslateText,
|
|
28
39
|
buildAnalyticEvent: () => buildAnalyticEvent,
|
|
29
40
|
buildEventKey: () => buildEventKey,
|
|
30
41
|
buildObject: () => buildObject,
|
|
@@ -1233,12 +1244,13 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1233
1244
|
};
|
|
1234
1245
|
} catch (error) {
|
|
1235
1246
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1247
|
+
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1236
1248
|
return {
|
|
1237
1249
|
translation: text,
|
|
1238
1250
|
sourceLanguage: targetLanguage,
|
|
1239
1251
|
success: false,
|
|
1240
1252
|
errorMessage: message,
|
|
1241
|
-
retryable:
|
|
1253
|
+
retryable: !isPermanentFailure
|
|
1242
1254
|
};
|
|
1243
1255
|
}
|
|
1244
1256
|
}
|
|
@@ -1802,9 +1814,93 @@ var AITranslateText = ({
|
|
|
1802
1814
|
] });
|
|
1803
1815
|
};
|
|
1804
1816
|
|
|
1817
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1818
|
+
var import_react6 = __toESM(require("react"));
|
|
1819
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1820
|
+
var CustomizableAITranslateText = ({
|
|
1821
|
+
text,
|
|
1822
|
+
shouldTranslate,
|
|
1823
|
+
showHighlight = false,
|
|
1824
|
+
segmenter,
|
|
1825
|
+
translatedIconProps
|
|
1826
|
+
}) => {
|
|
1827
|
+
const context = (0, import_react6.useContext)(AITranslationContext);
|
|
1828
|
+
const ait = context?.ait;
|
|
1829
|
+
const segments = (0, import_react6.useMemo)(
|
|
1830
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1831
|
+
[segmenter, text, shouldTranslate]
|
|
1832
|
+
);
|
|
1833
|
+
const [displayTexts, setDisplayTexts] = (0, import_react6.useState)(
|
|
1834
|
+
() => segments.map((s) => s.text)
|
|
1835
|
+
);
|
|
1836
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1837
|
+
(0, import_react6.useEffect)(() => {
|
|
1838
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1839
|
+
}, [segmentsKey]);
|
|
1840
|
+
(0, import_react6.useEffect)(() => {
|
|
1841
|
+
if (!ait || !shouldTranslate) {
|
|
1842
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
let cancelled = false;
|
|
1846
|
+
Promise.all(
|
|
1847
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1848
|
+
).then((resolved) => {
|
|
1849
|
+
if (cancelled) return;
|
|
1850
|
+
setDisplayTexts(resolved);
|
|
1851
|
+
}).catch(() => {
|
|
1852
|
+
if (cancelled) return;
|
|
1853
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1854
|
+
});
|
|
1855
|
+
return () => {
|
|
1856
|
+
cancelled = true;
|
|
1857
|
+
};
|
|
1858
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1859
|
+
const eventHandlerRef = (0, import_react6.useRef)(null);
|
|
1860
|
+
(0, import_react6.useEffect)(() => {
|
|
1861
|
+
if (!context) return;
|
|
1862
|
+
if (!eventHandlerRef.current) {
|
|
1863
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1864
|
+
}
|
|
1865
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1866
|
+
async (sourceTexts) => {
|
|
1867
|
+
if (!shouldTranslate) return;
|
|
1868
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1869
|
+
const sourceSet = new Set(sourceTexts);
|
|
1870
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1871
|
+
if (targets.length === 0) return;
|
|
1872
|
+
const fresh = await Promise.all(
|
|
1873
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1874
|
+
);
|
|
1875
|
+
setDisplayTexts((prev) => {
|
|
1876
|
+
const next = [...prev];
|
|
1877
|
+
targets.forEach(({ i }, j) => {
|
|
1878
|
+
const value = fresh[j];
|
|
1879
|
+
if (value !== void 0) next[i] = value;
|
|
1880
|
+
});
|
|
1881
|
+
return next;
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
);
|
|
1885
|
+
return () => unsubscribe();
|
|
1886
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1887
|
+
const changedFlags = segments.map(
|
|
1888
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1889
|
+
);
|
|
1890
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1891
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1892
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1893
|
+
showCellIcon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TranslatedIcon, { ...translatedIconProps }),
|
|
1894
|
+
segments.map((seg, i) => {
|
|
1895
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1896
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react6.default.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1897
|
+
})
|
|
1898
|
+
] });
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1805
1901
|
// src/utils/featureFlag.ts
|
|
1806
1902
|
var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
|
|
1807
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1903
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1808
1904
|
var getAITranslationLDId = (domain) => {
|
|
1809
1905
|
const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
|
|
1810
1906
|
if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {
|
|
@@ -1827,6 +1923,7 @@ var getAITranslationLDId = (domain) => {
|
|
|
1827
1923
|
AITranslationProvider,
|
|
1828
1924
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1829
1925
|
BUTTON_TYPE,
|
|
1926
|
+
CustomizableAITranslateText,
|
|
1830
1927
|
buildAnalyticEvent,
|
|
1831
1928
|
buildEventKey,
|
|
1832
1929
|
buildObject,
|
package/dist/modern/index.mjs
CHANGED
|
@@ -1201,12 +1201,13 @@ var ChromeTranslator = class _ChromeTranslator {
|
|
|
1201
1201
|
};
|
|
1202
1202
|
} catch (error) {
|
|
1203
1203
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1204
|
+
const isPermanentFailure = error instanceof DOMException && error.name === "NotSupportedError";
|
|
1204
1205
|
return {
|
|
1205
1206
|
translation: text,
|
|
1206
1207
|
sourceLanguage: targetLanguage,
|
|
1207
1208
|
success: false,
|
|
1208
1209
|
errorMessage: message,
|
|
1209
|
-
retryable:
|
|
1210
|
+
retryable: !isPermanentFailure
|
|
1210
1211
|
};
|
|
1211
1212
|
}
|
|
1212
1213
|
}
|
|
@@ -1776,9 +1777,93 @@ var AITranslateText = ({
|
|
|
1776
1777
|
] });
|
|
1777
1778
|
};
|
|
1778
1779
|
|
|
1780
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1781
|
+
import React4, { useContext as useContext4, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1782
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1783
|
+
var CustomizableAITranslateText = ({
|
|
1784
|
+
text,
|
|
1785
|
+
shouldTranslate,
|
|
1786
|
+
showHighlight = false,
|
|
1787
|
+
segmenter,
|
|
1788
|
+
translatedIconProps
|
|
1789
|
+
}) => {
|
|
1790
|
+
const context = useContext4(AITranslationContext);
|
|
1791
|
+
const ait = context?.ait;
|
|
1792
|
+
const segments = useMemo(
|
|
1793
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1794
|
+
[segmenter, text, shouldTranslate]
|
|
1795
|
+
);
|
|
1796
|
+
const [displayTexts, setDisplayTexts] = useState3(
|
|
1797
|
+
() => segments.map((s) => s.text)
|
|
1798
|
+
);
|
|
1799
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1800
|
+
useEffect3(() => {
|
|
1801
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1802
|
+
}, [segmentsKey]);
|
|
1803
|
+
useEffect3(() => {
|
|
1804
|
+
if (!ait || !shouldTranslate) {
|
|
1805
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
let cancelled = false;
|
|
1809
|
+
Promise.all(
|
|
1810
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1811
|
+
).then((resolved) => {
|
|
1812
|
+
if (cancelled) return;
|
|
1813
|
+
setDisplayTexts(resolved);
|
|
1814
|
+
}).catch(() => {
|
|
1815
|
+
if (cancelled) return;
|
|
1816
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1817
|
+
});
|
|
1818
|
+
return () => {
|
|
1819
|
+
cancelled = true;
|
|
1820
|
+
};
|
|
1821
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1822
|
+
const eventHandlerRef = useRef3(null);
|
|
1823
|
+
useEffect3(() => {
|
|
1824
|
+
if (!context) return;
|
|
1825
|
+
if (!eventHandlerRef.current) {
|
|
1826
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1827
|
+
}
|
|
1828
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1829
|
+
async (sourceTexts) => {
|
|
1830
|
+
if (!shouldTranslate) return;
|
|
1831
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1832
|
+
const sourceSet = new Set(sourceTexts);
|
|
1833
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1834
|
+
if (targets.length === 0) return;
|
|
1835
|
+
const fresh = await Promise.all(
|
|
1836
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1837
|
+
);
|
|
1838
|
+
setDisplayTexts((prev) => {
|
|
1839
|
+
const next = [...prev];
|
|
1840
|
+
targets.forEach(({ i }, j) => {
|
|
1841
|
+
const value = fresh[j];
|
|
1842
|
+
if (value !== void 0) next[i] = value;
|
|
1843
|
+
});
|
|
1844
|
+
return next;
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
);
|
|
1848
|
+
return () => unsubscribe();
|
|
1849
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1850
|
+
const changedFlags = segments.map(
|
|
1851
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1852
|
+
);
|
|
1853
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1854
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1855
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1856
|
+
showCellIcon && /* @__PURE__ */ jsx4(TranslatedIcon, { ...translatedIconProps }),
|
|
1857
|
+
segments.map((seg, i) => {
|
|
1858
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1859
|
+
return /* @__PURE__ */ jsx4(React4.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1860
|
+
})
|
|
1861
|
+
] });
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1779
1864
|
// src/utils/featureFlag.ts
|
|
1780
1865
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1781
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1866
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "ai-translation";
|
|
1782
1867
|
var getAITranslationLDId = (domain) => {
|
|
1783
1868
|
const { environment, zone } = getProcoreZone(domain);
|
|
1784
1869
|
if (isFederalZone(zone)) {
|
|
@@ -1800,6 +1885,7 @@ export {
|
|
|
1800
1885
|
AITranslationProvider,
|
|
1801
1886
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1802
1887
|
BUTTON_TYPE,
|
|
1888
|
+
CustomizableAITranslateText,
|
|
1803
1889
|
buildAnalyticEvent,
|
|
1804
1890
|
buildEventKey,
|
|
1805
1891
|
buildObject,
|
package/package.json
CHANGED