Address reviewer round-2 fail-closed feedback on 333f6cc:
- Incident doc no longer claims the regression uses @intlify/message-compiler
baseCompile; it states the regression uses vue-i18n's public createI18n/
global.t API recursing nested objects and arrays.
- The placeholder render assertion now runs inside the locale loop, so both
Thai and English are each explicitly asserted to render name@company.com
(previously only the final English locale was asserted after the loop).
Verified: frontend tests 11 passed; production build index-B4oVHpLg.js passed.
111 lines
3.7 KiB
JavaScript
111 lines
3.7 KiB
JavaScript
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')
|
|
const parseJson = relativePath => JSON.parse(read(relativePath))
|
|
|
|
const flattenKeys = (value, prefix = '') => {
|
|
const keys = []
|
|
for (const [key, child] of Object.entries(value)) {
|
|
const fullKey = prefix ? `${prefix}.${key}` : key
|
|
if (child && typeof child === 'object' && !Array.isArray(child)) {
|
|
keys.push(...flattenKeys(child, fullKey))
|
|
} else {
|
|
keys.push(fullKey)
|
|
}
|
|
}
|
|
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'])
|
|
assert.equal(languages.en.label, 'English')
|
|
assert.equal(languages.th.label, 'ไทย')
|
|
})
|
|
|
|
test('Thai and English translation dictionaries have the same keys', () => {
|
|
assert.deepEqual(
|
|
flattenKeys(parseJson('locales/en.json')),
|
|
flattenKeys(parseJson('locales/th.json')),
|
|
)
|
|
})
|
|
|
|
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`,
|
|
)
|
|
}
|
|
// The visible email placeholder must render as a literal email in both
|
|
// locales; vue-i18n must not misparse it as linked-message syntax.
|
|
assert.equal(
|
|
i18n.global.t('auth.emailPlaceholder'),
|
|
'name@company.com',
|
|
`${locale}:auth.emailPlaceholder`,
|
|
)
|
|
}
|
|
})
|
|
|
|
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['"]/)
|
|
assert.match(source, /fallbackLocale:\s*DEFAULT_LOCALE/)
|
|
assert.doesNotMatch(source, /\|\|\s*['"]zh['"]|fallbackLocale:\s*['"]zh['"]/)
|
|
})
|
|
|
|
test('HTML metadata has no Chinese locale or font dependency', () => {
|
|
const html = read('frontend/index.html')
|
|
assert.match(html, /<html\s+lang=["']th["']/)
|
|
assert.doesNotMatch(html, /lang=["']zh|Noto Sans SC|预测|社交媒体|中文/)
|
|
})
|
|
|
|
test('production build text artifacts contain no CJK code points', () => {
|
|
const distRoot = path.join(repoRoot, 'frontend/dist')
|
|
const binaryExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico'])
|
|
const textFiles = []
|
|
|
|
const visit = directory => {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const entryPath = path.join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
visit(entryPath)
|
|
} else if (!binaryExtensions.has(path.extname(entry.name).toLowerCase())) {
|
|
textFiles.push(entryPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
visit(distRoot)
|
|
for (const filePath of textFiles) {
|
|
const content = fs.readFileSync(filePath, 'utf8')
|
|
assert.doesNotMatch(content, /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/, filePath)
|
|
}
|
|
})
|