A zero-dependency Node.js CLI that statically analyzes your CSS / SCSS (and inline styles in JS / JSX / TSX / HTML / Vue / Svelte) for right-to-left (RTL) layout bugs — the class of defect that silently breaks Arabic, Hebrew, Persian and Urdu interfaces — and reports each one with a concrete logical-property fix.
Physical margin-left, hardcoded direction: ltr, an un-mirrored translateX, a
.icon-arrow-right that never gets flipped: these all look fine in an English build and fall apart
the moment the document direction becomes rtl. rtl-audit finds them before your users do.
- Zero runtime dependencies. Just Node 18+. Clone and run.
- Tolerant, CSS-aware scanner. A small hand-written tokenizer tracks selector context, line/column, comments and strings — no heavyweight CSS parser pulled in.
- Actionable output. Every finding names the offending snippet and the logical replacement.
- CI-friendly. Machine-readable
--format json, and a non-zero exit code when errors are found.
Modern CSS ships logical properties:
margin-inline-start instead of margin-left, inset-inline-start instead of left,
text-align: start instead of text-align: left. Logical properties resolve relative to the
writing direction, so one rule works in both LTR and RTL. Physical properties do not — they always
mean "the left edge of the screen", which is exactly wrong in a mirrored layout.
For the full background, see the RTL & bidirectional layout engineering guide and its companion, fixing mirrored icons & logical CSS properties.
This tool is distributed by cloning the repo — it is intentionally not published to npm, and it has no dependencies to install.
git clone https://github.com/i18n-l10n/rtl-audit.git
cd rtl-audit
node bin/rtl-audit.js examplesThat's it — there is no npm install step. (Node 18 or newer is required.)
node bin/rtl-audit.js [options] <path...>Paths may be files or directories; directories are scanned recursively. You can also run it through
the npm scripts: npm test, npm run audit:examples.
| Option | Description |
|---|---|
--format <pretty|json> |
Output format. Default pretty. |
--ignore <glob> |
Skip paths matching a glob (*, **, ?). Repeatable. |
--rule-off <id> |
Disable a rule by id. Repeatable. |
--fix-hints |
Append a per-rule count summary. |
--no-color |
Disable ANSI colours (auto-disabled when output is not a TTY). |
-h, --help |
Show help. |
-v, --version |
Show version. |
node_modules/, .git/, dist/, build/ and coverage/ are ignored by default.
| Code | Meaning |
|---|---|
0 |
No error-severity findings (a clean run, or only warn/info). |
1 |
At least one error-severity finding. |
2 |
Usage or I/O error (bad flag, no input paths). |
This makes rtl-audit a drop-in CI gate — see
GitHub Actions i18n CI gates
for how a check like this fits into a localization pipeline.
| Rule | Severity | What it catches |
|---|---|---|
physical-property |
error | Physical box/position/alignment properties and keywords: margin-left, padding-right, left, right, border-left, text-align: left, float: right, clear: left, border-top-left-radius, … → their logical equivalents. |
physical-shorthand |
error | Asymmetric 4-value shorthands (margin: 0 8px 0 16px) whose left/right differ and therefore will not mirror. |
hardcoded-direction |
error | direction: ltr|rtl pinned in CSS, and unicode-bidi: *-override, which fight the document direction. |
untranslated-transform |
error | transform: translateX(...) / translate(x, y) / translate3d(...) and the translate property with a non-zero horizontal component that won't flip in RTL. |
non-mirrored-arrow-icon |
warn | Directional icon selectors (.icon-arrow-right, .chevron-left, .btn-back) with no nearby [dir="rtl"] override — a heuristic prompt to verify mirroring. |
bidi-risk |
info | Interpolated/concatenated runtime values glued to bidi-sensitive punctuation (`${d}/${m}/${y}`, id + '-' + name) that commonly reorder in RTL context. |
Rules 1–4 are the solid, high-confidence core (severity error). Rules 5 and 6 are deliberately lower-signal heuristics (warn / info) that flag things worth a human glance.
Turn any rule off with --rule-off <id>, e.g. --rule-off bidi-risk.
The repo ships an examples/ directory with a deliberately broken stylesheet, a broken React
component, and a clean RTL-safe stylesheet:
examples/
├── broken.css # trips every CSS rule
├── Notification.jsx # inline-style + bidi bugs
└── clean.css # logical properties throughout → reports nothing
Running the tool against them:
$ node bin/rtl-audit.js --fix-hints examples/broken.css
examples/broken.css
5:3 error physical-property
Physical property "margin-left" does not flip in RTL.
found: margin-left: 16px
fix: Use "margin-inline-start: 16px" so it follows the writing direction.
docs: https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/
8:3 error physical-property
"text-align: left" is a physical direction that will not mirror in RTL.
found: text-align: left
fix: Use "text-align: start".
docs: https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/
12:3 error physical-shorthand
Asymmetric 4-value "margin" shorthand (right="8px", left="16px") is not RTL-safe.
found: margin: 0 8px 0 16px
fix: Split the horizontal values: "margin-inline-start: 16px; margin-inline-end: 8px" (block stays "margin-block: 0 0").
docs: https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/
23:3 error untranslated-transform
Horizontal translateX(-100%) will not mirror — it shifts the same direction in RTL.
found: transform: translateX(-100%)
fix: Add a `[dir="rtl"]` override that negates the X value, or drive the offset with logical properties.
docs: https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/fixing-mirrored-icons-and-logical-css-properties/
36:1 warn non-mirrored-arrow-icon
Directional icon selector ".icon-arrow-right" has no nearby RTL override — it may point the wrong way in RTL.
found: .icon-arrow-right
fix: Mirror it under `[dir="rtl"]` (e.g. `transform: scaleX(-1)`) or swap to a direction-neutral / start-end glyph.
docs: https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/fixing-mirrored-icons-and-logical-css-properties/
Summary
12 error · 2 warn · 0 info
14 finding(s) across 1 file(s).
By rule
2 hardcoded-direction
2 non-mirrored-arrow-icon
7 physical-property
2 physical-shorthand
1 untranslated-transform(Output above is trimmed for brevity — the real run lists every finding.)
The clean stylesheet reports nothing and exits 0:
$ node bin/rtl-audit.js examples/clean.css
✓ No RTL layout issues found.
Scanned 1 file(s).$ node bin/rtl-audit.js --format json examples/broken.css{
"summary": {
"filesScanned": 1,
"findings": 14,
"severities": { "error": 12, "warn": 2, "info": 0 },
"byRule": {
"physical-property": 7,
"physical-shorthand": 2,
"hardcoded-direction": 2,
"untranslated-transform": 1,
"non-mirrored-arrow-icon": 2
}
},
"findings": [
{
"file": "examples/broken.css",
"line": 5,
"col": 3,
"rule": "physical-property",
"severity": "error",
"message": "Physical property \"margin-left\" does not flip in RTL.",
"snippet": "margin-left: 16px",
"fix": "Use \"margin-inline-start: 16px\" so it follows the writing direction.",
"guide": "https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/"
}
]
}# .github/workflows/rtl.yml
name: RTL audit
on: [push, pull_request]
jobs:
rtl-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# Assuming rtl-audit is vendored (e.g. as a git submodule or copied in):
- run: node tools/rtl-audit/bin/rtl-audit.js srcThe step fails the job as soon as any error-severity finding appears, so physical-property
regressions never reach main. Pair it with
failing the build on untranslated keys
for a complete i18n gate.
rtl-audit deliberately avoids a full CSS parser. Instead, src/tokenizer.js implements a small,
tolerant declaration scanner that walks the source character by character while:
- correctly skipping
/* … */block comments and//line comments (but not//insideurl(...)), - treating quoted strings as opaque (so
content: "a; b { c }"doesn't confuse it), - tracking parenthesis depth (so
;inside adata:URI doesn't split a declaration), - maintaining a selector stack for SCSS-style nesting, and
- recording precise 1-based line/column positions for every property and selector.
For non-CSS files, src/extract.js pulls out <style> blocks, style="…" attributes, React
style={{ … }} objects (camelCase keys are converted to kebab-case) and Vue :style="{ … }"
bindings, then feeds them through the same rule engine. The rules themselves live in src/rules.js.
bin/rtl-audit.js CLI entry point
src/tokenizer.js tolerant CSS declaration scanner
src/extract.js inline-style / style-object extractors
src/rules.js the six RTL rules + orchestration
src/scanner.js per-file dispatch by extension
src/report.js pretty + JSON reporters
src/cli.js argument parsing, file walking, ignore globs
examples/ runnable broken + clean fixtures
test/ node:test suites (tokenizer, rules, extract, cli)
The test suite uses only Node's built-in node:test and node:assert/strict — no external test
dependencies.
npm test # or: node --testTests cover tokenizer edge cases (comments, strings, url(), data URIs, nesting) and every rule
with both true positives and true negatives — for example, margin-inline-start must not
be flagged, and a symmetric margin: 0 8px 0 8px must not be reported as asymmetric.
- The directional-icon and bidi-risk rules are heuristics — they favour a nudge-to-review over certainty, which is why they are warn/info rather than error.
- The scanner understands the common shapes of inline styles in JS/JSX/Vue/Svelte; it does not evaluate CSS-in-JS template functions or runtime-computed styles.
- Suggested fixes are literal, direct translations; always sanity-check the result in an actual RTL render.
From the i18n & l10n Pipelines reference at i18n-l10n.com:
- RTL & bidirectional layout engineering — the concepts behind every rule here.
- Fixing mirrored icons & logical CSS properties — the definitive fix list.
- Date & number formatting standards — why
${day}/${month}is a bidi hazard. - GitHub Actions i18n CI gates — wiring static checks into your pipeline.
MIT © 2026 i18n-l10n.
Built as an open tool for the i18n & l10n Pipelines reference — end-to-end internationalization and localization documentation for developers who ship in more than one language.