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, / { 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) } })