Dependency-free RTL and Unicode bidirectional-text toolkit for JavaScript and TypeScript. Locale helpers stay under 6 KiB. The full UAX #9 engine lives on a separate subpath, passes both official Unicode 17 suites with zero failures, and never reverses your DOM.
npm install @rtl-resolver/coreimport { directionFromLocale, isolateIfNeeded, resolveDirection } from "@rtl-resolver/core";
import { analyzeBidi, resolveBidiLevels } from "@rtl-resolver/core/bidi";
directionFromLocale("ar-EG"); // "rtl"
resolveDirection({ direction: "auto", text: userInput, locale: userLocale });
isolateIfNeeded("John Smith", "rtl");
resolveBidiLevels("مرحبا 123");
analyzeBidi("Hello שלום", { baseDirection: "auto" });Existing rtl-resolver / rtl-resolver/bidi imports still work. New apps
should depend on @rtl-resolver/core so they do not ship two copies of the
bidi tables.
Most RTL bugs are not “flip the page.” They are mixed-script names in Arabic
UI, physical margin-left in CSS-in-JS, scrollLeft that means the opposite
in Firefox, and dir="ltr" hard-coded in a layout. This workspace is the
engine plus the audit/migrate/React/browser layers around it.
| You need | Use |
|---|---|
| Page/UI direction from locale | @rtl-resolver/core |
Mixed-content isolation, <bdi> / <bdo>
|
@rtl-resolver/core, @rtl-resolver/react
|
| Spec-accurate levels, runs, visual maps | @rtl-resolver/core/bidi |
Next.js App Router lang / dir
|
@rtl-resolver/next + a Client DirectionProvider
|
| Physical CSS → logical CSS |
@rtl-resolver/cli (audit, migrate) |
| Scroll, keys, swipes | @rtl-resolver/browser |
| MUI / Radix / Headless menus and dialogs |
@rtl-resolver/mui, radix, headless-ui
|
It will not reverse DOM order, auto-rewrite left / right positioning,
execute CSS interpolations, or wrap entire component catalogs.
The engine is a full UAX #9 pipeline (P1–P3, X1–X10, W1–W7, N0–N2, I1–I2, L1–L2). Official suites are a release gate:
| Suite | Cases | Failures |
|---|---|---|
BidiCharacterTest.txt |
91,707 | 0 |
BidiTest.txt |
770,241 | 0 |
npm run test:bidi:conformance # hash-verified Unicode 17 download + both suites
npm test # offline smoke subset + 125 unit testsreorderBidi() is for terminals, canvas, and diagnostics. Browsers already
apply bidi layout — inserting an already-reordered string into HTML can
reorder it twice. Pass applyL3: true when a renderer must keep combining
marks beside their base.
Vitest benches use 100× repeated strings on this machine. Numbers are
diagnostic, not CI gates. rtl-resolver and bidi-js expose different
contracts and Unicode versions, so the comparison is throughput only.
| Bench | ops/sec |
|---|---|
ASCII resolveBidiLevels
|
~33,600 |
Mixed analyzeBidi
|
~1,210 |
bidi-js mixed embedding levels |
~4,380 |
ASCII-only LTR text takes a fast path (printable ASCII + TAB) that agrees with the full pipeline. Mixed Hebrew/Arabic/Latin pays for the real algorithm.
| Published entry | ESM raw | ESM gzip |
|---|---|---|
@rtl-resolver/core |
5.7 KB | 1.8 KB |
@rtl-resolver/core/bidi |
212 KB | 12.5 KB |
@rtl-resolver/core/plugin |
1.0 KB | 0.4 KB |
Every workspace ESM/CJS file has a raw + gzip budget in
scripts/check-package-size.mjs. Installing both rtl-resolver and
@rtl-resolver/core ships the bidi tables twice (~213 KB / ~14 KB gzip).
npm run benchmark
npm run check:sizeimport {
directionFromLocale,
directionFromText,
getDirection,
inlineSides,
isolate,
resolveDirection,
} from "@rtl-resolver/core";
directionFromLocale("ar-EG"); // "rtl"
directionFromText("مرحبا بالعالم"); // "rtl"
resolveDirection({ direction: "auto", text: userInput, locale: userLocale });
inlineSides("rtl"); // { start: "right", end: "left" } — only when an API is physicalresolveDirection() uses an explicit ltr/rtl value first. In auto mode
it checks the first strong character in text, then the locale, then
fallback (default ltr). Prefer CSS logical properties over inlineSides().
import { cookies } from "next/headers";
import { resolveNextDirection } from "@rtl-resolver/next";
import { DirectionProvider } from "./direction-provider";
export default async function RootLayout({ children, params }: LayoutProps<"/[lang]">) {
const { dir, lang } = await resolveNextDirection({ params, cookies: cookies() });
return (
<html dir={dir} lang={lang}>
<body>
<DirectionProvider locale={lang}>{children}</DirectionProvider>
</body>
</html>
);
}"use client";
export { DirectionProvider } from "@rtl-resolver/react";@rtl-resolver/react is a Client Component module ("use client"). Resolve
dir / lang on the server with @rtl-resolver/next; wrap the tree from a
client file. <Bdi> and <Bdo> are escaping-safe mixed-content elements.
npx rtl-resolver init
npx rtl-resolver audit ./src --write-baseline --baseline .rtl-resolver-baseline.json
npx rtl-resolver migrate ./src --dry-run --reportSafe migrate rewrites margin/padding/border and text-align. Positioning
left / right stays review-only. Full walkthrough:
docs/MIGRATION.md.
| Package | Role |
|---|---|
@rtl-resolver/core |
Locale, script, isolation, UAX #9, plugin types |
@rtl-resolver/react |
DirectionProvider, useDirection, <Bdi>, <Bdo>
|
@rtl-resolver/next |
App Router promised params/cookies → lang/dir
|
@rtl-resolver/browser |
Logical scroll, paging, keys, swipes |
@rtl-resolver/css + cli
|
CSS/CSS-in-JS analysis, audit / migrate / init
|
@rtl-resolver/eslint / stylelint
|
Direction regressions in JS and CSS |
@rtl-resolver/fonts |
cmap coverage, GSUB joining fixture, HarfBuzz |
@rtl-resolver/icons / motion
|
Mirror policies and logical start/end motion |
@rtl-resolver/adapters / testing
|
Forms, tables, LTR/RTL test helpers |
@rtl-resolver/mui / radix / headless-ui
|
Direction + dialog/menu/popover/select wrappers |
npm install @rtl-resolver/core @rtl-resolver/react @rtl-resolver/browser
npm install @rtl-resolver/css @rtl-resolver/cliRequires Node 20+. CI runs 20 and 22.
npm ci
npm test
npm run build
npm run build:workspaces
npm run check:size
npm run check:exports
npm run verify:release # typecheck, tests, builds, size, exports, Unicode suitesverify:release does not run Playwright, Storybook, or screen readers.
verify:full adds Chromium Playwright and the Storybook example. VoiceOver and
NVDA jobs exist and are unproven on hosted runners.
Coordinated 0.2.0 publishing: RELEASE_POLICY.md.
Latest numbers: docs/RUN_RESULTS.md.
MIT