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` 会提取 `