* feat: extend UAT checkpoint frame language pack (9 more languages) response_language is a free-form config value, but CHECKPOINT_FRAMES only covered 9 languages — any other configured language silently fell back to the English frame. Add Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian frames plus their aliases, with a regression test asserting each resolves instead of falling back. Follow-up to #2402 (PR #2457). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: add changeset for #2527 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(#2530): list UAT checkpoint frame languages in CONFIGURATION.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(#2530): point changeset fragment at PR #2557 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#2530): address Unicode language-pack review * fix: address checkpoint language review * fix: count spacing combining marks in checkpoint width * test: verify checkpoint aliases structurally * fix: isolate RTL checkpoint frames * fix: isolate RTL checkpoint frames correctly * test(#2530): assert checkpoint aliases neither collide nor go unreachable Review Minor #1. A duplicate alias key was invisible to the existing catalog tests: the runtime object is well-formed after JS collapses the literal, the self-alias assertion still holds, and the losing language just stops resolving. tsc catches the byte-equal case (TS1117), but not the two that survive compilation — an alias whose NFC-lowercase form already belongs to another language, and an alias not in lookup form at all, which resolveCheckpointFrame() can never produce. The check reads the source literal rather than the object, since the object no longer records what was written. Both assertions are independently load-bearing: an NFD twin of an existing alias trips the collision check, an uppercase alias trips the unreachability check. Review Minor #2: changeset retyped Changed -> Added. Nine wholly new supported response_language values are an addition under Keep a Changelog, not a modification of existing behavior. * test(#2530): check alias collisions on the catalog, not its source --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> Co-authored-by: Rezolv <dave@sienkowski.com>
This commit is contained in:
89
src/uat.cts
89
src/uat.cts
@@ -359,6 +359,7 @@ function parseExpectedFromTestBlock(block: string): string | null {
|
||||
interface CheckpointFrame {
|
||||
banner: string;
|
||||
instruction: string;
|
||||
direction?: 'rtl';
|
||||
}
|
||||
|
||||
const CHECKPOINT_BOX_WIDTH = 64; // total column width of the ╔══...╗ border, borders stay byte-identical
|
||||
@@ -400,6 +401,43 @@ const CHECKPOINT_FRAMES: Record<string, CheckpointFrame> = {
|
||||
banner: 'PUNTO DI CONTROLLO: Verifica richiesta',
|
||||
instruction: 'Digita `pass` o descrivi cosa non va.',
|
||||
},
|
||||
dutch: {
|
||||
banner: 'CONTROLEPUNT: Verificatie vereist',
|
||||
instruction: 'Typ `pass` of beschrijf wat er mis is.',
|
||||
},
|
||||
polish: {
|
||||
banner: 'PUNKT KONTROLNY: Wymagana weryfikacja',
|
||||
instruction: 'Wpisz `pass` lub opisz, co jest nie tak.',
|
||||
},
|
||||
russian: {
|
||||
banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка',
|
||||
instruction: 'Введите `pass` или опишите, что не так.',
|
||||
},
|
||||
ukrainian: {
|
||||
banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка',
|
||||
instruction: 'Введіть `pass` або опишіть, що не так.',
|
||||
},
|
||||
turkish: {
|
||||
banner: 'KONTROL NOKTASI: Doğrulama gerekli',
|
||||
instruction: '`pass` yazın veya sorunu açıklayın.',
|
||||
},
|
||||
hindi: {
|
||||
banner: 'चेकपॉइंट: सत्यापन आवश्यक',
|
||||
instruction: '`pass` लिखें या बताएं कि क्या गलत है।',
|
||||
},
|
||||
arabic: {
|
||||
banner: 'نقطة تحقق: المراجعة مطلوبة',
|
||||
instruction: 'اكتب `pass` أو صف المشكلة.',
|
||||
direction: 'rtl',
|
||||
},
|
||||
vietnamese: {
|
||||
banner: 'ĐIỂM KIỂM TRA: Cần xác minh',
|
||||
instruction: 'Nhập `pass` hoặc mô tả vấn đề.',
|
||||
},
|
||||
indonesian: {
|
||||
banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan',
|
||||
instruction: 'Ketik `pass` atau jelaskan apa yang salah.',
|
||||
},
|
||||
};
|
||||
|
||||
// Free-form response_language aliases → canonical CHECKPOINT_FRAMES key.
|
||||
@@ -413,22 +451,30 @@ const CHECKPOINT_LANGUAGE_ALIASES: Record<string, string> = {
|
||||
chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese',
|
||||
korean: 'korean', ko: 'korean', '한국어': 'korean',
|
||||
italian: 'italian', it: 'italian', italiano: 'italian',
|
||||
dutch: 'dutch', nl: 'dutch', nederlands: 'dutch', flemish: 'dutch', vlaams: 'dutch',
|
||||
polish: 'polish', pl: 'polish', polski: 'polish',
|
||||
russian: 'russian', ru: 'russian', 'ru-ru': 'russian', 'русский': 'russian',
|
||||
ukrainian: 'ukrainian', uk: 'ukrainian', ua: 'ukrainian', 'українська': 'ukrainian',
|
||||
turkish: 'turkish', tr: 'turkish', 'türkçe': 'turkish', turkce: 'turkish',
|
||||
hindi: 'hindi', hi: 'hindi', 'हिन्दी': 'hindi', 'हिंदी': 'hindi',
|
||||
arabic: 'arabic', ar: 'arabic', 'العربية': 'arabic',
|
||||
vietnamese: 'vietnamese', vi: 'vietnamese', 'tiếng việt': 'vietnamese', 'tieng viet': 'vietnamese',
|
||||
indonesian: 'indonesian', id: 'indonesian', 'bahasa indonesia': 'indonesian',
|
||||
};
|
||||
|
||||
function resolveCheckpointFrame(responseLanguage: string | undefined): CheckpointFrame {
|
||||
if (!responseLanguage) return CHECKPOINT_FRAMES.english;
|
||||
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()];
|
||||
const key = CHECKPOINT_LANGUAGE_ALIASES[
|
||||
responseLanguage.trim().normalize('NFC').toLowerCase()
|
||||
];
|
||||
return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
|
||||
}
|
||||
|
||||
// Approximate East Asian Width ranges (Unicode property values W and F) — the
|
||||
// CJK scripts CHECKPOINT_FRAMES ships (Japanese/Chinese/Korean) render each
|
||||
// matching code point at 2 terminal/display columns, not 1. Padding computed
|
||||
// from `.length` (UTF-16 code units) undercounts these by one column per
|
||||
// wide character, visually misaligning the box's right border (#2402 review
|
||||
// medium finding). Latin-script frames (English/Spanish/French/German/
|
||||
// Portuguese/Italian) contain no wide code points, so displayWidth === length
|
||||
// for them — no behavior change there.
|
||||
// Approximate terminal-cell width. East Asian Width W/F code points occupy two
|
||||
// cells, while Unicode combining marks occupy no additional cell beyond their
|
||||
// base character. Counting only W/F ranges is insufficient for scripts such as
|
||||
// Devanagari: Hindi vowel signs and viramas are combining marks, and treating
|
||||
// each as a full cell visibly shifts the checkpoint box's right border.
|
||||
function isWideCodePoint(codePoint: number): boolean {
|
||||
return (
|
||||
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
||||
@@ -447,11 +493,17 @@ function isWideCodePoint(codePoint: number): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// Non-spacing/enclosing marks and format controls occupy zero terminal cells.
|
||||
// Spacing combining marks (General_Category=Mc), such as Devanagari vowel
|
||||
// signs, still advance the cursor and must contribute one column.
|
||||
const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u;
|
||||
|
||||
// Iterates by Unicode code point (not UTF-16 code unit) so astral characters
|
||||
// are measured once, not as two surrogate units.
|
||||
function displayWidth(text: string): number {
|
||||
let width = 0;
|
||||
for (const ch of text) {
|
||||
if (ZERO_WIDTH_MARK_RE.test(ch)) continue;
|
||||
width += isWideCodePoint(ch.codePointAt(0) as number) ? 2 : 1;
|
||||
}
|
||||
return width;
|
||||
@@ -468,11 +520,22 @@ function checkpointBoxLine(text: string): string {
|
||||
return `║${padded}║`;
|
||||
}
|
||||
|
||||
const RTL_ISOLATE = '\u2067';
|
||||
const POP_DIRECTIONAL_ISOLATE = '\u2069';
|
||||
|
||||
function isolateCheckpointFrameText(text: string, frame: CheckpointFrame): string {
|
||||
return frame.direction === 'rtl'
|
||||
? `${RTL_ISOLATE}${text}${POP_DIRECTIONAL_ISOLATE}`
|
||||
: text;
|
||||
}
|
||||
|
||||
function buildCheckpoint(currentTest: { number: number; name: string; expected: string }, responseLanguage?: string): string {
|
||||
const frame = resolveCheckpointFrame(responseLanguage);
|
||||
const banner = isolateCheckpointFrameText(frame.banner, frame);
|
||||
const instruction = isolateCheckpointFrameText(frame.instruction, frame);
|
||||
return [
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
checkpointBoxLine(frame.banner),
|
||||
checkpointBoxLine(banner),
|
||||
'╚══════════════════════════════════════════════════════════════╝',
|
||||
'',
|
||||
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
||||
@@ -480,7 +543,7 @@ function buildCheckpoint(currentTest: { number: number; name: string; expected:
|
||||
currentTest.expected,
|
||||
'',
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
frame.instruction,
|
||||
instruction,
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
].join('\n');
|
||||
}
|
||||
@@ -953,5 +1016,9 @@ export = {
|
||||
cmdRenderCheckpoint,
|
||||
parseCurrentTest,
|
||||
buildCheckpoint,
|
||||
CHECKPOINT_FRAMES,
|
||||
CHECKPOINT_LANGUAGE_ALIASES,
|
||||
resolveCheckpointFrame,
|
||||
checkpointBoxLine,
|
||||
parseDeferredItems,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user