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",