@procore/ai-translations 0.6.2 → 0.8.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 +98 -0
- package/dist/legacy/index.d.mts +56 -2
- package/dist/legacy/index.d.ts +56 -2
- package/dist/legacy/index.js +97 -1
- package/dist/legacy/index.mjs +86 -1
- package/dist/modern/index.d.mts +56 -2
- package/dist/modern/index.d.ts +56 -2
- package/dist/modern/index.js +97 -1
- package/dist/modern/index.mjs +86 -1
- 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 = "pe_ai_translations_web_program_visible";
|
|
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 = "pe_ai_translations_web_program_visible";
|
|
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,
|
|
@@ -1825,9 +1836,93 @@ var AITranslateText = ({
|
|
|
1825
1836
|
] });
|
|
1826
1837
|
};
|
|
1827
1838
|
|
|
1839
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1840
|
+
var import_react6 = __toESM(require("react"));
|
|
1841
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1842
|
+
var CustomizableAITranslateText = ({
|
|
1843
|
+
text,
|
|
1844
|
+
shouldTranslate,
|
|
1845
|
+
showHighlight = false,
|
|
1846
|
+
segmenter,
|
|
1847
|
+
translatedIconProps
|
|
1848
|
+
}) => {
|
|
1849
|
+
const context = (0, import_react6.useContext)(AITranslationContext);
|
|
1850
|
+
const ait = context == null ? void 0 : context.ait;
|
|
1851
|
+
const segments = (0, import_react6.useMemo)(
|
|
1852
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1853
|
+
[segmenter, text, shouldTranslate]
|
|
1854
|
+
);
|
|
1855
|
+
const [displayTexts, setDisplayTexts] = (0, import_react6.useState)(
|
|
1856
|
+
() => segments.map((s) => s.text)
|
|
1857
|
+
);
|
|
1858
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1859
|
+
(0, import_react6.useEffect)(() => {
|
|
1860
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1861
|
+
}, [segmentsKey]);
|
|
1862
|
+
(0, import_react6.useEffect)(() => {
|
|
1863
|
+
if (!ait || !shouldTranslate) {
|
|
1864
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
let cancelled = false;
|
|
1868
|
+
Promise.all(
|
|
1869
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1870
|
+
).then((resolved) => {
|
|
1871
|
+
if (cancelled) return;
|
|
1872
|
+
setDisplayTexts(resolved);
|
|
1873
|
+
}).catch(() => {
|
|
1874
|
+
if (cancelled) return;
|
|
1875
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1876
|
+
});
|
|
1877
|
+
return () => {
|
|
1878
|
+
cancelled = true;
|
|
1879
|
+
};
|
|
1880
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1881
|
+
const eventHandlerRef = (0, import_react6.useRef)(null);
|
|
1882
|
+
(0, import_react6.useEffect)(() => {
|
|
1883
|
+
if (!context) return;
|
|
1884
|
+
if (!eventHandlerRef.current) {
|
|
1885
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1886
|
+
}
|
|
1887
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1888
|
+
async (sourceTexts) => {
|
|
1889
|
+
if (!shouldTranslate) return;
|
|
1890
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1891
|
+
const sourceSet = new Set(sourceTexts);
|
|
1892
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1893
|
+
if (targets.length === 0) return;
|
|
1894
|
+
const fresh = await Promise.all(
|
|
1895
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1896
|
+
);
|
|
1897
|
+
setDisplayTexts((prev) => {
|
|
1898
|
+
const next = [...prev];
|
|
1899
|
+
targets.forEach(({ i }, j) => {
|
|
1900
|
+
const value = fresh[j];
|
|
1901
|
+
if (value !== void 0) next[i] = value;
|
|
1902
|
+
});
|
|
1903
|
+
return next;
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
);
|
|
1907
|
+
return () => unsubscribe();
|
|
1908
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1909
|
+
const changedFlags = segments.map(
|
|
1910
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1911
|
+
);
|
|
1912
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1913
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1914
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1915
|
+
showCellIcon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TranslatedIcon, { ...translatedIconProps }),
|
|
1916
|
+
segments.map((seg, i) => {
|
|
1917
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1918
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react6.default.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1919
|
+
})
|
|
1920
|
+
] });
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1828
1923
|
// src/utils/featureFlag.ts
|
|
1829
1924
|
var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
|
|
1830
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1925
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "pe_ai_translations_web_program_visible";
|
|
1831
1926
|
var getAITranslationLDId = (domain) => {
|
|
1832
1927
|
const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
|
|
1833
1928
|
if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {
|
|
@@ -1850,6 +1945,7 @@ var getAITranslationLDId = (domain) => {
|
|
|
1850
1945
|
AITranslationProvider,
|
|
1851
1946
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1852
1947
|
BUTTON_TYPE,
|
|
1948
|
+
CustomizableAITranslateText,
|
|
1853
1949
|
buildAnalyticEvent,
|
|
1854
1950
|
buildEventKey,
|
|
1855
1951
|
buildObject,
|
package/dist/legacy/index.mjs
CHANGED
|
@@ -1801,9 +1801,93 @@ var AITranslateText = ({
|
|
|
1801
1801
|
] });
|
|
1802
1802
|
};
|
|
1803
1803
|
|
|
1804
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1805
|
+
import React4, { useContext as useContext4, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1806
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1807
|
+
var CustomizableAITranslateText = ({
|
|
1808
|
+
text,
|
|
1809
|
+
shouldTranslate,
|
|
1810
|
+
showHighlight = false,
|
|
1811
|
+
segmenter,
|
|
1812
|
+
translatedIconProps
|
|
1813
|
+
}) => {
|
|
1814
|
+
const context = useContext4(AITranslationContext);
|
|
1815
|
+
const ait = context == null ? void 0 : context.ait;
|
|
1816
|
+
const segments = useMemo(
|
|
1817
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1818
|
+
[segmenter, text, shouldTranslate]
|
|
1819
|
+
);
|
|
1820
|
+
const [displayTexts, setDisplayTexts] = useState3(
|
|
1821
|
+
() => segments.map((s) => s.text)
|
|
1822
|
+
);
|
|
1823
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1824
|
+
useEffect3(() => {
|
|
1825
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1826
|
+
}, [segmentsKey]);
|
|
1827
|
+
useEffect3(() => {
|
|
1828
|
+
if (!ait || !shouldTranslate) {
|
|
1829
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
let cancelled = false;
|
|
1833
|
+
Promise.all(
|
|
1834
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1835
|
+
).then((resolved) => {
|
|
1836
|
+
if (cancelled) return;
|
|
1837
|
+
setDisplayTexts(resolved);
|
|
1838
|
+
}).catch(() => {
|
|
1839
|
+
if (cancelled) return;
|
|
1840
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1841
|
+
});
|
|
1842
|
+
return () => {
|
|
1843
|
+
cancelled = true;
|
|
1844
|
+
};
|
|
1845
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1846
|
+
const eventHandlerRef = useRef3(null);
|
|
1847
|
+
useEffect3(() => {
|
|
1848
|
+
if (!context) return;
|
|
1849
|
+
if (!eventHandlerRef.current) {
|
|
1850
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1851
|
+
}
|
|
1852
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1853
|
+
async (sourceTexts) => {
|
|
1854
|
+
if (!shouldTranslate) return;
|
|
1855
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1856
|
+
const sourceSet = new Set(sourceTexts);
|
|
1857
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1858
|
+
if (targets.length === 0) return;
|
|
1859
|
+
const fresh = await Promise.all(
|
|
1860
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1861
|
+
);
|
|
1862
|
+
setDisplayTexts((prev) => {
|
|
1863
|
+
const next = [...prev];
|
|
1864
|
+
targets.forEach(({ i }, j) => {
|
|
1865
|
+
const value = fresh[j];
|
|
1866
|
+
if (value !== void 0) next[i] = value;
|
|
1867
|
+
});
|
|
1868
|
+
return next;
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
1872
|
+
return () => unsubscribe();
|
|
1873
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1874
|
+
const changedFlags = segments.map(
|
|
1875
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1876
|
+
);
|
|
1877
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1878
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1879
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1880
|
+
showCellIcon && /* @__PURE__ */ jsx4(TranslatedIcon, { ...translatedIconProps }),
|
|
1881
|
+
segments.map((seg, i) => {
|
|
1882
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1883
|
+
return /* @__PURE__ */ jsx4(React4.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1884
|
+
})
|
|
1885
|
+
] });
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1804
1888
|
// src/utils/featureFlag.ts
|
|
1805
1889
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1806
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1890
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "pe_ai_translations_web_program_visible";
|
|
1807
1891
|
var getAITranslationLDId = (domain) => {
|
|
1808
1892
|
const { environment, zone } = getProcoreZone(domain);
|
|
1809
1893
|
if (isFederalZone(zone)) {
|
|
@@ -1825,6 +1909,7 @@ export {
|
|
|
1825
1909
|
AITranslationProvider,
|
|
1826
1910
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1827
1911
|
BUTTON_TYPE,
|
|
1912
|
+
CustomizableAITranslateText,
|
|
1828
1913
|
buildAnalyticEvent,
|
|
1829
1914
|
buildEventKey,
|
|
1830
1915
|
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 = "pe_ai_translations_web_program_visible";
|
|
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 = "pe_ai_translations_web_program_visible";
|
|
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,
|
|
@@ -1802,9 +1813,93 @@ var AITranslateText = ({
|
|
|
1802
1813
|
] });
|
|
1803
1814
|
};
|
|
1804
1815
|
|
|
1816
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1817
|
+
var import_react6 = __toESM(require("react"));
|
|
1818
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1819
|
+
var CustomizableAITranslateText = ({
|
|
1820
|
+
text,
|
|
1821
|
+
shouldTranslate,
|
|
1822
|
+
showHighlight = false,
|
|
1823
|
+
segmenter,
|
|
1824
|
+
translatedIconProps
|
|
1825
|
+
}) => {
|
|
1826
|
+
const context = (0, import_react6.useContext)(AITranslationContext);
|
|
1827
|
+
const ait = context?.ait;
|
|
1828
|
+
const segments = (0, import_react6.useMemo)(
|
|
1829
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1830
|
+
[segmenter, text, shouldTranslate]
|
|
1831
|
+
);
|
|
1832
|
+
const [displayTexts, setDisplayTexts] = (0, import_react6.useState)(
|
|
1833
|
+
() => segments.map((s) => s.text)
|
|
1834
|
+
);
|
|
1835
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1836
|
+
(0, import_react6.useEffect)(() => {
|
|
1837
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1838
|
+
}, [segmentsKey]);
|
|
1839
|
+
(0, import_react6.useEffect)(() => {
|
|
1840
|
+
if (!ait || !shouldTranslate) {
|
|
1841
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
let cancelled = false;
|
|
1845
|
+
Promise.all(
|
|
1846
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1847
|
+
).then((resolved) => {
|
|
1848
|
+
if (cancelled) return;
|
|
1849
|
+
setDisplayTexts(resolved);
|
|
1850
|
+
}).catch(() => {
|
|
1851
|
+
if (cancelled) return;
|
|
1852
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1853
|
+
});
|
|
1854
|
+
return () => {
|
|
1855
|
+
cancelled = true;
|
|
1856
|
+
};
|
|
1857
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1858
|
+
const eventHandlerRef = (0, import_react6.useRef)(null);
|
|
1859
|
+
(0, import_react6.useEffect)(() => {
|
|
1860
|
+
if (!context) return;
|
|
1861
|
+
if (!eventHandlerRef.current) {
|
|
1862
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1863
|
+
}
|
|
1864
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1865
|
+
async (sourceTexts) => {
|
|
1866
|
+
if (!shouldTranslate) return;
|
|
1867
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1868
|
+
const sourceSet = new Set(sourceTexts);
|
|
1869
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1870
|
+
if (targets.length === 0) return;
|
|
1871
|
+
const fresh = await Promise.all(
|
|
1872
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1873
|
+
);
|
|
1874
|
+
setDisplayTexts((prev) => {
|
|
1875
|
+
const next = [...prev];
|
|
1876
|
+
targets.forEach(({ i }, j) => {
|
|
1877
|
+
const value = fresh[j];
|
|
1878
|
+
if (value !== void 0) next[i] = value;
|
|
1879
|
+
});
|
|
1880
|
+
return next;
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
return () => unsubscribe();
|
|
1885
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1886
|
+
const changedFlags = segments.map(
|
|
1887
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1888
|
+
);
|
|
1889
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1890
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1891
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1892
|
+
showCellIcon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TranslatedIcon, { ...translatedIconProps }),
|
|
1893
|
+
segments.map((seg, i) => {
|
|
1894
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1895
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react6.default.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1896
|
+
})
|
|
1897
|
+
] });
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1805
1900
|
// src/utils/featureFlag.ts
|
|
1806
1901
|
var import_web_sdk_mfe_utils = require("@procore/web-sdk-mfe-utils");
|
|
1807
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1902
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "pe_ai_translations_web_program_visible";
|
|
1808
1903
|
var getAITranslationLDId = (domain) => {
|
|
1809
1904
|
const { environment, zone } = (0, import_web_sdk_mfe_utils.getProcoreZone)(domain);
|
|
1810
1905
|
if ((0, import_web_sdk_mfe_utils.isFederalZone)(zone)) {
|
|
@@ -1827,6 +1922,7 @@ var getAITranslationLDId = (domain) => {
|
|
|
1827
1922
|
AITranslationProvider,
|
|
1828
1923
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1829
1924
|
BUTTON_TYPE,
|
|
1925
|
+
CustomizableAITranslateText,
|
|
1830
1926
|
buildAnalyticEvent,
|
|
1831
1927
|
buildEventKey,
|
|
1832
1928
|
buildObject,
|
package/dist/modern/index.mjs
CHANGED
|
@@ -1776,9 +1776,93 @@ var AITranslateText = ({
|
|
|
1776
1776
|
] });
|
|
1777
1777
|
};
|
|
1778
1778
|
|
|
1779
|
+
// src/components/CustomizableAITranslateText.tsx
|
|
1780
|
+
import React4, { useContext as useContext4, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1781
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1782
|
+
var CustomizableAITranslateText = ({
|
|
1783
|
+
text,
|
|
1784
|
+
shouldTranslate,
|
|
1785
|
+
showHighlight = false,
|
|
1786
|
+
segmenter,
|
|
1787
|
+
translatedIconProps
|
|
1788
|
+
}) => {
|
|
1789
|
+
const context = useContext4(AITranslationContext);
|
|
1790
|
+
const ait = context?.ait;
|
|
1791
|
+
const segments = useMemo(
|
|
1792
|
+
() => segmenter ? segmenter(text) : [{ text, translate: shouldTranslate }],
|
|
1793
|
+
[segmenter, text, shouldTranslate]
|
|
1794
|
+
);
|
|
1795
|
+
const [displayTexts, setDisplayTexts] = useState3(
|
|
1796
|
+
() => segments.map((s) => s.text)
|
|
1797
|
+
);
|
|
1798
|
+
const segmentsKey = JSON.stringify(segments);
|
|
1799
|
+
useEffect3(() => {
|
|
1800
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1801
|
+
}, [segmentsKey]);
|
|
1802
|
+
useEffect3(() => {
|
|
1803
|
+
if (!ait || !shouldTranslate) {
|
|
1804
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
let cancelled = false;
|
|
1808
|
+
Promise.all(
|
|
1809
|
+
segments.map((s) => s.translate ? ait(s.text) : Promise.resolve(s.text))
|
|
1810
|
+
).then((resolved) => {
|
|
1811
|
+
if (cancelled) return;
|
|
1812
|
+
setDisplayTexts(resolved);
|
|
1813
|
+
}).catch(() => {
|
|
1814
|
+
if (cancelled) return;
|
|
1815
|
+
setDisplayTexts(segments.map((s) => s.text));
|
|
1816
|
+
});
|
|
1817
|
+
return () => {
|
|
1818
|
+
cancelled = true;
|
|
1819
|
+
};
|
|
1820
|
+
}, [ait, shouldTranslate, segmentsKey]);
|
|
1821
|
+
const eventHandlerRef = useRef3(null);
|
|
1822
|
+
useEffect3(() => {
|
|
1823
|
+
if (!context) return;
|
|
1824
|
+
if (!eventHandlerRef.current) {
|
|
1825
|
+
eventHandlerRef.current = new EventHandler(context.tool);
|
|
1826
|
+
}
|
|
1827
|
+
const unsubscribe = eventHandlerRef.current.subscribeToRerenderEvent(
|
|
1828
|
+
async (sourceTexts) => {
|
|
1829
|
+
if (!shouldTranslate) return;
|
|
1830
|
+
if (!sourceTexts || sourceTexts.length === 0) return;
|
|
1831
|
+
const sourceSet = new Set(sourceTexts);
|
|
1832
|
+
const targets = segments.map((s, i) => ({ s, i })).filter(({ s }) => s.translate && sourceSet.has(s.text));
|
|
1833
|
+
if (targets.length === 0) return;
|
|
1834
|
+
const fresh = await Promise.all(
|
|
1835
|
+
targets.map(({ s }) => context.ait(s.text))
|
|
1836
|
+
);
|
|
1837
|
+
setDisplayTexts((prev) => {
|
|
1838
|
+
const next = [...prev];
|
|
1839
|
+
targets.forEach(({ i }, j) => {
|
|
1840
|
+
const value = fresh[j];
|
|
1841
|
+
if (value !== void 0) next[i] = value;
|
|
1842
|
+
});
|
|
1843
|
+
return next;
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
);
|
|
1847
|
+
return () => unsubscribe();
|
|
1848
|
+
}, [context, shouldTranslate, segmentsKey]);
|
|
1849
|
+
const changedFlags = segments.map(
|
|
1850
|
+
(s, i) => s.translate && shouldTranslate && (displayTexts[i] ?? s.text) !== s.text
|
|
1851
|
+
);
|
|
1852
|
+
const anyChanged = changedFlags.some(Boolean);
|
|
1853
|
+
const showCellIcon = showHighlight && anyChanged;
|
|
1854
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1855
|
+
showCellIcon && /* @__PURE__ */ jsx4(TranslatedIcon, { ...translatedIconProps }),
|
|
1856
|
+
segments.map((seg, i) => {
|
|
1857
|
+
const display = displayTexts[i] ?? seg.text;
|
|
1858
|
+
return /* @__PURE__ */ jsx4(React4.Fragment, { children: display }, `${i}-${seg.text}`);
|
|
1859
|
+
})
|
|
1860
|
+
] });
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1779
1863
|
// src/utils/featureFlag.ts
|
|
1780
1864
|
import { isFederalZone, getProcoreZone } from "@procore/web-sdk-mfe-utils";
|
|
1781
|
-
var AI_TRANSLATION_FEATURE_FLAG_KEY = "
|
|
1865
|
+
var AI_TRANSLATION_FEATURE_FLAG_KEY = "pe_ai_translations_web_program_visible";
|
|
1782
1866
|
var getAITranslationLDId = (domain) => {
|
|
1783
1867
|
const { environment, zone } = getProcoreZone(domain);
|
|
1784
1868
|
if (isFederalZone(zone)) {
|
|
@@ -1800,6 +1884,7 @@ export {
|
|
|
1800
1884
|
AITranslationProvider,
|
|
1801
1885
|
AI_TRANSLATION_FEATURE_FLAG_KEY,
|
|
1802
1886
|
BUTTON_TYPE,
|
|
1887
|
+
CustomizableAITranslateText,
|
|
1803
1888
|
buildAnalyticEvent,
|
|
1804
1889
|
buildEventKey,
|
|
1805
1890
|
buildObject,
|
package/package.json
CHANGED