/** * One-off: add `texts.label` to a list of morfos + corresponding entry in the * langs catalog. Used to batch-fix passive components missing A-1.3 in the * audit. */ import { readFileSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO = join(HERE, '..'); const MORFO_DIR = join(REPO, 'src', 'uix', 'morfo', 'components'); const LANGS_DIR = join(REPO, 'src', 'uix', 'langs', 'components'); interface Entry { kebab: string; label: { es: string; en: string }; } const BATCH: Entry[] = [ { kebab: 'alert-dialog', label: { es: 'Diálogo de alerta', en: 'Alert dialog' } }, { kebab: 'clipboard', label: { es: 'Portapapeles', en: 'Clipboard' } }, { kebab: 'color-picker', label: { es: 'Selector de color', en: 'Color picker' } }, { kebab: 'link-preview', label: { es: 'Vista previa de enlace', en: 'Link preview' } }, { kebab: 'pin-input', label: { es: 'Código PIN', en: 'PIN input' } }, { kebab: 'scroll-area', label: { es: 'Área desplazable', en: 'Scroll area' } }, { kebab: 'date-range-field', label: { es: 'Rango de fechas', en: 'Date range field' } }, { kebab: 'virtual-grid', label: { es: 'Cuadrícula virtual', en: 'Virtual grid' } }, { kebab: 'virtual-list', label: { es: 'Lista virtual', en: 'Virtual list' } }, { kebab: 'time-picker', label: { es: 'Selector de hora', en: 'Time picker' } }, { kebab: 'time-range-field', label: { es: 'Rango de horas', en: 'Time range field' } }, { kebab: 'time-range-picker', label: { es: 'Selector de rango de horas', en: 'Time range picker' } } ]; function addLabelToMorfo(kebab: string): boolean { const path = join(MORFO_DIR, `${kebab}.ts`); const src = readFileSync(path, 'utf8'); if (src.includes(`#?components.${kebab}.label`)) return false; // already has it // Find `texts: {` block const m = src.match(/^\ttexts:\s*\{/m); if (m) { // Insert label as first entry const insertAt = m.index! + m[0].length; const labelLine = `\n\t\tlabel: '#?components.${kebab}.label|${kebab[0].toUpperCase()}${kebab.slice(1).replace(/-(.)/g, (_, c) => ' ' + c.toUpperCase())}',`; const next = src.slice(0, insertAt) + labelLine + src.slice(insertAt); writeFileSync(path, next, 'utf8'); return true; } // No texts block — insert one after `scope:` line const scopeRe = /(\n\tscope:\s*\[[^\]]+\],?\n)/; const sm = src.match(scopeRe); if (!sm) { console.error(`No scope: line in ${path}`); return false; } const label = `\tscope: [...},\n\ttexts: {\n\t\tlabel: '#?components.${kebab}.label|Generated'\n\t},\n`; // Simpler: locate the kebab line and insert texts after it const kebabRe = new RegExp(`(\\n\\tkebab:\\s*'${kebab}'.*?\\n)`); const km = src.match(kebabRe); if (!km) { console.error(`No kebab: '${kebab}' line`); return false; } // Find next significant property after scope to anchor insert const scopeEndRe = /\tscope:\s*\[[^\]]+\][^\n]*\n/; const sem = src.match(scopeEndRe); if (!sem) return false; const start = sem.index! + sem[0].length; const camel = kebab[0].toUpperCase() + kebab.slice(1).replace(/-(.)/g, (_, c) => ' ' + c.toUpperCase()); const textsBlock = `\ttexts: {\n\t\tlabel: '#?components.${kebab}.label|${camel}'\n\t},\n`; const next = src.slice(0, start) + textsBlock + src.slice(start); writeFileSync(path, next, 'utf8'); return true; } function kebabToCamel(kebab: string): string { return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); } function addLabelToLangs(entry: Entry): boolean { const path = join(LANGS_DIR, `${entry.kebab}.ts`); const camel = kebabToCamel(entry.kebab); let src: string; try { src = readFileSync(path, 'utf8'); } catch { // Create new langs file src = `import type { LangNode } from '$libs/langs';\n\n/**\n * Default strings for the ${entry.kebab} component. Merged under\n * \`components.${entry.kebab}.*\` by \`ActiveUix\` (via \`componentLangs\` barrel).\n */\nexport const ${camel}Langs = {\n} satisfies LangNode;\n`; writeFileSync(path, src, 'utf8'); console.log(` + created ${path.replace(REPO, '')}`); // Re-read src = readFileSync(path, 'utf8'); // Add to barrel addToBarrel(entry.kebab, camel); } if (/^\s*label:\s*\{/m.test(src)) return false; // already has it const exportRe = new RegExp(`(export const ${camel}Langs\\s*=\\s*\\{)`); const m = src.match(exportRe); if (!m) { console.error(`No export const ${camel}Langs in ${path}`); return false; } const insertAt = m.index! + m[0].length; const labelBlock = `\n\tlabel: {\n\t\tes: '${entry.label.es}',\n\t\ten: '${entry.label.en}'\n\t},`; const next = src.slice(0, insertAt) + labelBlock + src.slice(insertAt); writeFileSync(path, next, 'utf8'); return true; } function addToBarrel(kebab: string, camel: string): void { const barrelPath = join(LANGS_DIR, 'index.ts'); let src = readFileSync(barrelPath, 'utf8'); if (src.includes(`from './${kebab}'`)) return; // Insert import in alphabetical-ish position by appending before last import const importLine = `import { ${camel}Langs } from './${kebab}';\n`; const lastImportMatch = [...src.matchAll(/^import .* from '\.\/[^']+';$/gm)].pop(); if (lastImportMatch) { const insertAt = lastImportMatch.index! + lastImportMatch[0].length + 1; src = src.slice(0, insertAt) + importLine + src.slice(insertAt); } // Add entry to satisfies block — insert before closing brace const closeBraceMatch = src.match(/\n\} satisfies LangNode;/); if (closeBraceMatch) { const keyName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(kebab) ? kebab : `'${kebab}'`; const entry = `\t${keyName}: ${camel}Langs,\n`; const insertAt = closeBraceMatch.index!; src = src.slice(0, insertAt) + entry + src.slice(insertAt); } writeFileSync(barrelPath, src, 'utf8'); console.log(` + barrel updated for ${kebab}`); } let added = 0; for (const entry of BATCH) { const m = addLabelToMorfo(entry.kebab); const l = addLabelToLangs(entry); if (m || l) { console.log(`✓ ${entry.kebab}${m ? ' [morfo]' : ''}${l ? ' [langs]' : ''}`); added++; } else { console.log(`- ${entry.kebab} (already has label)`); } } console.log(`\nAdded labels to ${added}/${BATCH.length} components.`);