可访问性与国际化:WCAG 2.2、ARIA 与 i18n 工程实践

引言 可访问性(a11y)与国际化(i18n)经常被当作项目收尾的「锦上添花」,但它们在工程上其实是同一件事的两种延伸:让产品服务于更广泛的用户。本文从 WCAG 2.2 四大原则出发,讲解 ARIA 的正确用法、键盘导航与焦点管理、屏幕阅读器兼容,以及基于 i18next/react-intl 的国际化工程方案,包括复数规则与 RTL 布局的落地细节。

引言

可访问性(a11y)与国际化(i18n)经常被当作项目收尾的"锦上添花",但它们在工程上其实是同一件事的两种延伸:让产品服务于更广泛的用户。本文从 WCAG 2.2 四大原则出发,讲解 ARIA 的正确用法、键盘导航与焦点管理、屏幕阅读器兼容,以及基于 i18next/react-intl 的国际化工程方案,包括复数规则与 RTL 布局的落地细节。文中涉及 WCAG 2.2 成功标准、ARIA 规范、axe-core、i18next、react-intl(FormatJS)、Intl.PluralRules 等均为真实标准与真实库。


一、WCAG 2.2 四大原则:感知、操作、理解、健壮

1.1 POUR 原则框架

WCAG(Web Content Accessibility Guidelines)2.2 把无障碍要求归纳为四大原则(POUR):

原则英文含义典型失败
感知Perceivable信息可被所有感官接收图片无 alt、纯颜色传达状态
操作Operable交互可被键盘/辅助技术操作焦点不可见、陷阱键盘
理解Understandable内容与操作可被理解语言混乱、导航不一致
健壮Robust可被各种 UA/AT 可靠解析语义错误、无效 ARIA

1.2 关键成功标准速查

WCAG 2.2 中与前端工程师最相关的高频标准:

标准级别要求落地
1.4.3 对比度AA正文对比度 ≥ 4.5:1色彩 token 校验
2.4.7 焦点可见AA键盘焦点清晰可见focus-visible 样式
2.4.11 焦点不被遮挡AA(新增)固定定位不遮挡焦点元素scroll-margin 处理
3.2.6 一致帮助A(新增)帮助机制位置一致帮助入口统一
4.1.2 名称/角色/值A控件有正确的语义ARIA + 原生语义

1.3 从"修漏洞"到"设计即无障碍"

无障碍的最佳实践不是在完成后修复,而是在设计阶段就纳入。一个务实的做法是建立a11y 需求检查单:每个新功能上线前,核对键盘可达、语义正确、焦点顺序合理、对比度达标。把这条检查单写进 PR 模板,比事后用工具扫描更根本。

无障碍不是一次性的「修复工程」,而是每个 PR 都要回答的常规问题:键盘能操作吗?读屏器能理解吗?焦点顺序合理吗?


二、ARIA 角色与属性:正确使用而非滥用

2.1 ARIA 的核心原则

ARIA(Accessible Rich Internet Applications)的作用是补充原生 HTML 无法表达的语义。它的第一原则是:能使用原生语义就绝不使用 ARIA——<button> 自带角色与键盘行为,比 <div role="button"> 更可靠。

<!-- 反模式:用 div 模拟按钮,需要手动补键盘行为 -->
<div class="btn" role="button" tabindex="0" onclick="submit()">提交</div>

<!-- 正确:直接用 button 元素 -->
<button type="button" onclick="submit()">提交</button>

2.2 常用 ARIA 属性速查

属性作用适用场景
aria-label为无文本控件提供名称图标按钮
aria-labelledby引用其他元素作为名称表单分组
aria-describedby关联补充描述表单错误提示
aria-expanded指示展开/收起状态折叠面板、下拉
aria-controls关联控制的区域Tab、手风琴
aria-live宣布动态内容变化通知、错误汇总
aria-current指示当前项导航高亮

2.3 一个带完整语义的组件

以自定义下拉选择为例,展示 aria 属性如何协同工作:

<div class="select" role="combobox" aria-expanded="false" aria-haspopup="listbox">
  <button id="select-trigger" aria-controls="select-listbox" aria-labelledby="select-label">
    当前:全部
  </button>
  <ul id="select-listbox" role="listbox" aria-labelledby="select-label">
    <li role="option" aria-selected="true">全部</li>
    <li role="option" aria-selected="false">电子</li>
    <li role="option" aria-selected="false">图书</li>
  </ul>
</div>

ARIA 的陷阱是使用错误的值或相互矛盾的属性,这比不用更糟——辅助技术会把冲突信息读给用户。引入 axe 等自动检测工具能拦截大部分低级错误。


三、键盘导航与焦点管理

3.1 焦点管理的三件套

Web 无障碍的"操作"原则依赖键盘。焦点管理有三个核心问题:焦点是否可达、是否可见、是否在预期位置。

  • tabindex:0 让元素可 Tab 聚焦,-1 让元素可通过脚本聚焦但不可 Tab 到达(常用于焦点恢复)。
  • focus-visible:仅键盘焦点时显示轮廓,鼠标点击不显示,避免样式闪烁。
/* 键盘焦点时才显示清晰轮廓 */
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* 下拉触发器的键盘交互 */
.dropdown-trigger:focus-visible {
  box-shadow: 0 0 0 2px var(--color-focus-ring);
}

3.2 弹窗的焦点陷阱

弹窗(Dialog)是焦点管理最常出错的地方。正确行为:打开时焦点移入弹窗、Tab 循环限制在弹窗内、关闭后焦点恢复到触发按钮。React 中常配合 react-focus-lock 或手写守卫:

import { useEffect, useRef } from 'react';

function Dialog({ onClose, children }) {
  const dialogRef = useRef(null);

  useEffect(() => {
    const previousFocus = document.activeElement;
    dialogRef.current.focus(); // 焦点移入弹窗

    const handleKey = (e) => {
      if (e.key === 'Escape') onClose(); // Esc 关闭
    };
    document.addEventListener('keydown', handleKey);

    return () => {
      document.removeEventListener('keydown', handleKey);
      previousFocus.focus(); // 恢复焦点
    };
  }, [onClose]);

  return (
    <div
      ref={dialogRef}
      role="dialog"
      aria-modal="true"
      aria-labelledby="dialog-title"
      tabIndex={-1}
    >
      <h2 id="dialog-title">确认操作</h2>
      {children}
    </div>
  );
}

3.3 跳过链接与 Landmark

长页面键盘用户最痛的是每次进入都要 Tab 过整个导航。Skip Link(跳到主内容)是廉价的解法:

<a class="skip-link" href="#main-content">跳到主要内容</a>
<nav aria-label="主导航">…</nav>
<main id="main-content">…</main>

同时善用 HTML5 Landmark:<main>、<nav>、<aside>、<header>、<footer> 提供了语义地标,让屏幕阅读器用户可以直接跳转到区域。


四、屏幕阅读器:NVDA 与 VoiceOver 的兼容实践

4.1 两类主流屏幕阅读器

屏幕阅读器平台特点
NVDAWindows开源,配合 Firefox/Chrome
VoiceOvermacOS / iOS系统内置,macOS 用 Cmd+F5 开启
TalkBackAndroid安卓系统内置

前端工程师至少应掌握:用 NVDA + Firefox 与 VoiceOver + Safari 各走一遍核心流程。常见差异点:aria-label 的读法、aria-live 区域的触发时机、table 的读法在两种环境可能不同。

4.2 aria-live 的节奏控制

aria-live 用于宣布非焦点触发的动态变化(如"保存成功"提示)。节奏控制很关键:

  • polite:读屏器完成当前朗读后再播报,适合通知类。
  • assertive:立即中断当前朗读,只用于紧急错误。
<!-- 表单提交后的成功提示 -->
<div role="status" aria-live="polite">
  保存成功
</div>

<!-- 校验错误汇总 -->
<div role="alert" aria-live="assertive">
  有 2 个字段填写错误
</div>

4.3 测试的核心误区

用屏幕阅读器测试时常见的误区是"每个控件都要有 aria-label"。事实是:只有原生语义无法表达时才需要 ARIA。一个 <label for="email">邮箱</label> 配 <input id="email"> 已经给出了控件名称,无需再叠加 aria-label。过度添加会让读屏器信息过载——“名称重复读三遍"就是典型症状。

<!-- 正确:原生 label 已提供名称 -->
<label for="email">邮箱</label>
<input id="email" type="email" autocomplete="email" />

<!-- 多余:label 已够用,不应再叠加 aria-label -->
<label for="email">邮箱</label>
<input id="email" type="email" aria-label="邮箱" />

五、色彩对比度与无障碍设计

5.1 对比度的硬性门槛

WCAG 要求普通文本对比度 ≥ 4.5:1,大字号(≥18pt 或 ≥14pt 加粗)≥ 3:1。非文本信息(图标、边框、焦点环)≥ 3:1。设计系统应建立对比度校验:任何新增色板都必须通过自动化比对。

// 用 real 工具校验对比度(概念示例,可接入设计 token)
// 对比度 = (L1 + 0.05) / (L2 + 0.05),L 为相对亮度
function contrastRatio(fg: string, bg: string): number {
  // 简化示意:真实实现需计算 sRGB 相对亮度
  const [r1, g1, b1] = hexToRgb(fg);
  const [r2, g2, b2] = hexToRgb(bg);
  const l1 = relativeLuminance(r1, g1, b1);
  const l2 = relativeLuminance(r2, g2, b2);
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}

5.2 不只颜色传达状态

“以红色表示错误"对色觉障碍用户(全球约 8% 男性)不可靠。错误提示应同时具备非颜色信号:图标、文本描述、role="alert"。

状态仅颜色无障碍版本
错误红字红字 + ⚠ 图标 + 错误文本
必填红色星号红色星号 + aria-required
选中高亮背景高亮背景 + aria-selected
连接状态绿点绿点 + 文本"已连接”

5.3 深色模式与对比度

深色模式引入新的对比度风险:深底上的低饱和色容易低于 4.5:1。设计 token 在深色主题下应有独立的一套色板,并同样过校验,而不是简单取反色。


六、国际化 i18n:i18next 与 react-intl

6.1 两套主流方案的定位

  • i18next:功能全面的通用 i18n 框架,支持嵌套 key、复数列、语言检测、资源加载,配合 react-i18next 用于 React。
  • react-intl(FormatJS):基于 Intl 标准 API,消息用 ICU MessageFormat 语法,类型安全更好,配合 @formatjs/cli 可提取与管理消息。
// react-intl 的 Provider 与消息定义
import { FormattedMessage, useIntl } from 'react-intl';

const messages = {
  'app.hello': '你好,{name}!',
  'app.items': '{count, plural, one {# 个商品} other {# 个商品}}',
};

function Greeting() {
  const intl = useIntl();
  const text = intl.formatMessage({ id: 'app.hello' }, { name: 'Leeting' });
  return <p>{text}</p>;
}

6.2 消息管理与命名规范

i18n 工程化的重点是消息管理:key 的命名规范、消息的提取、翻译文件的同步。推荐做法:

  • key 使用点分命名:page.checkout.submit,避免语义在翻译中丢失。
  • 用工具提取硬编码字符串(react-intl 的 extract、i18next 的 scanner)。
  • 翻译文件按语言组织,构建时打包对应语言。
// locales/zh-CN.json
{
  "page.checkout.submit": "提交订单",
  "page.checkout.confirm": "确认订单信息",
  "page.cart.empty": "购物车是空的"
}

6.3 语言检测与资源加载

语言检测顺序建议:显式选择 > localStorage > navigator.language > 默认语言。注意不要把所有语言一次性打进 bundle,按需加载:

// i18next 按需加载语言资源
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n.use(LanguageDetector).init({
  fallbackLng: 'zh-CN',
  load: 'languageOnly',
  resources: {
    'zh-CN': { translation: zhCN },
    'en': { translation: en },
  },
});

七、复数与 RTL 布局

7.1 复数的陷阱

中文没有词形变化,但英语、俄语、阿拉伯语有复杂的复数规则。永远不要用字符串拼接处理复数。react-intl 的 ICU 语法与 Intl.PluralRules 能正确处理所有语言的复数类别:

import { FormattedMessage } from 'react-intl';

// 正确:交给 ICU 复数规则
<FormattedMessage
  id="app.itemCount"
  values={{ count: 5 }}
/>
{
  "app.itemCount": "{count, plural, =0 {没有商品} one {# 个商品} other {# 个商品}}"
}
// Intl.PluralRules 是真实标准 API,可直接获得复数类别
const rule = new Intl.PluralRules('en-US');
rule.select(1); // 'one'
rule.select(5); // 'other'

7.2 RTL 布局:不只是镜像

阿拉伯语、希伯来语是 RTL(从右到左)语言。RTL 不是简单的镜像:阅读顺序、箭头方向、图标含义、文本对齐都要翻转。工程上:

  • 用逻辑属性 margin-inline-start、padding-inline-end 替代物理属性,让布局自动适配。
  • 用 dir="rtl" 设置文档方向,配合 CSS direction。
  • 图标类组件单独评审方向语义(前进/后退箭头)。
/* 使用逻辑属性,RTL 下自动翻转 */
.card {
  margin-inline-start: 16px;  /* LTR 为 left,RTL 为 right */
  padding-inline-end: 8px;
  text-align: start;           /* 跟随文档方向 */
}

/* 多语言站点的 dir 切换 */
html[dir='rtl'] .arrow-next {
  transform: scaleX(-1); /* 方向性图标翻转 */
}

7.3 布局弹性的验收清单

多语言站点的布局验收清单:文本换行不溢出、RTL 下无镜像错误、长单词(德语复合词)不撑破容器、日期/数字/货币按区域格式显示、Intl.DateTimeFormat 与 Intl.NumberFormat 使用正确:

// 日期与货币按区域格式化
const date = new Intl.DateTimeFormat('ar-EG').format(new Date(2026, 8, 27));
const price = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR',
}).format(1999.5);

八、自动化 a11y 测试:axe 与 lint 集成

8.1 axe-core:规则最全的自动检测

axe-core 由 Deque 开发,覆盖 WCAG 大部分可自动化检测的规则。在 Playwright 中集成:

// Playwright + axe 集成
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';

test('首页无严重可访问性违规', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations.filter((v) => v.impact === 'critical')).toEqual([]);
});

React Testing Library 也可以配合 jest-axe 做组件级检测:

import { render } from '@testing-library/react';
import { axe } from 'jest-axe';

it('组件无可访问性违规', async () => {
  const { container } = render(<NavMenu />);
  expect(await axe(container)).toHaveNoViolations();
});

8.2 lint 层的静态拦截

eslint-plugin-jsx-a11y 在编译前拦截常见的 a11y 反模式(img 无 alt、button 无可访问名、错误 ARIA 用法):

npm i -D eslint-plugin-jsx-a11y
// .eslintrc 或 eslint.config.js
export default [
  {
    plugins: { 'jsx-a11y': jsxA11y },
    rules: {
      'jsx-a11y/alt-text': 'error',
      'jsx-a11y/anchor-has-content': 'error',
      'jsx-a11y/no-autofocus': 'error',
      'jsx-a11y/aria-props': 'error',
    },
  },
];

8.3 自动检测的边界

必须诚实说明:自动化检测只能覆盖约 30-50% 的 WCAG 标准。焦点顺序是否合理、读屏器读起来是否流畅、语音导航是否可用——这些需要人工 + 屏幕阅读器的真实验证。最佳组合是:lint 拦截语法层错误 + axe 扫描 DOM 语义层问题 + 核心页面人工走查。

自动检测能兜住语法层与语义层的低级错误,但「读起来顺不顺」最终只有屏幕阅读器用户能回答——请真实用户走查,而不是只依赖工具。


九、a11y 与 i18n 的落地路线图

9.1 分层落地节奏

阶段动作工具
第一周全站 lint 规则 + axe 扫描eslint-plugin-jsx-a11y、axe
第二周修复 critical 违规,规范语义组件组件库 audit
第三周键盘导航与焦点管理走查人工 + Playwright 焦点测试
第四周屏幕阅读器核心流程走查NVDA / VoiceOver
持续新功能 a11y 检查单进 PR团队规范

9.2 i18n 改造的工程要点

存量项目接 i18n 的务实顺序:

  1. 先抽取界面文案到消息文件,建立语言资源体系。
  2. 接入语言检测与按需加载。
  3. 处理日期、数字、货币的区域化格式。
  4. 支持 RTL 语言(逻辑属性改造)。
  5. 建立翻译质量管理(CI 校验 key 完整性、缺失翻译拦截)。
# CI 中校验翻译 key 完整性
npx @formatjs/cli extract --format simple 'src/**/*.tsx' --out-file messages.json
node scripts/check-translations.js locales/

9.3 一句总结

可访问性不是给少数群体的额外工作,而是产品质量的底线指标:对色觉障碍、键盘用户、读屏用户的友好,往往同时提升了所有用户的可用性——语义化 HTML 对 SEO 友好,清晰的焦点管理对效率用户友好,逻辑属性对国际化友好。a11y 与 i18n 不是孤立模块,它们共同构成"产品面向世界"的基础设施。


结语

本文从 WCAG 2.2 的 POUR 原则出发,覆盖了 ARIA 的正确用法、键盘导航与焦点管理、屏幕阅读器兼容、色彩对比度、i18next/react-intl 国际化、复数与 RTL 布局,以及 axe 自动化测试。核心要点可以浓缩为三条:能用原生语义就用原生语义,ARIA 只在原生不足时补充;对比度与焦点是硬门槛,自动化工具只能兜住一部分;i18n 的本质是尊重每一种语言的形态与阅读方向。

最后给出一个实用的自检问题清单:这个功能能用键盘完成吗?屏幕阅读器能理解它的语义吗?切换到另一种语言或 RTL 后布局还正确吗?当团队把这三个问题写进验收标准,a11y 与 i18n 就从"加分项"变成了"不返工项”。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. CSS 架构与样式方案:从方法论到现代 CSS 新特性
  2. SSR/SSG 渲染模式全景:Next.js App Router、流式渲染与岛屿架构
  3. 前端测试体系:从 Vitest 单元测试到 Playwright E2E 的完整落地