scripts/extract-i18n.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* Extracts translatable strings from `t(editor, '...')` call sites in
* editor/**\/*.ts and merges them into editor/locales/en.json as identity keys
* (English string used as its own key, matching the project's i18n house style).
*
* Usage:
* node scripts/extract-i18n.mjs Add missing keys, report unused ones
* node scripts/extract-i18n.mjs --prune Also remove keys no longer referenced
* node scripts/extract-i18n.mjs --check Fail (exit 1) if any key is missing,
* without writing anything
*/
import { readFileSync, writeFileSync, readdirSync } from 'fs'
import path from 'path'
const root = process.cwd()
const localeFile = path.join(root, 'editor', 'locales', 'en.json')
const checkMode = process.argv.includes('--check')
const pruneMode = process.argv.includes('--prune')
const STRING_LITERAL = '(`(?:[^`\\\\]|\\\\.)*`|\'(?:[^\'\\\\]|\\\\.)*\'|"(?:[^"\\\\]|\\\\.)*")'
// First arg is always some way of referring to the editor instance:
// `editor`, `this.editor`, `config.getEditor()`, etc.
const EDITOR_ARG = '(?:[\\w.]+(?:\\([^()]*\\))?)'
const T_CALL_RE = new RegExp(`\\bt\\(\\s*${EDITOR_ARG}\\s*,\\s*${STRING_LITERAL}`, 'g')
function walk(dir) {
return readdirSync(dir, { withFileTypes: true, recursive: true })
.filter(entry => entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts'))
.map(entry => path.join(entry.parentPath ?? entry.path, entry.name))
}
function unquote(raw) {
const quote = raw[0]
return raw.slice(1, -1).replace(/\\(.)/g, (_, c) => {
if (c === 'n') return '\n'
if (c === 't') return '\t'
if (c === quote || c === '\\') return c
return c
})
}
function extractKeys(files, pattern) {
const keys = new Set()
for (const file of files) {
const content = readFileSync(file, 'utf8')
for (const match of content.matchAll(pattern)) {
keys.add(unquote(match[1]))
}
}
return keys
}
const editorFiles = walk(path.join(root, 'editor'))
const foundKeys = extractKeys(editorFiles, T_CALL_RE)
const existing = JSON.parse(readFileSync(localeFile, 'utf8'))
const existingKeys = new Set(Object.keys(existing))
const missing = [...foundKeys].filter(key => !existingKeys.has(key))
const unused = [...existingKeys].filter(key => !foundKeys.has(key))
if (checkMode) {
if (missing.length) {
console.error(`i18n:extract --check failed: ${missing.length} key(s) referenced in source but missing from en.json:`)
missing.forEach(key => console.error(` - ${JSON.stringify(key)}`))
process.exit(1)
}
console.log('i18n:extract --check passed: en.json is in sync with source.')
process.exit(0)
}
const result = { ...existing }
missing.forEach(key => { result[key] = key })
if (pruneMode) {
unused.forEach(key => { delete result[key] })
}
const sorted = Object.fromEntries(Object.keys(result).sort().map(key => [key, result[key]]))
writeFileSync(localeFile, JSON.stringify(sorted, null, 2) + '\n')
console.log(`i18n:extract: ${missing.length} new key(s) added, ${unused.length} unused key(s) ${pruneMode ? 'pruned' : 'found (run with --prune to remove)'}.`)
if (unused.length && !pruneMode) {
unused.forEach(key => console.log(` - unused: ${JSON.stringify(key)}`))
}