Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rtl-audit

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.

Why this matters

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.


Installation

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 examples

That's it — there is no npm install step. (Node 18 or newer is required.)


Usage

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.

Options

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.

Exit codes

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.


The rules

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.


Example run

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).

JSON output

$ 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/"
    }
  ]
}

Use it in CI

# .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 src

The 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.


How it works

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 // inside url(...)),
  • treating quoted strings as opaque (so content: "a; b { c }" doesn't confuse it),
  • tracking parenthesis depth (so ; inside a data: 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.

Project layout

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)

Development & tests

The test suite uses only Node's built-in node:test and node:assert/strict — no external test dependencies.

npm test          # or: node --test

Tests 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.


Limitations

  • 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.

Further reading

From the i18n & l10n Pipelines reference at i18n-l10n.com:


License

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.

About

Zero-dependency CLI that finds RTL layout bugs in CSS/SCSS and inline styles and suggests logical-property fixes

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages