From 3768395e5920e65ba06de6af7e785e897c6f84d1 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Tue, 8 Sep 2026 16:25:37 +0000 Subject: [PATCH] fix(ui): escape a second live vue-i18n message-compile crash + add a full-sweep test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same class of bug as the Minibits address label (settings.passwordNeedSpecial: "...(!@#$%^&* etc.)" — a bare @ vue-i18n parses as linked-message syntax). This one is live in ChangePasswordSection.vue's password-strength validator: typing a new password with no special character throws this exact SyntaxError the moment the message is rendered. Fixed the same way ({'@'} escaping). Added locales/__tests__/i18nMessagesCompile.test.ts, which walks every string in every locale file and asks the real vue-i18n compiler to parse it — confirmed it fails on both bad strings before their fixes and passes clean now, with no other landmines left in either locale file. This closes the whole bug class rather than just these two instances; a future bad interpolation string fails `npm test` instead of only a live crash report. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EawZPP9iidXj6Tvg3EpG3a --- .../__tests__/i18nMessagesCompile.test.ts | 48 +++++++++++++++++++ neode-ui/src/locales/en.json | 2 +- neode-ui/src/locales/es.json | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 neode-ui/src/locales/__tests__/i18nMessagesCompile.test.ts diff --git a/neode-ui/src/locales/__tests__/i18nMessagesCompile.test.ts b/neode-ui/src/locales/__tests__/i18nMessagesCompile.test.ts new file mode 100644 index 00000000..2cd34f7f --- /dev/null +++ b/neode-ui/src/locales/__tests__/i18nMessagesCompile.test.ts @@ -0,0 +1,48 @@ +// Every message string must survive vue-i18n's message compiler. Found the +// hard way (2026-09-08): a bare `@` in a message is parsed as the start of +// "linked message" syntax (`@:key`), so a literal `@` (an email/handle-style +// placeholder, e.g. "user@example.com") throws a SyntaxError the first time +// it's *rendered*, not at build time — see [[vue-i18n-bare-at-sign-crash]] +// in project memory for the full incident (it blanked a whole modal in both +// the browser and the Android companion's WebView). A literal `@`, `{`, `}` +// or other message-syntax character must be escaped as e.g. `{'@'}`. +// +// This walks every string in every locale file and asks the real compiler +// to parse it — no rendering, no component needed, so it's fast and catches +// the whole class of bug regardless of which component ever ends up using +// the string. +import { describe, it, expect } from 'vitest' +import i18n from '@/i18n' +import en from '../en.json' +import es from '../es.json' + +function collectStrings(obj: unknown, path: string, out: Array<[string, string]>) { + if (typeof obj === 'string') { + out.push([path, obj]) + } else if (obj && typeof obj === 'object') { + for (const [k, v] of Object.entries(obj as Record)) { + collectStrings(v, path ? `${path}.${k}` : k, out) + } + } +} + +describe('locale messages compile', () => { + it.each([ + ['en', en], + ['es', es], + ])('every %s message string compiles under the real vue-i18n compiler', (_locale, messages) => { + const strings: Array<[string, string]> = [] + collectStrings(messages, '', strings) + expect(strings.length).toBeGreaterThan(100) + + const failures: string[] = [] + for (const [path, msg] of strings) { + try { + i18n.global.t(path) + } catch (e) { + failures.push(`${path}: ${(e as Error).message.split('\n')[0]} (source: ${JSON.stringify(msg)})`) + } + } + expect(failures).toEqual([]) + }) +}) diff --git a/neode-ui/src/locales/en.json b/neode-ui/src/locales/en.json index 751bc17b..b7e17f8a 100644 --- a/neode-ui/src/locales/en.json +++ b/neode-ui/src/locales/en.json @@ -315,7 +315,7 @@ "passwordNeedUppercase": "Password must contain at least one uppercase letter", "passwordNeedLowercase": "Password must contain at least one lowercase letter", "passwordNeedDigit": "Password must contain at least one digit", - "passwordNeedSpecial": "Password must contain at least one special character (!@#$%^&* etc.)", + "passwordNeedSpecial": "Password must contain at least one special character (!{'@'}#$%^&* etc.)", "setupFailed": "Setup failed", "verificationFailed": "Verification failed", "disableFailed": "Failed to disable 2FA", diff --git a/neode-ui/src/locales/es.json b/neode-ui/src/locales/es.json index d1ceb5cd..5f9fa831 100644 --- a/neode-ui/src/locales/es.json +++ b/neode-ui/src/locales/es.json @@ -315,7 +315,7 @@ "passwordNeedUppercase": "La contrase\u00f1a debe contener al menos una letra may\u00fascula", "passwordNeedLowercase": "La contrase\u00f1a debe contener al menos una letra min\u00fascula", "passwordNeedDigit": "La contrase\u00f1a debe contener al menos un d\u00edgito", - "passwordNeedSpecial": "La contrase\u00f1a debe contener al menos un car\u00e1cter especial (!@#$%^&* etc.)", + "passwordNeedSpecial": "La contrase\u00f1a debe contener al menos un car\u00e1cter especial (!{'@'}#$%^&* etc.)", "setupFailed": "La configuraci\u00f3n fall\u00f3", "verificationFailed": "La verificaci\u00f3n fall\u00f3", "disableFailed": "Error al deshabilitar 2FA",