fix: escape @ in login email placeholder (vue-i18n linked syntax)
Production /login rendered a blank page (browser console: SyntaxError: 10 through the vue-i18n parser). Root cause proved with a RED regression (RES: vue-i18n public API reproduces 'Invalid linked format' code 10) plus an independent reviewer: auth.emailPlaceholder="name@company.com" is invalid vue-i18n linked-message syntax, so createI18n() throws a message-compilation SyntaxError while LoginView renders t('auth.emailPlaceholder'). Fix: escape the literal at-sign as name{'@'}company.com in th and en so the message compiles and the visible label is unchanged (name@company.com). Add an all-translations regression that translates every string in th/en (objects and arrays) through vue-i18n's public createI18n/global.t API and asserts the visible placeholder value. Verification: - RED test failed at th:auth.emailPlaceholder (code 10) before the fix. - Independent reviewer verified reproduction + fix, finished PASS. - Frontend tests 11 passed; production build passed (index-B4oVHpLg.js). - Chrome headless rendered the login card, Thai heading, and name@company.com from the production dist. Artifact checksum hash 3621155075b3d9245d2d05511aaf39b1b0cbcaeea local vs server.
This commit is contained in:
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, '../..')
|
||||
const read = relativePath => fs.readFileSync(path.join(repoRoot, relativePath), 'utf8')
|
||||
@@ -20,6 +21,19 @@ const flattenKeys = (value, prefix = '') => {
|
||||
return keys.sort()
|
||||
}
|
||||
|
||||
const flattenStringKeys = (value, prefix = '') => {
|
||||
const keys = []
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||||
if (child && typeof child === 'object') {
|
||||
keys.push(...flattenStringKeys(child, fullKey))
|
||||
} else if (typeof child === 'string') {
|
||||
keys.push(fullKey)
|
||||
}
|
||||
}
|
||||
return keys.sort()
|
||||
}
|
||||
|
||||
test('locale registry exposes only Thai and English', () => {
|
||||
const languages = parseJson('locales/languages.json')
|
||||
assert.deepEqual(Object.keys(languages).sort(), ['en', 'th'])
|
||||
@@ -34,6 +48,26 @@ test('Thai and English translation dictionaries have the same keys', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('every translation message compiles through the vue-i18n public API', () => {
|
||||
const messages = {
|
||||
th: parseJson('locales/th.json'),
|
||||
en: parseJson('locales/en.json'),
|
||||
}
|
||||
const i18n = createI18n({ legacy: false, locale: 'th', messages })
|
||||
|
||||
for (const locale of ['th', 'en']) {
|
||||
i18n.global.locale.value = locale
|
||||
for (const keyPath of flattenStringKeys(messages[locale])) {
|
||||
assert.doesNotThrow(
|
||||
() => i18n.global.t(keyPath),
|
||||
`${locale}:${keyPath} is not valid vue-i18n syntax`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(i18n.global.t('auth.emailPlaceholder'), 'name@company.com')
|
||||
})
|
||||
|
||||
test('frontend i18n uses Thai as the safe default and fallback', () => {
|
||||
const source = read('frontend/src/i18n/index.js')
|
||||
assert.match(source, /DEFAULT_LOCALE\s*=\s*['"]th['"]/)
|
||||
|
||||
Reference in New Issue
Block a user