---
slug: "pi-intl-segmenter-fallback"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/T0mSIlver/pi-intl-segmenter-fallback@main/README.md"
repo: "https://github.com/T0mSIlver/pi-intl-segmenter-fallback"
source_file: "README.md"
branch: "main"
---
# pi-intl-segmenter-fallback

[![npm](https://img.shields.io/npm/v/pi-intl-segmenter-fallback)](https://www.npmjs.com/package/pi-intl-segmenter-fallback)
[![CI](https://github.com/T0mSIlver/pi-intl-segmenter-fallback/actions/workflows/ci.yml/badge.svg)](https://github.com/T0mSIlver/pi-intl-segmenter-fallback/actions/workflows/ci.yml)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

A [pi](https://pi.dev) extension that stops the TUI from **segfaulting at
startup** on Node builds compiled with small-ICU — typically the RHEL/Fedora
`nodejs` RPM installed without `nodejs-full-i18n`.

## The problem

On small-ICU Node builds, `new Intl.Segmenter()` succeeds but the first
`.segment()` call dereferences a null `icu::BreakIterator` inside V8 and kills
the process with SIGSEGV ([nodejs/node#51752]). pi's TUI calls `.segment()`
for grapheme-cluster cursor and width math on its very first render, so pi
dies instantly on those builds ([earendil-works/pi#6359]).

Check whether your build is affected:

```sh
node -e 'console.log(process.config.variables.icu_small)'   # true → affected
```

> **The real fix** is installing full ICU data (`sudo dnf install
> nodejs-full-i18n`). This extension is a stopgap that keeps pi usable when
> you can't — no root, immutable image, distro without the package.

## Install

```sh
pi install npm:pi-intl-segmenter-fallback
```

Or straight from git, or pinned, or just for one run:

```sh
pi install git:github.com/T0mSIlver/pi-intl-segmenter-fallback
pi install npm:pi-intl-segmenter-fallback@0.1.0   # pinned, skipped by pi update
pi -e npm:pi-intl-segmenter-fallback              # try without installing
```

## How it works

At extension **import time**, if `process.config.variables.icu_small ===
true`, the extension replaces `Intl.Segmenter.prototype.segment` and
`.resolvedOptions` with a pure-JS fallback. On full-ICU/system-ICU builds it
is a **strict no-op** — method identity is untouched (verified by the test
suite).

It patches the *prototype*, not the constructor, because the constructor
works even on broken builds — only `.segment()` crashes. pi's TUI creates its
segmenter singletons at module load, *before* extensions run; the prototype
patch covers those pre-existing instances, so the extension only needs to
load before the first `.segment()` call (the first TUI render), which pi's
extension loader guarantees. Granularity of pre-existing instances is
recovered through the native `resolvedOptions`, which only reads internal
slots and is safe on broken builds (verified on a real one).

When the fallback activates, a one-time warning is shown at session start.

### Fallback semantics

Fine for a terminal UI, not linguistically exact:

| Granularity | Behavior |
|---|---|
| `grapheme` | One segment per Unicode code point (surrogate-pair safe). ZWJ/skin-tone clusters split into parts — `👍🏽` takes two arrow presses instead of one; cursor math stays self-consistent because pi derives widths from the same segmentation. |
| `word` | Runs of `\p{L}\p{N}\p{M}\p{Pc}` tagged `isWordLike: true` (what pi's Alt-B/Alt-F word navigation reads), runs of whitespace, other characters individually. |
| `sentence` | Naive split after terminal punctuation (unused by pi). |

Segments objects are re-iterable and support `.containing(index)`; each
segment is `{segment, index, input[, isWordLike]}`.

### Environment overrides

| Variable | Effect |
|---|---|
| `PI_SEGMENTER_FALLBACK=1` | Force the patch on even with healthy ICU (testing) |
| `PI_SEGMENTER_FALLBACK=0` | Never patch, even on small-ICU builds |

## Test evidence

Verified against a **real small-ICU build** (AlmaLinux 9 `nodejs-22.22.2`
without `nodejs-full-i18n`): without the extension, `pi` segfaults at startup
(exit 139); with it, the TUI renders, cursor movement across `héllo 👍🏽 test`
is consistent, and word navigation works. On full-ICU Node 24 the extension
provably changes nothing. Full transcripts and methodology:
[docs/test-evidence.md](https://github.com/T0mSIlver/pi-intl-segmenter-fallback/blob/HEAD/docs/test-evidence.md).

## Development

```sh
npm install
npm run verify        # typecheck + test suite (needs Node ≥ 22.18)
pi install .          # smoke-test the package manifest against a local pi
```

The test suite (`test/fallback.mjs`) simulates pi's usage pattern — segmenter
instances constructed *before* the extension imports — and asserts no-op
behavior on healthy builds (`noop` mode) and full fallback semantics
(`forced` mode).

## License

[MIT](https://github.com/T0mSIlver/pi-intl-segmenter-fallback/tree/HEAD/LICENSE)

[nodejs/node#51752]: https://github.com/nodejs/node/issues/51752
[earendil-works/pi#6359]: https://github.com/earendil-works/pi/issues/6359
