fix(ui): escape a second live vue-i18n message-compile crash + add a full-sweep test
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EawZPP9iidXj6Tvg3EpG3a
This commit is contained in:
@@ -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<string, unknown>)) {
|
||||
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([])
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user