Localizing the explanation

explainRegex gives each line a ready-made English text plus a language-neutral desc. For other languages, call formatExplain(desc, messages) with a translated table — it can be partial, and any key you omit falls back to English. No i18n library required; the English text is itself rendered this way, so the two never drift.

import { 
explainRegex
,
formatExplain
} from '@wzo/regex-diagram'
// Zero config — item.text is ready-made English: for (const
it
of
explainRegex
('a+', 'i') ?? [])
console
.
log
(
it
.
text
)
// Localize — hand formatExplain your translated table. // Templates use {placeholder} slots; any key you omit stays English. const
zh
= {
csDigit
: '数字(0-9)',
qExact
: '恰好重复 {n} 次',
qRange
: '重复 {min} 到 {max} 次',
groupNamed
: '捕获组 #{index}(命名为 "{name}")',
// …only the keys you want to translate } for (const
it
of
explainRegex
('(?<y>\\d{2,4})') ?? [])
console
.
log
(
formatExplain
(
it
.
desc
,
zh
))

The message table

Every key formatExplain understands, with its default English template — this is the EXPLAIN_EN table you spread from. {...} slots are filled at render time; override any subset and the rest stay English.

export const EXPLAIN_EN = {
  char: 'the character "{value}"',
  space: 'a space',
  text: 'the text "{value}"',
  csAny: 'any character (except line breaks)',
  csDigit: 'a digit (0-9)',
  csDigitNeg: 'any non-digit',
  csWord: 'a word character (letter, digit, or underscore)',
  csWordNeg: 'a non-word character',
  csSpace: 'a whitespace character',
  csSpaceNeg: 'a non-whitespace character',
  csProperty: 'a character with Unicode property {raw}',
  classEmpty: 'nothing',
  classEmptyNeg: 'any character',
  classOneOf: 'one of: {parts}',
  classExcept: 'any character except: {parts}',
  listSep: ', ',
  classExpr: 'a set-operation character class (see the diagram)',
  anchorStart: 'the start of the string (or line with the m flag)',
  anchorEnd: 'the end of the string (or line with the m flag)',
  wordBoundary: 'a word boundary',
  notWordBoundary: 'a position that is not a word boundary',
  followedBy: 'followed by',
  notFollowedBy: 'not followed by',
  precededBy: 'preceded by',
  notPrecededBy: 'not preceded by',
  qStar: 'repeated zero or more times',
  qPlus: 'repeated one or more times',
  qMin: 'repeated {min} or more times',
  qOptional: 'optional (zero or one time)',
  qExact: 'repeated exactly {n} times',
  qRange: 'repeated {min} to {max} times',
  lazy: '{base} (lazy)',
  groupNamed: 'capturing group #{index} (named "{name}")',
  group: 'capturing group #{index}',
  groupNonCapturing: 'a non-capturing group',
  backref: 'a back-reference',
  backrefTo: 'a back-reference to group #{index}',
  option: 'option {n}',
  withColon: '{text}:',
}

ExplainDesc

The description handed to formatExplain. Switch on kind if you want full control instead of a table.

// (shape simplified; see the .d.ts for the exact discriminated unions)
type ExplainDesc =
  | { kind: 'char', value: string }
  | { kind: 'text', value: string }
  | { kind: 'charset', set: 'any' | 'digit' | 'word' | 'space' | 'property', negate?: boolean, raw?: string }
  | { kind: 'class', negate: boolean, members: ClassMember[] }
  | { kind: 'anchor', at: 'start' | 'end' | 'word', negate?: boolean }
  | { kind: 'lookaround', dir: 'ahead' | 'behind', negate: boolean }
  | { kind: 'quantifier', min: number, max: number, greedy: boolean }
  | { kind: 'group', capturing: boolean, index?: number, name?: string }
  | { kind: 'backref', index?: number }
  | { kind: 'option', n: number }
  // …and 'space' / 'classExpr'