Files
microfish/frontend/tests/i18n-contract.test.mjs
Kunthawat Greethong 03598561e3 docs+test: align white-screen evidence with public vue-i18n API
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.
2026-09-01 12:15:57 +07:00

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)
}
})