← Lovora

JavaScript's \b Is ASCII-Only: How It Silently Disabled Rules in Six Languages

Last updated: September 27, 2026  |  Lovora is an adults-only (18+) service on which all content is generated by artificial intelligence.

Lovora is a chat application used in nine languages, and a surprising amount of its behaviour depends on regular expressions: recognising that a user said “yes”, detecting that a message asks for a photo, and — in the safety layer — catching stated ages under 18. In September 2026 the same defect was found three times in three different places, each time without an error, a log line or a failing test. A rule simply never fired.

The cause is a property of JavaScript regular expressions that many developers do not expect: \b does not know about Unicode.

1. The behaviour

In JavaScript, a word boundary \b is a position between a character in [A-Za-z0-9_] and one that is not. That definition is fixed: it does not change with the u or v flag. Every accented Latin letter, and every letter of every other script, counts as a non-word character.

/\b(si|sì)\b/i.test('si')        // true
/\b(si|sì)\b/i.test('sì')        // false  — nothing after ì is a boundary
/\b(yes|はい)\b/.test('はい')     // false
/\bâge\b/.test('âge')            // false  — nothing before â is a boundary either
/\b(ok|好的)\b/.test('好的!')    // false

An alternative that ends with a non-ASCII character can never be followed by \b at the end of a string or before a space, and one that starts with a non-ASCII character can never be preceded by one. The pattern reads correctly, compiles, and matches the ASCII alternatives next to it — which is exactly why it goes unnoticed.

2. Three incidents in one codebase

  1. Affirmations in Arabic, Chinese, Japanese and Russian (found September 4). The pattern that recognises a short “yes” had been extended with نعم, 好的, はい and да. None of them had ever matched. The fix anchored the non-Latin alternatives differently — and missed the next case.
  2. Accented Latin (found September 5, same pattern). sì (Italian) and sí (Spanish), the ordinary way to say yes in two of the site’s languages, had never been recognised. The unaccented si worked, which made the rule look healthy in every quick test.
  3. A safety rule (found September 5). A deterministic rule that catches stated ages under 18 closed with \b after the Chinese, Japanese and Arabic words for “years old”, so in those three scripts it could not fire; a French variant had the same defect at the start, before âge. The rule had also only been written around Latin-script openings (“I’m”, “j’ai”, “ich bin”). Both were fixed the same day. The service’s other moderation layers — including an independent classifier model, which does not use these patterns — were not affected; see Safety, Moderation and Enforcement.

Six of the site’s nine languages were affected somewhere: Italian, Spanish, French, Chinese, Japanese and Arabic.

3. The fix

Replace \b with lookarounds over Unicode property classes, and add the u flag:

// before
const YES = /\b(yes|si|sì|sí|oui|ja)\b/i;

// after: a "word character" is any letter or digit in any script
const B_START = '(?<![\\p{L}\\p{N}_])';
const B_END   = '(?![\\p{L}\\p{N}_])';
const YES = new RegExp(`${B_START}(yes|si|sì|sí|oui|ja)${B_END}`, 'iu');

YES.test('sì');   // true
YES.test('sì!');  // true
YES.test('sìx');  // false

Chinese and Japanese need no trailing boundary at all. These scripts do not put spaces between words, so a correct match is usually followed directly by another letter: 15歳です (“I am 15”) has no break after 歳, and even the Unicode lookahead would reject it. Tokens in these scripts are specific enough to match on their own; anchor them only where the grammar requires it.

4. Finding it automatically

Reading the patterns does not work — they look right. A mechanical check does. This one scans a source tree for regex literals and reports the two shapes of the bug:

// find-ascii-boundaries.mjs — usage: node find-ascii-boundaries.mjs src
import fs from 'node:fs';
import path from 'node:path';

const nonAscii = (ch) => ch && ch.codePointAt(0) > 127;
function* files(dir) {
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, e.name);
    if (e.isDirectory() && e.name !== 'node_modules') yield* files(p);
    else if (/\.(m?[jt]sx?)$/.test(e.name)) yield p;
  }
}
for (const f of files(process.argv[2] || '.')) {
  fs.readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
    // 1) an alternative ending in a non-ASCII char right before ")\b"
    for (const m of line.matchAll(/\(([^()]*)\)\\b/g))
      for (const alt of m[1].split('|'))
        if (nonAscii([...alt].pop())) console.log(`${f}:${i + 1}  "${alt}" before )\\b`);
    // 2) \b written directly next to a non-ASCII character
    if (/[^\x00-\x7F]\\b|\\b[^\x00-\x7F]/.test(line)) console.log(`${f}:${i + 1}  \\b touches non-ASCII`);
  });
}

In our codebase this script found one more case — in a rule unrelated to the three above — after all the manual fixes were considered complete.

5. What we changed in how we work

  • Every multilingual pattern is tested with one real sentence per language before it counts as working, including the accented and non-Latin forms, not only the ASCII ones.
  • A pattern that must work in more than one script is written with the Unicode lookarounds from the start; \b is reserved for patterns that are ASCII by design.
  • The scanner above runs over the codebase after any change to a pattern file.

Other languages are not immune. Python’s re module and Java with UNICODE_CHARACTER_CLASS treat Unicode letters as word characters, but many engines and flags do not; check the definition for the engine you use rather than assuming it.

6. Questions people ask

Does \b work with Unicode in JavaScript?

No. In JavaScript, \b is defined only against the ASCII word characters [A-Za-z0-9_], even with the u or v flag. A letter such as é, ì, 好, は or م is treated as a non-word character, so there is never a word boundary between it and the end of the string or a following space.

What is the Unicode-aware replacement for \b in JavaScript?

Use lookarounds with Unicode property escapes and the u flag: (?<![\p{L}\p{N}_]) before a word and (?![\p{L}\p{N}_]) after it. For Chinese and Japanese tokens, drop the trailing boundary entirely: those scripts do not separate words with spaces, so even the Unicode lookahead would reject a correct match such as 15歳です.

Why does /\b(si|sì)\b/ match "si" but not "sì"?

Because ì is not an ASCII word character, the position after it is not a word boundary, so the alternative "sì" can never be followed by \b. The unaccented form still matches, which is what makes the bug hard to notice: the rule appears to work.

How can I find this bug in an existing codebase?

Look for alternatives immediately before a closing )\b whose last character is outside ASCII, and for \b written directly next to a non-ASCII character. A short script that does this is on this page; in our codebase it found a remaining case after all manual fixes were done.

Can this note be quoted?

Yes, under Creative Commons Attribution 4.0 with a link to this page. The code snippets on this page may be used without attribution.

Related documents