Ctrl K
ComunidadGuías…

scripts/extract-i18n.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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)}`))
}