dequelabs / dequelabs/axe-core

label-content-name-mismatch: punctuation normalization is language-dependent — CJK / full-width punctuation is not removed, causing false positives on Chinese, Japanese and Korean pages

Open
#5,308 1 comment 0 reactions 0 assignees View on GitHub
fix label-content-name-mismatch rules
Dominant language
JavaScript
Stars
7.5k
Forks
933
Avg merge
2d 23h
Merged PRs (30d)
17

Description

### Product

axe-core

### Product Version

4.13.0

### Latest Version

- [x] I have tested the issue with the latest version of the product

### Issue Description

## Summary

`isStringContained()` normalizes both strings with `removeUnicode(str, { punctuations: true })`
before comparing, so a visible label and an accessible name that differ **only in punctuation**
are treated as a match.

`getPunctuationRegExp()` only covers ASCII/Latin punctuation. It does not cover CJK punctuation
(U+3000–U+303F) or the full-width forms (U+FF01–U+FF65) that are used in essentially all
Chinese, Japanese and Korean content.

The result: **the same authoring pattern passes in English and fails in Chinese.**

## Environment

- axe-core **4.13.0** (also reproduced on **4.10.3** — long-standing, not a regression)
- Chromium 145.0.7632.6 (Playwright 1.58.2); also reproducible in Chrome DevTools
- Rule: `label-content-name-mismatch` — WCAG 2.5.3 Label in Name, **Level A**

## Steps to reproduce

```html

label-content-name-mismatch: CJK punctuation


Example Corp (new window)


範例公司官網(另開新視窗)

```

```js
await axe.run(document, { runOnly: { type: 'rule', values: ['label-content-name-mismatch'] } });
```

### Expected

Both pass. In both cases the accessible name contains the visible label; only the punctuation
separating the supplementary phrase differs (`(...)` / `, ...` vs `(...)` / `,...`).

### Actual

```json
{ "violations": ["#zh"], "passes": ["#en"] }
```

## Root cause

`curateString()` → `removeUnicode(str, { emoji: true, nonBmp: true, punctuations: true })`
→ `getPunctuationRegExp()`:

```js
function getPunctuationRegExp() {
return /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&\xa3\xa2\xa5\xa7\u20ac()*+,\-.\/:;<=>?@\[\]^_`{|}~\xb1]/g;
}
```

Covers General Punctuation (U+2000–U+206F), Supplemental Punctuation (U+2E00–U+2E7F) and ASCII
punctuation. Not covered:

- **U+3000–U+303F** — CJK Symbols and Punctuation (`、` `。` `「」` `『』` `()` `〈〉` …)
- **U+FF01–U+FF65** — Halfwidth and Fullwidth Forms (`!` `"` `(` `)` `,` `.` `:` `;` …)
- **U+FE10–U+FE1F**, **U+FE30–U+FE4F**, **U+FE50–U+FE6F** — vertical / compatibility / small forms

Verified with the exposed commons API:

```js
const curate = (s) =>
axe.commons.text.sanitize(
axe.commons.text.removeUnicode(s, { emoji: true, nonBmp: true, punctuations: true }));

curate('Example Corp (new window)'); // "Example Corp new window"
curate('Example Corp, new window'); // "Example Corp new window" → contained ✔

curate('範例公司官網(另開新視窗)'); // "範例公司官網(另開新視窗)" ← punctuation kept
curate('範例公司官網,另開新視窗'); // "範例公司官網,另開新視窗" → not contained ✘
```

## Divergence from the ACT rule

ACT rule [`2ee8b8` Visible label is part of accessible name](https://www.w3.org/WAI/standards-guidelines/act/rules/2ee8b8/proposed/)
— the rule axe-core maps to, and which #5203 tracks aligning with — defines the tokenization
step as:

> For each character that either a) represents non-text content, or b) isn’t a letter or a digit:
> replace that character with a space character. […] Use the Unicode general categories
> “L” (Letter) and “N” (Number). (This will exclude hyphens, punctuation, emoji, and more.)

Under that definition, `(`, `)` and `,` are neither L nor N, so both strings tokenize to
`[範例公司官網, 另開新視窗]` and the example **passes**. axe-core's punctuation regex is an
approximation of that step whose coverage happens to depend on the language of the content.

Note that the algorithm's parenthesis-removal step is explicitly scoped to U+0028/U+0029, so the
full-width parentheses are not removed there — they fall out at the letter/digit step quoted above.
The algorithm also applies Unicode case folding and NFKD normalization before tokenizing.

## Impact

WCAG 2.5.3 is **Level A**, so one false positive is enough to fail an automated audit.

The affected pattern — a visible label plus a supplementary phrase in the accessible name, e.g.
`(另開新視窗)` / `(新しいウィンドウで開きます)` ("opens in a new window") — is a standard,
widely-taught convention on CJK government and enterprise sites.

This was found while auditing production sites in a CJK locale, not in a synthetic test.

## Related

- #5203 — get `label-content-name-mismatch` consistent with ACT `2ee8b8` (this issue is one
concrete instance of the divergence)
- #4311 — punctuation handling is *too* forgiving for hyphens (opposite direction, same code path)
- #2128 (closed) — why the unicode regexes are split between punctuation and nonBmp
- #4678 — invisible text wrongly counted as visible text (same rule, different step)

## Possibly also affected

`removeUnicode(..., { punctuations: true })` is also used by `identicalLinksSamePurposeEvaluate`
(`identical-links-same-purpose`, SC 2.4.9) and by `isUnicodeOrPunctuation()` /
`isHumanInterpretable()`. Links differing only by CJK punctuation would not be recognized as
having the same purpose. Not verified in detail.

## Suggested fix

**ACT-aligned** (also resolves #4311, since replacing with a space rather than deleting makes
`non-standard` / `nonstandard` tokenize differently):

```js
// replace anything that is not a Letter or a Number with a space, then tokenize
str.replace(/[^\p{L}\p{N}]/gu, ' ')
```

**Minimal** (keeps current semantics, just closes the language gap):

```js
function getPunctuationRegExp() {
// \p{P} = punctuation, \p{S} = symbols. Both are needed: the current character class
// includes $ + < = > ^ ` | ~ and £ ¢ ¥ ± which are Symbol, not Punctuation.
return /[\p{P}\p{S}]/gu;
}
```

The only characters in the current ranges not covered by `[\p{P}\p{S}]` are U+2000/U+2001
(whitespace, already collapsed by `sanitize()`) and U+206F (a format character, already removed
by `getCategoryFormatRegExp()` in the same `removeUnicode()` call).

⚠️ **Caveat on the minimal option:** `getPunctuationRegExp()` is shared with
`isUnicodeOrPunctuation()` → `isHumanInterpretable()`, so widening it is not local to this
comparison. Strings made only of symbols (`★`, `→`, `✓`) would start curating down to an empty
string, making them "not human interpretable" — which makes this rule return `incomplete` rather
than pass/fail for such labels. The ACT-aligned option avoids that, because the tokenization would
live in the comparison rather than in the shared helper. Either way the change should be run
against the existing test suite; I have only verified the label/name comparison itself.

⚠️ Either way, **full-width letters and digits must not be stripped** (U+FF10–U+FF19,
U+FF21–U+FF3A, U+FF41–U+FF5A) — those are meaningful characters, not punctuation.
`\p{P}` and `\p{L}\p{N}` both handle this correctly.

Both were checked against the following cases; each behaves as expected with either fix:

| visible label | accessible name | current | with either fix |
| --- | --- | --- | --- |
| `範例公司官網(另開新視窗)` | `範例公司官網,另開新視窗` | ✘ violation | ✔ contained |
| `Example Corp (new window)` | `Example Corp, new window` | ✔ contained | ✔ contained (unchanged) |
| `新しいウィンドウで開きます。` | `新しいウィンドウで開きます` | ✘ violation | ✔ contained |
| `全形數字123ABC` | `全形數字123ABC` | ✔ | ✔ (full-width alphanumerics preserved) |

Happy to open a PR with the fix plus test cases if that would help.

Contributor guide

Open the contributing guide

Research direction

Locate getPunctuationRegExp(), removeUnicode(), curateString(), and the label-content-name-mismatch rule and its existing tests. Run the relevant test suite, then compare the ACT-aligned and minimal approaches against the CJK examples, shared helper behavior, and full-width letters and digits. Done means the reported CJK cases pass without regressing existing punctuation or human-interpretable checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
accessibility
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.