npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@procore/ai-translation-utils

v0.4.0

Published

Utility functions for AI translation services

Maintainers

dancingshelldancingshelljustinmwattsjustinmwattsantonyayoubantonyayoubrysmithprocorerysmithprocorerobbiegprocorerobbiegprocorejadamsssjadamsssjeremy.bouzigardjeremy.bouzigardjgentesjgentesfaraz.haniffaraz.haniftimdohertytimdohertyajaykumar-procoreajaykumar-procoreb.bookoutb.bookoutjalyngjalyngchadryderchadryderhtaelhtaelrefaiepcnrefaiepcnjames.lawsonjames.lawsonvinayakaprabhuvinayakaprabhudavidshuredavidshurejames.clearyjames.clearyjl4everjl4everandersonbispoprocoreandersonbispoprocoredev-account-admindev-account-adminbrockpcorbrockpcorrowan.ibrahimrowan.ibrahimsseanwangsseanwangramysaid2ramysaid2vinaya-procorevinaya-procorelalovar-procorelalovar-procorebhargavrndbhargavrndihor.diachenko_procoreihor.diachenko_procorefarismmkfarismmkgideon-procoregideon-procoredannyporrellodannyporrelloalanprocorealanprocorechance.eakin.procorechance.eakin.procorestevenliprocorestevenliprocorejavio-procorejavio-procorekani-procorekani-procoreenyagaenyagadanny.oudanny.oumessanjahmessanjahdavid-christensen-procoredavid-christensen-procoreshradha.khardshradha.khardwinson.chuwinson.chueyvettesoueyvettesoulzhou888lzhou888jnhoang1jnhoang1nickprocorenickprocoreneil.mckeemanneil.mckeemanpam-whisenhuntpam-whisenhuntjgee67jgee67youssefameryoussefamermike-arndt-procoremike-arndt-procorebob.laskowskibob.laskowskicagmzcagmzmariah_delaneymariah_delaneylukenispellukenispelfabriciobdfabriciobdbikash.sahoobikash.sahoobbreyel921bbreyel921kimhin267kimhin267andy.mayerandy.mayerphil.custerphil.custerelijah.procoreelijah.procorejuliana.hernandezjuliana.hernandezjudy-lu-pcjudy-lu-pcprocore-it-supportprocore-it-supportandrewburke-pcandrewburke-pcjkleintechjkleintechrachel.arkebauerrachel.arkebauerprocore-npm-botprocore-npm-botgrafffffffgrafffffffyoyis3000yoyis3000james.dabbs-procorejames.dabbs-procorelaurenbrandsteinprocorelaurenbrandsteinprocorescottbieser-procorescottbieser-procoreamir-iskanderamir-iskanderzach.mckenzie.procorezach.mckenzie.procoreamyprocoreamyprocoreshayonj_procoreshayonj_procoreheplayskeysheplayskeysmike.southmike.souththomasoboylethomasoboyledischordedischordederek-carter-procorederek-carter-procoredlgasserdlgassercfprocorecfprocoreevan.waitsevan.waitsjeremy-marcusjeremy-marcusjmejia-fsljmejia-fslersgonzaloersgonzalotimofeeetimofeeestephan-procorestephan-procorealeclarsenprocorealeclarsenprocoresarah.freitassarah.freitasyihai.zweifelyihai.zweifeljay-rajanjay-rajanjacky-leijacky-leiapcarroll_procoreapcarroll_procoreprocore_halzyprocore_halzymehrdad-panahandehmehrdad-panahandehpeter.jinpeter.jinuddhavjoglekaruddhavjoglekarbrookyboy009brookyboy009denzylbalramdenzylbalramchangprocorechangprocoreallenanle.procoreallenanle.procoredevin.cunningham.procoredevin.cunningham.procoreari-procoreari-procorenoor.alinoor.alihgouhierprocorehgouhierprocorecyrille.baicyrille.baibrad.uranibrad.uranidmccraw-procoredmccraw-procorepatrick.lardinpatrick.lardinabhijit.patwardhanabhijit.patwardhanmatt.harris0223matt.harris0223alan.bresanialan.bresanijesse.olsenjesse.olsendtorres-procoredtorres-procoredineshkumar.jayakdineshkumar.jayakjason-kayejason-kayeyadhu.prakashyadhu.prakashleandro-procleandro-procandrew.wheelerandrew.wheelersherylnapigkitsherylnapigkitlydiaharalydiaharakahliholmeskahliholmessateesh-kadiyala-procoresateesh-kadiyala-procoreepalinprocoreepalinprocoredennis.heckmandennis.heckmanjamie-dugan-procorejamie-dugan-procoreviktoriia_azarovskaviktoriia_azarovskadaniel.ferreira-contractordaniel.ferreira-contractorwillpankonienwillpankonienladavargaladavargasteven.hinklesteven.hinkletxin1txin1chris.berberchris.berberetokarevetokarevritchleeritchleekarina.mendez-contractorkarina.mendez-contractorworldofsatyakiworldofsatyakigreg.sparksgreg.sparkskyle.williamskyle.williamskuldeepsingh4556kuldeepsingh4556jeremy.lundjeremy.lundbrocktillotsonprocorebrocktillotsonprocorestajicsstajicsryanfuentesprocoreryanfuentesprocoretyler.wasden.procoretyler.wasden.procorefabiomelo513fabiomelo513cody_schindler_procorecody_schindler_procoreamit.gurav-contractoramit.gurav-contractoryoasyo25yoasyo25kalyani.gosavikalyani.gosavihectorthielehectorthieleandersontr15andersontr15vishal-procorevishal-procoreomar.wagdyomar.wagdyyogevfine1yogevfine1charan_procorecharan_procorescorgiat-procorescorgiat-procorembartlett413mbartlett413attachiattachiahmed.ghorabahmed.ghorabvaromirvaromiralyelashram_procorealyelashram_procoreilya.dryha-contractorilya.dryha-contractorevan.cerwonka.procoreevan.cerwonka.procorevsobol-cvsobol-cdmitri_wmdmitri_wmkellikearnskellikearnsrichard.bunnrichard.bunnchaitra-m-15chaitra-m-15conner-procoreconner-procoremishaelowoyemimishaelowoyemipeterknifpeterknifaleh.haurylenia-contractoraleh.haurylenia-contractormiguel.garcia-procoremiguel.garcia-procorecodyrobertsprocorecodyrobertsprocorea.elbadaweia.elbadaweilnspatz914lnspatz914melch-procoremelch-procoremustafa-abdelrahmanmustafa-abdelrahmanatoaimaatoaimajasaswinijasaswiniadarsh.gautamadarsh.gautamamin.jaipuriamin.jaipurimax.helmetagmax.helmetags_kudryks_kudrykhyogmanhyogmankyle.liukyle.liudavidkangprodavidkangprostevenkang3stevenkang3cbathgatecbathgatevictorbendeck-pcvictorbendeck-pcsarah.herediasarah.herediamoaz-ashrafmoaz-ashrafaly-el-kerdanyaly-el-kerdanyprocore-oss-userprocore-oss-userabhishekkumar123abhishekkumar123stephanie.breretonstephanie.breretonsaurasumprocoresaurasumprocoremona.khairbekmona.khairbekelewando-procoreelewando-procorejyang-procorejyang-procoretedyangtedyangdeiabdeiabjgreene_procorejgreene_procoreasamayasamaykenny.foisykenny.foisyganesh.raghupathyganesh.raghupathyrajatmenhdirattarajatmenhdirattayzhou2024yzhou2024dlameter-procoredlameter-procoredecha-sansondecha-sansonkylepietzkylepietzconnie-feng-procoreconnie-feng-procoreroger-procoreroger-procorematheusprocorematheusprocorefernandocamilottifernandocamilottisimona.iancusimona.iancujacksonleach-procorejacksonleach-procoreg2mitchellg2mitchelltatsiana.cliftontatsiana.cliftonphunguyen-pcorphunguyen-pcorpmfrawleypmfrawleybrian.smith1brian.smith1scottsternscottsternneil1023neil1023srichaitanya.peddintisrichaitanya.peddintijake-pitkinjake-pitkinerikthoresonerikthoresonlhuang325lhuang325abhijit-procoreabhijit-procorerodayna.ehabrodayna.ehabfairchildfairchildmustafa-u-abdelrahmanmustafa-u-abdelrahmanaberkowitzaberkowitzpwhisenhunt-procorepwhisenhunt-procoremariia.solodovnikmariia.solodovniknigeld-procorenigeld-procoresamad.viranisamad.viranibohdan-horai-procorebohdan-horai-procoremathenes_procoremathenes_procorevinoth.kuppusamyvinoth.kuppusamyzayterzayteralan.facchini-contractoralan.facchini-contractorcassianomatos-procorecassianomatos-procoreamitk030amitk030sflang-procoresflang-procoretracy.ottotracy.ottodaniel-pierre-procoredaniel-pierre-procoreglidenorglidenorashish.sharma2024ashish.sharma2024gaurav.sharma.procoregaurav.sharma.procoreandres-mendez-procoreandres-mendez-procoreroobo-romeskiroobo-romeskikylemartinez-procorekylemartinez-procoresean.spearman.procoresean.spearman.procoregturkadzegturkadzejeffgiaquintojeffgiaquintoezrasimeloffezrasimeloffbill-wagnerbill-wagnerkellen.stewartkellen.stewartrodrigo.dejuanarodrigo.dejuanasaranahal2saranahal2andrew.isaacandrew.isaacagamaleldinagamaleldinmostafaeltazymostafaeltazymagdyyxxmagdyyxxandreszorrilla-procoreandreszorrilla-procoremohitsharma97mohitsharma97tejeshwartejeshwarswati.jadhavswati.jadhavsquidbeakssquidbeakssmishra06smishra06subham.panigrahisubham.panigrahideepak.kumartsdeepak.kumartsvaibhav6521vaibhav6521bagnaram-procorebagnaram-procorehatimkhandwawala-procorehatimkhandwawala-procoremahesh-s96mahesh-s96oleksandr133oleksandr133mohamed.adelmohamed.adelzveli-procorezveli-procorenubs-procorenubs-procorerana.eltayarrana.eltayarmahmoud-sharsharmahmoud-sharsharsyamphanindrasyamphanindraveroniaosamaveroniaosamaimanselimimanselimhelmy162-procorehelmy162-procorepclemonspclemonssamuelvelez8383samuelvelez8383vinitdeshkar-procorevinitdeshkar-procoremariam_mazenmariam_mazenmina-elnagarmina-elnagardaniel_andrewsdaniel_andrewsmohanad-aymanmohanad-aymanarsenii.derkach-procorearsenii.derkach-procorestepanvanzuriakprocorestepanvanzuriakprocorend-procorend-procoremarwansalem-prcmarwansalem-prcyoussefothmanyoussefothmanandrii.datsenko-contractorandrii.datsenko-contractor

Keywords

Readme

@procore/ai-translation-utils

AG Grid data-table integration utilities for [@procore/ai-translations](https://github.com/procore/platform-internationalization-js-monorepo). Provides type-safe column-level translation toggling, cell rendering, progress popups, and menu option factories -- all wired through AG Grid's GridApi and React refs.


Table of Contents


Quick Start

Installation

yarn add @procore/ai-translation-utils

Hello World -- A Fully Typed AG Grid with AI Translation

import React, { useRef } from 'react';
import type { GridApi, ColDef } from '@ag-grid-community/core';
import { AgGridReact } from '@ag-grid-community/react';
import { AITranslationProvider } from '@procore/ai-translations';
import {
  handleGridReady,
  createAIMenuOptions,
  TranslatableCell,
  TranslationProgressPopup,
  ModelDownloadProgressPopup,
  type AIMenuOption,
} from '@procore/ai-translation-utils';

export function TranslatableGrid() {
  const gridApiRef = useRef<GridApi | null>(null);

  const aiMenuOptions: AIMenuOption[] = createAIMenuOptions(gridApiRef, {
    translate: 'Translate Column',
    highlight: 'Highlight Translations',
  });

  const columnDefs: ColDef[] = [
    {
      field: 'description',
      cellRenderer: TranslatableCell,
      headerComponentParams: { menuOptions: aiMenuOptions },
    },
  ];

  return (
    <AITranslationProvider>
      <AgGridReact
        columnDefs={columnDefs}
        rowData={[{ description: 'Hello, world!' }]}
        onGridReady={(params) => handleGridReady(params, gridApiRef)}
      />
      <TranslationProgressPopup />
      <ModelDownloadProgressPopup />
    </AITranslationProvider>
  );
}

Every export above is explicitly typed. gridApiRef is MutableRefObject<GridApi | null>, aiMenuOptions is AIMenuOption[], and columnDefs uses AG Grid's ColDef -- the compiler enforces correctness at every integration point.


Core Architecture

The package is organized into a single data-table module that exposes three layers: state management, UI components, and a menu factory.

Module Structure

src/
├── index.ts                  # Barrel: re-exports everything from data-table/
└── data-table/
    ├── index.ts                         # Public API barrel
    ├── columnFeatureState.ts            # WeakMap-backed per-grid, per-column state
    ├── aiMenuOptions.ts                 # Menu option factory (depends on columnFeatureState)
    ├── TranslatableCell.tsx             # AG Grid cell renderer — translates the whole cell value
    ├── CustomizableTranslatableCell.tsx # AG Grid cell renderer — translates chosen substrings via a segmenter
    ├── TranslationProgressPopup.tsx
    ├── ModelDownloadProgressPopup.tsx
    └── popupShared.tsx                  # Internal: shared styles, ProgressBar, ErrorBoundary

Class & Interface Diagram

classDiagram
    direction LR

    class AIMenuOption {
        <<interface>>
        +label: string
        +value: string
        +action(colDef: ColDef | null) void
    }

    class columnFeatureState {
        <<module>>
        -translationStateMap: WeakMap~GridApi, Record~string, boolean~~
        -highlightStateMap: WeakMap~GridApi, Record~string, boolean~~
        -highlightModeMap: WeakMap~GridApi, Record~string, HighlightMode~~
        +isColumnTranslationEnabled(gridApi, field) boolean
        +toggleColumnTranslation(gridApi, field) void
        +isColumnHighlightEnabled(gridApi, field) boolean
        +toggleColumnHighlight(gridApi, field) void
        +getColumnHighlightMode(gridApi, field) HighlightMode
        +setColumnHighlightMode(gridApi, field, mode) void
        +clearColumnFeatureStates(gridApi) void
        +handleGridReady(params, gridApiRef) void
    }

    class createAIMenuOptions {
        <<function>>
        +createAIMenuOptions(gridApiRef, labels) AIMenuOption[]
    }

    class TranslatableCell {
        <<component>>
        +TranslatableCell(params: ICellRendererParams) JSX.Element
        +extractText(value: unknown) string
    }

    class CustomizableTranslatableCell {
        <<component>>
        +CustomizableTranslatableCell(props: ICellRendererParams and CustomizableTranslatableCellParams) JSX.Element
    }

    class TranslationProgressPopup {
        <<component>>
        +label? string
        +TranslationProgressPopup(props) JSX.Element
    }

    class ModelDownloadProgressPopup {
        <<component>>
        +label? string
        +ModelDownloadProgressPopup(props) JSX.Element
    }

    class ProgressErrorBoundary {
        <<internal>>
        +state: hasError boolean
        +getDerivedStateFromError() state
        +render() ReactNode | null
    }

    createAIMenuOptions --> columnFeatureState : calls toggle functions
    createAIMenuOptions --> AIMenuOption : returns
    TranslatableCell --> columnFeatureState : reads state
    TranslatableCell --> AITranslateText : delegates rendering
    CustomizableTranslatableCell --> columnFeatureState : reads state
    CustomizableTranslatableCell --> CustomizableAITranslateText : delegates rendering
    TranslationProgressPopup --> ProgressErrorBoundary : wrapped by
    ModelDownloadProgressPopup --> ProgressErrorBoundary : wrapped by
    TranslationProgressPopup --> useAITranslation : reads translationProgress
    ModelDownloadProgressPopup --> useAITranslation : reads modelDownloadProgress

API Reference

Exported Types & Interfaces

| Type / Interface | Source | Properties / Signature | Description | | ------------------------------------ | ---------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AIMenuOption | aiMenuOptions.ts | label: string, value: string, action: (colDef: ColDef \| null) => void | Describes a single menu entry for AG Grid column headers. action receives the column definition so toggle logic can identify which column to operate on. | | TranslationProgressPopupProps | TranslationProgressPopup.tsx | label?: string | Optional props for TranslationProgressPopup. label overrides the default "Translating..." text. | | ModelDownloadProgressPopupProps | ModelDownloadProgressPopup.tsx | label?: string | Optional props for ModelDownloadProgressPopup. label overrides the default "Downloading AI Model..." text. | | TextSegmenter | @procore/ai-translations | (text: string) => TranslatableSegment[] | Pure function that splits an extracted cell string into ordered translatable / non-translatable segments. Import directly from @procore/ai-translations and pass via cellRendererParams.segmenter. | | TranslatableSegment | @procore/ai-translations | text: string, translate: boolean | A single segment returned by a TextSegmenter. When translate is true the segment goes through the AI translation pipeline; otherwise it is rendered as plain text. Import from @procore/ai-translations. | | HighlightMode | @procore/ai-translations | 'segment' \| 'cell' | Where the translation highlight icon appears. Import from @procore/ai-translations. | | CustomizableTranslatableCellParams | CustomizableTranslatableCell.tsx | segmenter?: TextSegmenter, highlightMode?: HighlightMode | cellRendererParams shape for CustomizableTranslatableCell. highlightMode is a static initial value; for dynamic runtime changes use setColumnHighlightMode. |

Dependency types used in public signatures (from peer packages, not re-exported):

| Type | Source Package | Usage | | --------------------- | ------------------------- | --------------------------------------------------- | | GridApi | @ag-grid-community/core | Grid instance passed to all column state functions | | GridReadyEvent | @ag-grid-community/core | Event parameter for handleGridReady | | ColDef | @ag-grid-community/core | Column definition received by AIMenuOption.action | | ICellRendererParams | @ag-grid-community/core | Props received by TranslatableCell | | MutableRefObject<T> | react | React ref holding GridApi \| null |


Column Feature State Functions

All state is stored in module-scoped WeakMap instances keyed by GridApi. State is automatically garbage-collected when a GridApi is disposed and is isolated per grid instance.

| Function | Signature | Returns | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | isColumnTranslationEnabled | (gridApi: GridApi \| null \| undefined, field: string \| undefined) => boolean | boolean | Returns true if translation is active for field. Safely returns false for nullish inputs. | | toggleColumnTranslation | (gridApi: GridApi \| null \| undefined, field: string \| undefined) => void | void | Flips the translation flag for field and calls gridApi.refreshCells(). No-op if gridApi, field, or the column definition is missing. | | isColumnHighlightEnabled | (gridApi: GridApi \| null \| undefined, field: string \| undefined) => boolean | boolean | Returns true if highlighting is active for field. | | toggleColumnHighlight | (gridApi: GridApi \| null \| undefined, field: string \| undefined) => void | void | Flips the highlight flag for field and refreshes cells. | | getColumnHighlightMode | (gridApi: GridApi \| null \| undefined, field: string \| undefined) => HighlightMode | HighlightMode | Returns the current highlight mode for field. Defaults to 'segment' when not explicitly set. | | setColumnHighlightMode | (gridApi: GridApi \| null \| undefined, field: string \| undefined, mode: HighlightMode) => void | void | Sets the highlight mode for field and refreshes cells. Use this instead of cellRendererParams to avoid grid re-initialization on mode changes. | | clearColumnFeatureStates | (gridApi: GridApi \| null \| undefined) => void | void | Resets all state maps (translation, highlight, highlight mode) for the grid. Refreshes only columns that had active states. | | handleGridReady | (params: GridReadyEvent, gridApiRef: MutableRefObject<GridApi \| null>) => void | void | Stores params.api in the ref and registers a paginationChanged listener that auto-clears feature states. |


Menu Options Factory

| Function | Signature | Returns | | --------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------- | | createAIMenuOptions | (gridApiRef: MutableRefObject<GridApi \| null>, labels: { translate: string; highlight: string }) => AIMenuOption[] | AIMenuOption[] |

Returns two AIMenuOption entries (translate and highlight). Each option's action closure captures the gridApiRef and calls the corresponding toggle function from columnFeatureState. The labels parameter allows localization of menu text.

const options = createAIMenuOptions(gridApiRef, {
  translate: 'Traducir columna', // Spanish
  highlight: 'Resaltar traducciones',
});

Cell Renderers & Utilities

TranslatableCell

(params: ICellRendererParams) => JSX.Element;

AG Grid cell renderer component. Reads column-level translation/highlight state from the GridApi on every render, extracts display text via extractText, and delegates to AITranslateText from @procore/ai-translations. Uses React.memo internally to avoid unnecessary re-renders of the translation component.

extractText

(value: unknown) => string;

Coerces an AG Grid cell value into a display string:

| Input Type | Behavior | | -------------------------------------- | ------------------------------------------------------------------------------------------------- | | string | Returned as-is | | Array<{ label?: string } \| unknown> | Each element: uses .label if object with label key, otherwise String(v); joined with ', ' | | { label?: string } | Returns .label ?? '' | | Anything else | Returns '' |

CustomizableTranslatableCell

(props: ICellRendererParams & CustomizableTranslatableCellParams) =>
  JSX.Element;

AG Grid cell renderer for cases where only part of the cell value should be translated. Thin adapter over CustomizableAITranslateText from @procore/ai-translations — does three jobs:

  1. Reads per-column translate/highlight state (and highlight mode) from columnFeatureState so the header-menu toggles continue to work.
  2. Extracts the display string from params.value via extractText.
  3. Passes the consumer's segmenter directly to CustomizableAITranslateText.

If you need the same "translate part of this string" behaviour outside a data table (forms, cards, descriptions, etc.), reach for CustomizableAITranslateText directly in @procore/ai-translations.

| cellRendererParams field | Type | Description | | -------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | segmenter | TextSegmenter (optional) | Pure function (text: string) => TranslatableSegment[]. Splits the extracted cell string into translatable and non-translatable parts. When omitted, the whole extracted value is translated (same behaviour as TranslatableCell). | | highlightMode | HighlightMode (optional) | Static initial value for where the highlight icon appears ('segment' or 'cell'). Use this when the mode is fixed at column-definition time and will never change at runtime. For dynamic post-mount changes, call setColumnHighlightMode(gridApi, field, mode) instead — it updates the WeakMap and refreshes cells without recreating the column definition, which would reset all toggle state. |

Types
import type {
  TextSegmenter,
  TranslatableSegment,
  HighlightMode,
} from '@procore/ai-translations';

type CustomizableTranslatableCellParams = {
  segmenter?: TextSegmenter;
  highlightMode?: HighlightMode;
};
Example: translating only the label in "{code} - {label}"
import {
  CustomizableTranslatableCell,
  setColumnHighlightMode,
  handleGridReady,
  type TextSegmenter,
} from '@procore/ai-translation-utils';
import { useRef } from 'react';

const codeLabelSegmenter: TextSegmenter = (text) => {
  const idx = text.indexOf(' - ');
  if (idx === -1) return [{ text, translate: true }];
  return [
    { text: text.slice(0, idx + 3), translate: false },
    { text: text.slice(idx + 3), translate: true },
  ];
};

function MyGrid() {
  const gridApiRef = useRef(null);

  // Set highlight mode once after the grid is ready
  const onGridReady = (params) => {
    handleGridReady(params, gridApiRef);
    setColumnHighlightMode(params.api, 'item', 'cell'); // one icon per cell
  };

  const columnDefs = [
    {
      field: 'item',
      cellRenderer: CustomizableTranslatableCell,
      cellRendererParams: { segmenter: codeLabelSegmenter },
      columnHeaderParams: { menuOptions: aiMenuOptions },
    },
  ];

  return <AgGridReact columnDefs={columnDefs} onGridReady={onGridReady} />;
}
Example: key-value split "Status: Awaiting Review"
const colonSplitSegmenter: TextSegmenter = (text) => {
  const idx = text.indexOf(': ');
  if (idx === -1) return [{ text, translate: true }];
  return [
    { text: text.slice(0, idx + 2), translate: false }, // "Status: " — keep
    { text: text.slice(idx + 2), translate: true }, // "Awaiting Review" — translate
  ];
};

cellRendererParams: {
  segmenter: colonSplitSegmenter;
}

Progress Popups

Both components consume the useAITranslation() hook from @procore/ai-translations and render fixed-position popups. Each is wrapped in a ProgressErrorBoundary that renders null on error, preventing translation UI failures from crashing the host app.

| Component | Hook Data | Visibility | Progress Bar Color | | ---------------------------- | ----------------------- | ----------------------------------- | ------------------ | | TranslationProgressPopup | translationProgress | Hidden when null or total === 0 | #f47e42 (orange) | | ModelDownloadProgressPopup | modelDownloadProgress | Hidden when null or isComplete | #3b82f6 (blue) |

Props

Both components accept an optional label prop. When omitted, the default label is used.

| Prop | Type | Required | Default (TranslationProgressPopup) | Default (ModelDownloadProgressPopup) | | ------- | -------- | -------- | ---------------------------------- | ------------------------------------ | | label | string | No | "Translating..." | "Downloading AI Model..." |

// Default labels
<TranslationProgressPopup />
<ModelDownloadProgressPopup />

// Custom labels (e.g. for localization)
<TranslationProgressPopup label="Traduciendo..." />
<ModelDownloadProgressPopup label="Descargando modelo..." />

Workflow: Request Flow

The following sequence diagram shows how a user toggling "Translate Column" from a column header menu flows through the system:

sequenceDiagram
    participant User
    participant AGGrid as AG Grid Column Menu
    participant Factory as createAIMenuOptions
    participant State as columnFeatureState
    participant WeakMap as WeakMap<GridApi, Record>
    participant Cell as TranslatableCell
    participant AIText as AITranslateText

    Note over Factory: At mount time, factory creates menu options
    Factory->>State: Captures toggleColumnTranslation in action closure

    User->>AGGrid: Clicks "Translate Column"
    AGGrid->>Factory: Invokes action(colDef)
    Factory->>State: toggleColumnTranslation(gridApi, colDef.field)
    State->>WeakMap: Flip field flag in translationStateMap
    State->>AGGrid: gridApi.refreshCells({ columns: [field], force: true })

    AGGrid->>Cell: Re-renders TranslatableCell(params)
    Cell->>State: isColumnTranslationEnabled(api, field) → true
    Cell->>State: isColumnHighlightEnabled(api, field)
    Cell->>Cell: extractText(params.value) → display string
    Cell->>AIText: <AITranslateText text shouldTranslate showHighlight />
    AIText-->>User: Translated text rendered in cell

Grid Initialization Flow

sequenceDiagram
    participant App
    participant AGGrid as AG Grid
    participant Handler as handleGridReady
    participant Ref as gridApiRef (MutableRefObject)
    participant State as columnFeatureState

    AGGrid->>Handler: onGridReady(params)
    Handler->>Ref: gridApiRef.current = params.api
    Handler->>AGGrid: api.addEventListener('paginationChanged', callback)

    Note over AGGrid: User navigates to page 2
    AGGrid->>State: paginationChanged → clearColumnFeatureStates(gridApi)
    State->>State: Reset both WeakMaps for this grid
    State->>AGGrid: refreshCells for previously active columns

Type Safety & Generics

WeakMap State Isolation (columnFeatureState.ts)

The central design decision in columnFeatureState.ts is using WeakMap instances keyed by GridApi for all per-column runtime state:

const translationStateMap = new WeakMap<GridApi, Record<string, boolean>>();
const highlightStateMap = new WeakMap<GridApi, Record<string, boolean>>();
const highlightModeMap = new WeakMap<GridApi, Record<string, HighlightMode>>();

Keeping highlightMode in the WeakMap (rather than in cellRendererParams) is important: if highlightMode were part of cellRendererParams, the column definition would change whenever the mode changed, causing AG Grid to re-initialize and silently lose all toggle state stored in the other two WeakMaps.

This delivers three type-safety guarantees:

  1. Key type constraint -- Only GridApi instances can be used as keys. Passing a plain object or null is a compile-time error at the WeakMap level; the exported functions add runtime guards (if (!gridApi) return) for the null | undefined union that consumers pass.
  2. Value type contract -- Record<string, boolean> enforces that column field names (string) always map to boolean flags. This prevents accidental storage of richer objects or numeric states.
  3. Automatic cleanup -- WeakMap allows the JavaScript engine to garbage-collect entries when the GridApi instance is no longer referenced. This is critical in SPAs where grids mount/unmount; no manual teardown is needed.

Nullable Parameter Patterns

Every public function accepts GridApi | null | undefined and string | undefined rather than requiring non-null values. This is deliberate -- it matches how AG Grid hands back these values in practice (e.g., colDef?.field is string | undefined, and a ref starts as null). The functions guard internally, so consumers don't need to litter their code with null checks:

// No need for: if (gridApi && field) { toggleColumnTranslation(gridApi, field); }
// Just call it directly -- the function handles nullish values safely:
toggleColumnTranslation(gridApiRef.current, colDef?.field);

MutableRefObject<GridApi | null>

The createAIMenuOptions and handleGridReady functions accept MutableRefObject<GridApi | null> -- React's useRef return type. This ensures:

  • The ref's .current property is always typed as GridApi | null
  • The ref object itself is stable across renders (referential equality)
  • Action closures in menu options always read the latest GridApi via the ref, avoiding stale closure bugs

AIMenuOption.action and ColDef

The action callback is typed as (colDef: ColDef | null) => void. The ColDef | null union accounts for cases where the menu is invoked without a column context. Implementations use optional chaining (colDef?.field) to safely extract the field name.


Advanced Patterns

Custom Menu Options

Extend the menu options array with your own entries that follow the AIMenuOption interface:

import type { AIMenuOption } from '@procore/ai-translation-utils';
import { createAIMenuOptions } from '@procore/ai-translation-utils';

const baseOptions = createAIMenuOptions(gridApiRef, {
  translate: 'Translate',
  highlight: 'Highlight',
});

const customOption: AIMenuOption = {
  label: 'Export Translations',
  value: 'export',
  action: (colDef) => {
    if (colDef?.field) {
      exportTranslationsForColumn(colDef.field);
    }
  },
};

const allOptions: AIMenuOption[] = [...baseOptions, customOption];

Custom Cell Renderer Wrapping extractText

Build on extractText to create a cell renderer with additional formatting:

import {
  extractText,
  isColumnTranslationEnabled,
} from '@procore/ai-translation-utils';
import type { ICellRendererParams } from '@ag-grid-community/core';

export function CustomTranslatableCell(params: ICellRendererParams) {
  const field = params.colDef?.field;
  const isTranslating = isColumnTranslationEnabled(params.api, field);
  const text = extractText(params.value);

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      {isTranslating && <TranslationIcon />}
      <span>{text}</span>
    </div>
  );
}

Multi-Grid State Isolation

Because state is keyed by GridApi via WeakMap, multiple grids on the same page are automatically isolated:

function DualGridPage() {
  const gridApiRefA = useRef<GridApi | null>(null);
  const gridApiRefB = useRef<GridApi | null>(null);

  // Each grid gets its own independent translation/highlight state
  const menuA = createAIMenuOptions(gridApiRefA, {
    translate: 'Translate',
    highlight: 'Highlight',
  });
  const menuB = createAIMenuOptions(gridApiRefB, {
    translate: 'Translate',
    highlight: 'Highlight',
  });

  return (
    <>
      <AgGridReact
        onGridReady={(p) => handleGridReady(p, gridApiRefA)} /* ... */
      />
      <AgGridReact
        onGridReady={(p) => handleGridReady(p, gridApiRefB)} /* ... */
      />
    </>
  );
}

Programmatic State Control

Bypass the menu UI to toggle translation from external controls:

import {
  toggleColumnTranslation,
  isColumnTranslationEnabled,
} from '@procore/ai-translation-utils';

function TranslateAllButton({
  gridApiRef,
  fields,
}: {
  gridApiRef: MutableRefObject<GridApi | null>;
  fields: string[];
}) {
  const handleClick = () => {
    for (const field of fields) {
      if (!isColumnTranslationEnabled(gridApiRef.current, field)) {
        toggleColumnTranslation(gridApiRef.current, field);
      }
    }
  };

  return <button onClick={handleClick}>Translate All Columns</button>;
}

Developer Experience

Prerequisites

This package is part of the platform-internationalization-js-monorepo

Scripts

| Command | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | yarn build | Builds the package via hammer lib:build (tsup/ESBuild). Produces dist/modern/ (ESM + CJS with conditional exports) and dist/legacy/ (fallback entry points). | | yarn dev | Starts a watch-mode dev build (hammer lib:start --env=development). | | yarn test | Builds first, then runs Jest tests with coverage. | | yarn test:nodeps | Runs tests without a preceding build (useful during development). | | yarn test:watch | Runs tests in watch mode. | | yarn lint | Runs both ESLint (lint:code) and TypeScript type checking (lint:types). | | yarn format | Formats all files with Prettier. | | yarn format:check | Checks formatting without writing changes. | | yarn analyze | Builds with metafile output, then opens an interactive bundle visualizer. | | yarn clean | Removes the dist/ directory. |

Build Output: dist/ vs src/

The src/ directory contains TypeScript source files. The build process (tsup via Hammer) compiles these into two output directories:

dist/
├── modern/          # Used by package.json "exports" field
│   ├── index.mjs    # ESM bundle (import)
│   ├── index.js     # CJS bundle (require)
│   ├── index.d.mts  # ESM type declarations
│   └── index.d.ts   # CJS type declarations
└── legacy/          # Used by package.json "main"/"module"/"types" fields
    ├── index.js     # CJS entry
    ├── index.mjs    # ESM entry
    └── index.d.ts   # Type declarations
  • Modern consumers (Node 16+, bundlers with exports support) resolve via dist/modern/.
  • Legacy consumers fall back to dist/legacy/ via the main, module, and types fields.
  • Only the dist/ directory is published to npm ("files": ["dist"]).

Testing

Tests use Jest (via @procore/hammer-test-jest) with React Testing Library and jest-dom matchers. Test files live alongside source code in __tests__/ directories.

src/data-table/__tests__/
├── columnFeatureState.test.ts            # Unit tests with mocked GridApi
├── aiMenuOptions.test.ts                 # Tests with mocked columnFeatureState
├── TranslatableCell.test.tsx             # Component rendering tests
├── CustomizableTranslatableCell.test.tsx # Adapter wiring: segmenter passthrough, column state, highlight
├── TranslationProgressPopup.test.tsx     # Popup visibility/progress tests
└── ModelDownloadProgressPopup.test.tsx

Key testing patterns used:

  • GridApi methods are mocked via Jest (jest.fn()) since AG Grid is a peer dependency
  • @procore/ai-translations is mocked at the module level to isolate adapter logic from the translation pipeline
  • columnFeatureState is mocked in component tests so isColumnTranslationEnabled, isColumnHighlightEnabled, and getColumnHighlightMode return controlled values

Peer Dependencies

| Package | Version | Purpose | | -------------------------- | -------- | ------------------------------------------------------------------------------------- | | @ag-grid-community/core | >= 31 | AG Grid types and API (GridApi, ColDef, ICellRendererParams, GridReadyEvent) | | @procore/ai-translations | >= 0.6.0 | AITranslateText component, useAITranslation hook, AITranslationProvider context | | react | >= 17 | React runtime for components, hooks, and refs |


License

SEE LICENSE IN LICENSE