#5308·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

Author: doinsaneCreated Aug 15, 2026Updated Sep 18, 2026
Labelsfixruleslabel-content-name-mismatch

Product

axe-core

Product Version

4.13.0

Latest Version

  • 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

xml
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>label-content-name-mismatch: CJK punctuation</title></head>
<body>
  <!-- English: visible text and accessible name differ only in punctuation -->
  <a id="en" href="/a" aria-label="Example Corp, new window">Example Corp (new window)</a>

  <!-- Chinese: identical relationship, full-width punctuation -->
  <a id="zh" href="/b" aria-label="範例公司官網,另開新視窗">範例公司官網(另開新視窗)</a>
</body>
</html>
javascript
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():

javascript
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:

javascript
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 — 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):

javascript
// 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):

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