150 lines
3.9 KiB
JavaScript
150 lines
3.9 KiB
JavaScript
import {
|
|
closeSync, fstatSync, openSync, opendirSync, readFileSync,
|
|
} from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
const localesDir = path.join(root, 'src/lib/locales')
|
|
const sourcePath = path.join(localesDir, 'en.json')
|
|
|
|
function readCatalog(filePath) {
|
|
return JSON.parse(readUtf8File(filePath))
|
|
}
|
|
|
|
function readUtf8File(filePath) {
|
|
const fd = openSync(filePath, 'r')
|
|
try {
|
|
return readFileSync(fd, 'utf8')
|
|
} finally {
|
|
closeSync(fd)
|
|
}
|
|
}
|
|
|
|
function directoryFiles(dirPath) {
|
|
const dir = opendirSync(dirPath)
|
|
try {
|
|
const files = []
|
|
let entry = dir.readSync()
|
|
while (entry) {
|
|
if (entry.isFile()) files.push(entry.name)
|
|
entry = dir.readSync()
|
|
}
|
|
return files
|
|
} finally {
|
|
dir.closeSync()
|
|
}
|
|
}
|
|
|
|
function ensureDirectory(dirPath) {
|
|
const fd = openSync(dirPath, 'r')
|
|
try {
|
|
if (!fstatSync(fd).isDirectory()) {
|
|
throw new Error(`${dirPath} is not a directory`)
|
|
}
|
|
} finally {
|
|
closeSync(fd)
|
|
}
|
|
}
|
|
|
|
function isFlatObject(value) {
|
|
if (!value) return false
|
|
if (typeof value !== 'object') return false
|
|
return !Array.isArray(value)
|
|
}
|
|
|
|
function assertFlatStringCatalog(locale, catalog) {
|
|
if (!isFlatObject(catalog)) {
|
|
throw new Error(`${locale}: expected a flat object of translation keys`)
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(catalog)) {
|
|
if (typeof value !== 'string') {
|
|
throw new Error(`${locale}: key "${key}" must map to a string`)
|
|
}
|
|
}
|
|
}
|
|
|
|
function missingKeys(sourceKeys, localeKeys) {
|
|
const localeKeySet = new Set(localeKeys)
|
|
return sourceKeys.filter((key) => !localeKeySet.has(key))
|
|
}
|
|
|
|
function extraKeys(sourceKeys, localeKeys) {
|
|
const sourceKeySet = new Set(sourceKeys)
|
|
return localeKeys.filter((key) => !sourceKeySet.has(key))
|
|
}
|
|
|
|
function placeholders(value) {
|
|
return Array.from(value.matchAll(/\{(\w+)\}/g), (match) => match[1]).sort()
|
|
}
|
|
|
|
function sameValues(left, right) {
|
|
if (left.length !== right.length) return false
|
|
return left.every((value, index) => value === right[index])
|
|
}
|
|
|
|
function formatValues(values) {
|
|
return values.length === 0 ? 'none' : values.join(', ')
|
|
}
|
|
|
|
function placeholderIssues(locale, sourceCatalog, catalog) {
|
|
const issues = []
|
|
|
|
for (const [key, sourceValue] of Object.entries(sourceCatalog)) {
|
|
if (!(key in catalog)) continue
|
|
|
|
const sourcePlaceholders = placeholders(sourceValue)
|
|
const localePlaceholders = placeholders(catalog[key])
|
|
if (sameValues(sourcePlaceholders, localePlaceholders)) continue
|
|
|
|
issues.push(
|
|
`${locale}: key "${key}" placeholders differ ` +
|
|
`(expected ${formatValues(sourcePlaceholders)}, found ${formatValues(localePlaceholders)})`,
|
|
)
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
const sourceCatalog = readCatalog(sourcePath)
|
|
assertFlatStringCatalog('en', sourceCatalog)
|
|
|
|
const sourceKeys = Object.keys(sourceCatalog).sort()
|
|
ensureDirectory(localesDir)
|
|
const localeFiles = directoryFiles(localesDir).filter((file) => file.endsWith('.json'))
|
|
const issues = []
|
|
|
|
for (const file of localeFiles) {
|
|
const locale = file.replace(/\.json$/, '')
|
|
const filePath = path.join(localesDir, file)
|
|
const catalog = readCatalog(filePath)
|
|
|
|
assertFlatStringCatalog(locale, catalog)
|
|
|
|
if (locale === 'en') continue
|
|
|
|
const keys = Object.keys(catalog).sort()
|
|
const missing = missingKeys(sourceKeys, keys)
|
|
const extra = extraKeys(sourceKeys, keys)
|
|
|
|
if (missing.length > 0) {
|
|
issues.push(`${locale}: missing ${missing.length} key(s)`)
|
|
}
|
|
if (extra.length > 0) {
|
|
issues.push(`${locale}: extra ${extra.length} key(s)`)
|
|
}
|
|
|
|
issues.push(...placeholderIssues(locale, sourceCatalog, catalog))
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
console.error('Locale validation failed:')
|
|
for (const issue of issues) {
|
|
console.error(`- ${issue}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`Validated ${localeFiles.length} locale catalog(s) against ${sourceKeys.length} English keys.`)
|