mirror of
https://github.com/BeamMP/BeamMP-Website.git
synced 2026-10-11 19:14:27 +00:00
134 lines
4.7 KiB
JavaScript
134 lines
4.7 KiB
JavaScript
/**
|
|
* The site is shown in seven languages from seven JSON files. Nothing else
|
|
* notices when one of them falls behind, so this does: every language has
|
|
* every English text, nothing is blank, translations keep the same {placeholders}
|
|
* as English, and every text a page asks for exists.
|
|
*/
|
|
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { LANGUAGES, SUPPORT_LOCALES } from '@/i18n'
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
const localesDir = path.join(root, 'src/locales')
|
|
const read = (code) => JSON.parse(fs.readFileSync(path.join(localesDir, `${code}.json`), 'utf8'))
|
|
|
|
function flatten(obj, prefix = '') {
|
|
const out = {}
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const full = prefix ? `${prefix}.${key}` : key
|
|
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
Object.assign(out, flatten(value, full))
|
|
else out[full] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Texts that are known to be untranslated, so the test can pass today and still
|
|
// catch anything new. Translate one and delete it from here: the "still
|
|
// missing" test below fails if an entry is left behind.
|
|
const KNOWN_UNTRANSLATED = {}
|
|
|
|
const english = flatten(read('en'))
|
|
const others = SUPPORT_LOCALES.filter((code) => code !== 'en')
|
|
|
|
describe('the language list', () => {
|
|
test('every supported language has a file, and every file is a supported language', () => {
|
|
const files = fs
|
|
.readdirSync(localesDir)
|
|
.filter((f) => f.endsWith('.json'))
|
|
.map((f) => f.replace('.json', ''))
|
|
expect(files.sort()).toEqual([...SUPPORT_LOCALES].sort())
|
|
})
|
|
|
|
test('every supported language has a name and a flag for the picker', () => {
|
|
for (const code of SUPPORT_LOCALES) {
|
|
expect(LANGUAGES[code]).toMatchObject({
|
|
code,
|
|
name: expect.any(String),
|
|
flag: expect.any(String),
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
describe.each(others)('%s translation', (code) => {
|
|
const messages = flatten(read(code))
|
|
const allowedMissing = (key) => (KNOWN_UNTRANSLATED[key] || []).includes(code)
|
|
|
|
test('has every English text', () => {
|
|
const missing = Object.keys(english).filter((key) => !(key in messages) && !allowedMissing(key))
|
|
expect(missing).toEqual([])
|
|
})
|
|
|
|
test('has no text English does not have (a leftover or a typo in the name)', () => {
|
|
expect(Object.keys(messages).filter((key) => !(key in english))).toEqual([])
|
|
})
|
|
|
|
test('has no blank texts', () => {
|
|
const blank = Object.entries(messages).filter(
|
|
([, value]) => typeof value !== 'string' || !value.trim()
|
|
)
|
|
expect(blank.map(([key]) => key)).toEqual([])
|
|
})
|
|
|
|
test('keeps the same {placeholders} as English', () => {
|
|
const placeholders = (text) => [...String(text).matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort()
|
|
const mismatched = Object.keys(messages)
|
|
.filter((key) => key in english)
|
|
.filter(
|
|
(key) =>
|
|
JSON.stringify(placeholders(messages[key])) !== JSON.stringify(placeholders(english[key]))
|
|
)
|
|
expect(mismatched).toEqual([])
|
|
})
|
|
})
|
|
|
|
test('every entry in the known-untranslated list is still untranslated', () => {
|
|
const stale = []
|
|
for (const [key, codes] of Object.entries(KNOWN_UNTRANSLATED)) {
|
|
if (!(key in english)) stale.push(`${key} (not in English)`)
|
|
for (const code of codes)
|
|
if (key in flatten(read(code))) stale.push(`${key} (${code} has it now)`)
|
|
}
|
|
expect(stale).toEqual([])
|
|
})
|
|
|
|
describe('texts the pages ask for', () => {
|
|
const sourceFiles = []
|
|
const walk = (dir) => {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) walk(full)
|
|
else if (/\.(vue|js)$/.test(entry.name)) sourceFiles.push(full)
|
|
}
|
|
}
|
|
walk(path.join(root, 'src'))
|
|
|
|
const used = new Map()
|
|
const dynamic = []
|
|
for (const file of sourceFiles) {
|
|
const source = fs.readFileSync(file, 'utf8')
|
|
for (const m of source.matchAll(/(?<![\w.])\$?(?:t|te|tm|rt)\(\s*(['"`])([^'"`]+)\1/g)) {
|
|
if (m[2].includes('${')) dynamic.push(`${path.relative(root, file)}: ${m[2]}`)
|
|
else used.set(m[2], path.relative(root, file))
|
|
}
|
|
}
|
|
|
|
test('finds the texts the pages use (the scan itself works)', () => {
|
|
expect(used.size).toBeGreaterThan(50)
|
|
})
|
|
|
|
test('every text a page asks for exists in English', () => {
|
|
const missing = [...used].filter(
|
|
([key]) => !(key in english) && !Object.keys(english).some((k) => k.startsWith(`${key}.`))
|
|
)
|
|
expect(missing.map(([key, file]) => `${key} (${file})`)).toEqual([])
|
|
})
|
|
|
|
test('no page builds a text name on the fly, which this check could not see', () => {
|
|
expect(dynamic).toEqual([])
|
|
})
|
|
})
|