Ctrl K
ComunidadGuías…

scripts/rewrite-i18n-blade.js

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// Version: 0.1.15
const { readdir, readFile, writeFile, stat } = require('node:fs/promises');
const { existsSync } = require('node:fs');
const path = require('node:path');
const process = require('node:process');

// Import shared utilities
const { getIgnorePatterns, shouldIgnoreText } = require('./lib/ignorePatterns');

const projectRoot = path.resolve(__dirname, '..');
const autoDir = path.resolve(projectRoot, 'resources', 'js', 'i18n', 'auto');
const baseLocale = 'en';
const MAX_FILE_SIZE_BYTES = Number(process.env.AI_I18N_MAX_FILE_SIZE || 2 * 1024 * 1024);
const CONCURRENCY = Number(process.env.AI_I18N_CONCURRENCY || 8);

// Initialize ignore patterns from shared utility
getIgnorePatterns(projectRoot);

function normalizeText(text) {
  return String(text || '').replace(/\s+/g, ' ').trim();
}

function shouldTranslateText(text) {
  const trimmed = normalizeText(text);
  return trimmed && !shouldIgnoreText(trimmed);
}

function buildKeyMapFromTranslations(translations) {
  const map = new Map();

  function walk(node, pathSegments) {
    if (!node || typeof node !== 'object' || Array.isArray(node)) {
      return;
    }
    for (const [key, value] of Object.entries(node)) {
      const nextPath = [...pathSegments, key];
      if (typeof value === 'string') {
        if (nextPath.length < 3) continue;
        const normalized = normalizeText(value);
        if (!shouldTranslateText(normalized)) continue;
        const slug = nextPath[nextPath.length - 1];
        const kind = nextPath[nextPath.length - 2];
        const nsSegments = nextPath.slice(0, -2);
        const namespace = nsSegments.join('.');
        const keyId = `${namespace}|${kind}|${normalized}`;
        const fullKey = nextPath.join('.');
        if (!map.has(keyId)) {
          map.set(keyId, fullKey);
        }
      } else {
        walk(value, nextPath);
      }
    }
  }

  walk(translations, []);
  return map;
}

async function listBladeFiles(root) {
  const out = [];
  async function walkDir(dir) {
    const entries = await readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const entryPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (['node_modules', 'vendor', '.git', 'storage', 'bootstrap', 'public'].includes(entry.name)) {
          continue;
        }
        await walkDir(entryPath);
      } else if (entry.isFile()) {
        if (/\.(blade\.php|php)$/i.test(entry.name)) {
          out.push(entryPath);
        }
      }
    }
  }
  if (existsSync(root)) {
    await walkDir(root);
  }
  return out;
}

async function runConcurrent(items, worker, limit = CONCURRENCY) {
  const total = Array.isArray(items) ? items.length : 0;
  if (total === 0) return;
  let index = 0;
  const runners = Array.from({ length: Math.min(limit, total) }, () => (async () => {
    while (true) {
      const i = index;
      index += 1;
      if (i >= total) break;
      const item = items[i];
      try {
        await worker(item);
      } catch (err) {
        console.error('[i18n-rewrite-blade] Worker failed for', item, '-', err && err.message ? err.message : err);
      }
    }
  })());
  await Promise.all(runners);
}

function getNamespaceFromBladeFile(filePath) {
  const viewsRoot = path.resolve(projectRoot, 'resources', 'views');
  let rel = path.relative(viewsRoot, filePath);
  rel = rel.replace(/\.blade\.php$/i, '').replace(/\.php$/i, '');
  const rawSegments = rel.split(path.sep).filter(Boolean);
  const segments = rawSegments
    .map((segment) => segment.replace(/[_\-]+/g, ' '))
    .map((segment) =>
      segment
        .split(/\s+/)
        .filter(Boolean)
        .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
        .join(''),
    )
    .filter(Boolean);
  if (segments.length === 0) return 'Views';
  return segments.join('.');
}

function inferKindFromTagName(name) {
  if (!name) return 'text';
  const lower = name.toLowerCase();
  if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(lower)) return 'heading';
  if (lower === 'label') return 'label';
  if (lower === 'button') return 'button';
  if (lower === 'a' || lower === 'link') return 'link';
  if (lower === 'input' || lower === 'textarea' || lower === 'select') return 'placeholder';
  return 'text';
}

async function processBladeFile(filePath, keyMap) {
  try {
    const s = await stat(filePath);
    if (s && s.size > MAX_FILE_SIZE_BYTES) {
      return false;
    }
  } catch {}
  let code = await readFile(filePath, 'utf8');
  const namespace = getNamespaceFromBladeFile(filePath);

  let template = code.replace(/{{--[\s\S]*?--}}/g, ' ');

  const tagRegex = /<([A-Za-z][A-Za-z0-9-_]*)\b([^>]*)>([^<]+)<\/\1>/g;
  let changed = false;

  template = template.replace(tagRegex, (whole, tagName, attrs, rawText) => {
    if (!rawText || typeof rawText !== 'string') return whole;
    if (rawText.includes("__('") || rawText.includes('__("') || rawText.includes('@lang') || rawText.includes('trans(')) {
      return whole;
    }
    if (rawText.includes('{{')) {
      return whole;
    }
    const normalized = normalizeText(rawText);
    if (!shouldTranslateText(normalized)) return whole;

    const kind = inferKindFromTagName(tagName);
    const keyId = `${namespace}|${kind}|${normalized}`;
    const fullKey = keyMap.get(keyId);
    if (!fullKey) return whole;

    changed = true;

    const leadingSpaceMatch = rawText.match(/^\s*/);
    const trailingSpaceMatch = rawText.match(/\s*$/);
    const leadingSpace = leadingSpaceMatch ? leadingSpaceMatch[0] : '';
    const trailingSpace = trailingSpaceMatch ? trailingSpaceMatch[0] : '';

    const escapedKey = String(fullKey).replace(/"/g, '\\"');
    const wrapped = `${leadingSpace}{{ __("${escapedKey}") }}${trailingSpace}`;
    return `<${tagName}${attrs}>${wrapped}</${tagName}>`;
  });

  if (!changed) {
    return false;
  }

  const viewsRoot = path.resolve(projectRoot, 'resources', 'views');
  const rel = path.relative(viewsRoot, filePath);
  // We operate on the full file content (with Blade comments stripped), so
  // simply write the transformed template back.
  code = template;
  await writeFile(filePath, code, 'utf8');
  console.log(`[i18n-rewrite-blade] Updated ${rel}`);
  return true;
}

async function loadTranslations() {
  const groupedDir = path.resolve(autoDir, baseLocale);
  let hadLocaleReadErrors = false;
  async function readJsonSafe(p) {
    try {
      const raw = await readFile(p, 'utf8');
      return JSON.parse(raw);
    } catch (err) {
      hadLocaleReadErrors = true;
      console.error(`[i18n-rewrite-blade] Failed to read/parse JSON: ${p}`);
      console.error(err?.message || err);
      return null;
    }
  }
  function deepMerge(target, source) {
    if (!source || typeof source !== 'object') return target;
    for (const [k, v] of Object.entries(source)) {
      if (v && typeof v === 'object' && !Array.isArray(v)) {
        if (!target[k] || typeof target[k] !== 'object') target[k] = {};
        deepMerge(target[k], v);
      } else {
        target[k] = v;
      }
    }
    return target;
  }

  let translations = {};
  if (existsSync(groupedDir)) {
    const fsSync = await import('node:fs');
    const stack = [groupedDir];
    while (stack.length) {
      const dir = stack.pop();
      const entries = fsSync.readdirSync(dir, { withFileTypes: true });
      for (const entry of entries) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) stack.push(full);
        else if (entry.isFile() && entry.name.endsWith('.json')) {
          const obj = await readJsonSafe(full);
          if (obj && typeof obj === 'object') deepMerge(translations, obj);
        }
      }
    }
  } else {
    const basePath = path.resolve(autoDir, `${baseLocale}.json`);
    if (!existsSync(basePath)) {
      return null;
    }
    const obj = await readJsonSafe(basePath);
    if (obj && typeof obj === 'object') translations = obj;
  }

  if (hadLocaleReadErrors) {
    console.error('[i18n-rewrite-blade] Aborting: one or more locale JSON files could not be parsed. No source files were modified.');
    return null;
  }

  return translations;
}

async function main() {
  if (!existsSync(autoDir)) {
    console.error('[i18n-rewrite-blade] Auto locale directory not found:', autoDir);
    process.exit(1);
  }

  const translations = await loadTranslations();
  if (!translations || typeof translations !== 'object') {
    console.error('[i18n-rewrite-blade] No translations available for base locale. Run extract first.');
    process.exit(1);
  }

  const keyMap = buildKeyMapFromTranslations(translations);
  if (keyMap.size === 0) {
    console.log('[i18n-rewrite-blade] No candidate translations found. Nothing to rewrite.');
    process.exit(0);
  }

  const viewsRoot = path.resolve(projectRoot, 'resources', 'views');
  if (!existsSync(viewsRoot)) {
    console.log('[i18n-rewrite-blade] resources/views not found. Nothing to rewrite.');
    process.exit(0);
  }

  const files = await listBladeFiles(viewsRoot);
  if (!files.length) {
    console.log('[i18n-rewrite-blade] No Blade/PHP view files found to rewrite.');
    process.exit(0);
  }

  let changedCount = 0;
  await runConcurrent(files, async (file) => {
    const changed = await processBladeFile(file, keyMap);
    if (changed) changedCount += 1;
  }, CONCURRENCY);

  console.log(`[i18n-rewrite-blade] Completed. Updated ${changedCount} file(s).`);
}

main().catch((err) => {
  const msg = err && err.message ? err.message : err;
  console.error('[i18n-rewrite-blade] Failed:', msg);
  process.exit(1);
});