@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 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:
@@ -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 = "ai-translation";
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 };
@@ -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 = "ai-translation";
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 };
@@ -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 = "ai-translation";
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,
@@ -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 = "ai-translation";
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,
@@ -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 = "ai-translation";
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 };
@@ -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 = "ai-translation";
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 };
@@ -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 = "ai-translation";
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,
@@ -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 = "ai-translation";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@procore/ai-translations",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Library that provides a solution to use AI to translate text into a language",
5
5
  "main": "dist/legacy/index.js",
6
6
  "types": "dist/legacy/index.d.ts",