i18n-l10n/rtl-audit
GitHub: i18n-l10n/rtl-audit
一个零依赖的 Node.js CLI 工具,通过静态分析检测 CSS/SCSS 及内联样式中的 RTL 布局缺陷并给出逻辑属性修复建议。
Stars: 0 | Forks: 0
# rtl-audit
一个零依赖的 Node.js CLI 工具,用于静态分析您的 **CSS / SCSS**(以及
**JS / JSX / TSX / HTML / Vue / Svelte** 中的内联样式),查找**从右到左 (RTL) 布局错误**——这类
缺陷会悄无声息地破坏阿拉伯语、希伯来语、波斯语和乌尔多语界面——并为每个问题报告具体的
**逻辑属性修复方案**。
物理属性 `margin-left`、硬编码的 `direction: ltr`、未镜像翻转的 `translateX`、永远不会被翻转的
`.icon-arrow-right`:这些在英文构建版本中看起来都很好,但当文档方向变为 `rtl` 时就会出问题。
`rtl-audit` 会在您的用户发现问题之前找到它们。
- **零运行时依赖。** 只需 Node 18+。克隆即可运行。
- **容错的、支持 CSS 的扫描器。** 一个小型手写的 tokenizer 会跟踪选择器上下文、
行/列位置、注释和字符串——无需引入重量级的 CSS 解析器。
- **可操作的输出。** 每个发现都会指出有问题的代码片段*以及*逻辑替换方案。
- **对 CI 友好。** 支持机器可读的 `--format json`,并在发现错误时返回非零退出代码。
## 为什么这很重要
现代 CSS 采用了 [**逻辑属性**](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values):
使用 `margin-inline-start` 代替 `margin-left`,使用 `inset-inline-start` 代替 `left`,
使用 `text-align: start` 代替 `text-align: left`。逻辑属性会根据*书写方向*进行解析,
因此一条规则可以同时适用于 LTR 和 RTL。物理属性则不然——它们永远是指
“屏幕的左边缘”,这在镜像布局中是完全错误的。
有关完整的背景信息,请参阅
[RTL 与双向布局工程指南](https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/)
及其配套文章,
[修复镜像图标与 CSS 逻辑属性](https://www.i18n-l10n.com/core-i18n-architecture-locale-negotiation/rtl-bidirectional-layout-engineering/fixing-mirrored-icons-and-logical-css-properties/)。
## 安装说明
该工具**通过克隆代码库**分发——它故意不发布到 npm,并且没有
需要安装的依赖项。
```
git clone https://github.com/i18n-l10n/rtl-audit.git
cd rtl-audit
node bin/rtl-audit.js examples
```
就是这样——没有 `npm install` 步骤。(需要 Node 18 或更高版本。)
## 运行说明
```
node bin/rtl-audit.js [options]
```
路径可以是文件或目录;目录将被递归扫描。您也可以通过
npm 脚本运行它:`npm test`、`npm run audit:examples`。
### 选项
| 选项 | 描述 |
| --- | --- |
| `--format ` | 输出格式。默认为 `pretty`。 |
| `--ignore ` | 跳过匹配 glob 的路径 (`*`, `**`, `?`)。可重复使用。 |
| `--rule-off ` | 按 id 禁用某条规则。可重复使用。 |
| `--fix-hints` | 附加每条规则的计数摘要。 |
| `--no-color` | 禁用 ANSI 颜色(当输出不是 TTY 时自动禁用)。 |
| `-h`, `--help` | 显示帮助。 |
| `-v`, `--version` | 显示版本。 |
`node_modules/`、`.git/`、`dist/`、`build/` 和 `coverage/` 默认会被忽略。
### 退出代码
| 代码 | 含义 |
| --- | --- |
| `0` | 没有**错误**级别的发现(运行正常,或只有 warn/info)。 |
| `1` | 至少有一个**错误**级别的发现。 |
| `2` | 使用或 I/O 错误(错误的标志,没有输入路径)。 |
这使得 `rtl-audit` 可以直接作为 CI 的检查门禁——请参阅
[GitHub Actions i18n CI 门禁](https://www.i18n-l10n.com/translation-workflows-cicd-pipeline-sync/github-actions-i18n-ci-gates/)
了解此类检查如何融入本地化 pipeline。
## 规则
| 规则 | 严重性 | 捕获内容 |
| --- | --- | --- |
| `physical-property` | error | 物理盒模型/定位/对齐属性和关键字:`margin-left`、`padding-right`、`left`、`right`、`border-left`、`text-align: left`、`float: right`、`clear: left`、`border-top-left-radius` 等 → 转换为它们对应的逻辑属性。 |
| `physical-shorthand` | error | 非对称的 4 值简写(`margin: 0 8px 0 16px`),其左/右值不同,因此不会发生镜像。 |
| `hardcoded-direction` | error | 在 CSS 中固定的 `direction: ltr\|rtl`,以及与文档方向冲突的 `unicode-bidi: *-override`。 |
| `untranslated-transform` | error | `transform: translateX(...)` / `translate(x, y)` / `translate3d(...)` 以及具有非零水平分量的 `translate` 属性,它们在 RTL 中不会发生翻转。 |
| `non-mirrored-arrow-icon` | warn | 没有附近 `[dir="rtl"]` 重写的定向图标选择器(`.icon-arrow-right`、`.chevron-left`、`.btn-back`)——这是一种用于提醒验证镜像翻转的启发式提示。 |
| `bidi-risk` | info | 拼接在双向敏感标点符号上的插值/串联运行时值(`` `${d}/${m}/${y}` ``、`id + '-' + name`),它们通常在 RTL 上下文中发生重排。 |
规则 1–4 是坚实、高置信度的核心(严重性为 **error**)。规则 5 和 6 特意设定为
较低信号的启发式规则(**warn** / **info**),用于标记值得人工检查的内容。
使用 `--rule-off ` 关闭任何规则,例如 `--rule-off bidi-risk`。
## 运行示例
该代码库附带了一个 `examples/` 目录,其中包含一个故意写错的样式表、一个有问题的 React
组件,以及一个干净的 RTL 安全样式表:
```
examples/
├── broken.css # trips every CSS rule
├── Notification.jsx # inline-style + bidi bugs
└── clean.css # logical properties throughout → reports nothing
```
针对它们运行该工具:
```
$ 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
```
(为简洁起见,上面的输出已被截断——实际运行会列出所有发现。)
干净的样式表不会报告任何内容并以 `0` 退出:
```
$ node bin/rtl-audit.js examples/clean.css
✓ No RTL layout issues found.
Scanned 1 file(s).
```
### JSON 输出
```
$ 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/"
}
]
}
```
## 在 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
```
只要出现任何**错误**级别的发现,该步骤就会导致作业失败,因此物理属性
回归问题永远不会进入 `main` 分支。将其与
[在未翻译的键上使构建失败](https://www.i18n-l10n.com/translation-workflows-cicd-pipeline-sync/github-actions-i18n-ci-gates/failing-build-on-untranslated-keys/)
结合使用,可构建完整的 i18n 门禁。
## 工作原理
`rtl-audit` 刻意避免使用完整的 CSS 解析器。相反,`src/tokenizer.js` 实现了一个小型的、
容错的声明扫描器,它会逐个字符遍历源代码,同时:
- 正确跳过 `/* … */` 块注释和 `//` 行注释(但**不**包括 `url(...)` 内的 `//`),
- 将带引号的字符串视为不透明的(这样 `content: "a; b { c }"` 就不会对其造成干扰),
- 跟踪括号深度(这样 `data:` URI 内的 `;` 就不会分割声明),
- 为 SCSS 样式的嵌套维护选择器栈,并且
- 为每个属性和选择器记录精确的基于 1 的行/列位置。
对于非 CSS 文件,`src/extract.js` 会提取 `