@procore/ai-translations
v0.9.0
Published
Library that provides a solution to use AI to translate text into a language
Maintainers
Keywords
Readme
@procore/ai-translations
React package for AI-powered UI string translation in frontend apps. It provides:
AITranslationProviderto initialize translation contextAITranslateTextto render translated textuseAITranslationfor direct access to translation, consent, and progress state
Need deeper implementation details? See the internal guide: README.internal.md.
This package supports two runtime strategies:
frontend_translations: browser-based translation (Chrome-family AI APIs). Translation runs sequentially and is gated on user consent when an on-device model must be downloaded.backend_translations: server-side translation API. No on-device model downloads; model-consent popups are suppressed automatically.
For Procore-specific operational details, see README.internal.md.
Installation
yarn add @procore/ai-translationsPeer dependencies:
react>= 16 < 19react-dom>= 16 < 19@procore/web-sdk-mfe-utils> 1.0.0 < 2
Quick Start
import React from 'react';
import {
AITranslationProvider,
AITranslateText,
} from '@procore/ai-translations';
export function AppRoot() {
return (
<AITranslationProvider
tool="timecard"
companyId={1}
projectId={10}
userId={42}
locale="es"
enableAIT={true}
companyLocale="de-DE"
projectLocale="fr-CA"
>
<AITranslateText text="Hello world" shouldTranslate={true} />
</AITranslationProvider>
);
}Public API
AITranslationProvider
Required props:
locale: string- target locale (BCP-47, for examplees,fr,de)tool: string- tool/MFE identifier used to scope translation eventscompanyId: numberprojectId: numberuserId: number
Optional props:
enableAIT?: boolean(defaulttrue) - whenfalse, returns source text without translationcompanyLocale?: string- company-level locale from environment metadata (e.g."de-DE"); passed through to context for downstream consumers such as AmplitudeprojectLocale?: string- project-level locale from environment metadata (e.g."fr-CA"); passed through to context for downstream consumers such as Amplitude
AITranslateText
Props:
text: stringshouldTranslate: booleanshowHighlight?: boolean(defaultfalse)translatedIconProps?: TranslatedIconProps
CustomizableAITranslateText
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.
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.
Props:
text: string— the full text value, passed tosegmentershouldTranslate: boolean— master switch; whenfalseall segments render as plain textshowHighlight?: boolean(defaultfalse) — whether to show theTranslatedIconhighlightMode?: 'segment' | 'cell'(default'segment') — see belowsegmenter?: (text: string) => TranslatableSegment[]— split rule; when omitted the whole text is one translatable segmenttranslatedIconProps?: TranslatedIconProps
Segmenter examples
"{code} - {label}" — keep the code prefix, translate only the label:
import { CustomizableAITranslateText } from '@procore/ai-translations';
<CustomizableAITranslateText
text="INS-001 - Safety Inspection"
shouldTranslate={true}
segmenter={(text) => {
const idx = text.indexOf(' - ');
if (idx === -1) return [{ text, translate: true }];
return [
{ text: text.slice(0, idx + 3), translate: false }, // "INS-001 - "
{ text: text.slice(idx + 3), translate: true }, // "Safety Inspection"
];
}}
/>;"Key: Value" — keep the field name, translate only the value:
<CustomizableAITranslateText
text="Status: Awaiting Review"
shouldTranslate={true}
segmenter={(text) => {
const idx = text.indexOf(': ');
if (idx === -1) return [{ text, translate: true }];
return [
{ text: text.slice(0, idx + 2), translate: false }, // "Status: "
{ text: text.slice(idx + 2), translate: true }, // "Awaiting Review"
];
}}
/>"A | B" — translate both halves, keep the divider:
<CustomizableAITranslateText
text="Safety | Critical"
shouldTranslate={true}
segmenter={(text) => {
const idx = text.indexOf(' | ');
if (idx === -1) return [{ text, translate: true }];
return [
{ text: text.slice(0, idx), translate: true }, // "Safety"
{ text: ' | ', translate: false }, // " | "
{ text: text.slice(idx + 3), translate: true }, // "Critical"
];
}}
/>"A > B > C" breadcrumb — translate every crumb independently:
<CustomizableAITranslateText
text="Projects > Building A > Floor 3"
shouldTranslate={true}
segmenter={(text) => {
const parts = text.split(' > ');
return parts.flatMap((part, i) => [
{ text: part, translate: true },
...(i < parts.length - 1 ? [{ text: ' > ', translate: false }] : []),
]);
}}
/>highlightMode
Controls where TranslatedIcon appears when a translation actually changes the text:
| Mode | Behavior |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 'segment' (default) | One icon next to each segment whose translated output differs from its source. Precisely marks which substrings are AI-generated. |
| '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. |
showHighlight={false} suppresses all icons regardless of mode.
useAITranslation()
Returns:
ait(text: string): Promise<string>- translates text or returns cached valuelocale: stringcompanyLocale: string | undefined- company-level locale from environment metadataprojectLocale: string | undefined- project-level locale from environment metadatatool: stringtranslationProgress: { progress: number; current: number; total: number } | nullmodelDownloadProgress: ModelDownloadProgress | nullmodelConsent: ModelConsentRequest | null- non-null while the translation run is paused awaiting user consent to download a source→target model;nullotherwiseconfirmModelDownload: () => void- call this (e.g. from a Download button'sonClick) to provide the required user gesture, start the download, and resume translation; no-op whenmodelConsentisnullor when the active strategy isbackend_translationsrenderVersion: number | undefined
normalizeLanguage(language: string): string
Utility that strips the region/script subtag from a BCP-47 language tag (e.g. "en-US" → "en"). Falls back to the original string when no subtag is present.
import { normalizeLanguage } from '@procore/ai-translations';
normalizeLanguage('en-US'); // 'en'
normalizeLanguage('fr'); // 'fr'Feature flag helpers
AI_TRANSLATION_FEATURE_FLAG_KEYgetAITranslationLDId(domain: string)
How It Works
flowchart LR
consumerApp[ConsumerApp] --> provider[AITranslationProvider]
provider --> aitCall["ait(text)"]
aitCall --> registryCheck[TranslationRegistryCheck]
registryCheck --> returnCached[ReturnCachedText]
registryCheck --> queueText[QueueText]
queueText --> manager[TranslationManager]
manager --> strategy[TranslatorStrategy]
strategy --> frontendClient[ChromeFrontendClient]
strategy --> backendClient[BackendApiClient]
frontendClient --> modelGate{ModelDownloadGate}
modelGate -- model ready --> translate[Translate]
modelGate -- model missing --> consentEvent[MODEL_CONSENT_REQUIRED event]
consentEvent --> popup[ModelDownloadConsentPopup]
popup -- user clicks Download --> confirmModelDownload[confirmModelDownload]
confirmModelDownload --> downloadModel[downloadModel]
downloadModel --> markReady[gate.markReady]
markReady --> translate
translate --> persist[PersistToIndexedDB]
backendClient --> persist
persist --> publish[PublishTranslationEvents]
publish --> rerender[AITranslateTextRerender]
rerender --> translatedUi[DisplayTranslatedText]At runtime, strings are registered through ait(), processed sequentially (frontend strategy) or in configurable batches (backend strategy), cached in memory and IndexedDB, and then rerendered via package events.
Frontend model-download consent flow
When the frontend_translations strategy encounters a source→target model that has not yet been downloaded, Chrome requires a user gesture to initiate the download. The package handles this automatically:
- The translation run pauses and fires a
MODEL_CONSENT_REQUIREDevent carrying{ sourceLanguage, targetLanguage }. useAITranslation().modelConsentbecomes non-null — render<ModelDownloadConsentPopup />(from@procore/ai-translation-utils) to surface the consent UI to the user.- The user clicks the Download button, calling
confirmModelDownload()which provides the required gesture, starts the download, and shows<ModelDownloadProgressPopup />. - When the download completes,
modelDownloadGate.markReady(source, target)is called and the paused translation run resumes automatically. - Steps 1–4 repeat for each unique source→target model pair that is not yet cached. Already-downloaded models skip the consent step entirely.
- After all texts are translated the run terminates. Translated text is displayed immediately as each translation completes, not only at the end.
Important: your app must render
<ModelDownloadConsentPopup />insideAITranslationProviderfor the consent flow to work. Without it,ensureModelReadywill never resolve and the translation run will hang indefinitely.
Configuration
The provider uses useConfig (React Query) to fetch translation config and refetch every 30 minutes.
Expected remote config shape:
type ToolConfig = {
strategy: 'frontend_translations' | 'backend_translations';
supportedLocales: string[];
translationBatchSize: number;
renderingBatchSize: number;
apiTimeout: number;
apiVersion: string;
};Operational Notes
AITranslateTextanduseAITranslationmust be used insideAITranslationProvider.- Frontend strategy depends on Chrome-family AI support; behavior differs by browser support.
- Backend/config endpoints are resolved from
window.location.originunder/rest/. - Translation cache uses IndexedDB; some restricted browser environments may limit persistence.
- For error handling behavior and failure modes, see
docs/error-handling.md.
Development
From packages/ai-translations:
yarn build
yarn test
yarn lint
yarn storybook
yarn cypress:run