You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/scripts/migrate-translations-to-tex...

194 lines
6.7 KiB

/**
* One-off migration: morfo.translations → morfo.texts + per-component langs.
*
* For each `src/uix/morfo/components/*.ts` whose declaration includes a
* `translations: { ... }` block:
*
* 1. Extract the literal block as JS data.
* 2. Write `src/uix/langs/components/{kebab}.ts` exporting `{camelCase}Langs`
* with the same data, typed as `LangNode`.
* 3. Replace the morfo's `translations: { ... }` with `texts: { ... }` where
* every leaf becomes an idlangref `'#?components.{kebab}.{path}|<fallback>'`
* keyed by the dotted path.
* 4. Regenerate `src/uix/langs/components/index.ts` so it spreads every
* per-component module under its kebab key into `componentLangs`.
*
* The translation blocks in current morfos are pure literal data — nested
* objects with leaf shape `{ es, en, ... }`. No `v.*` builders or expressions
* appear inside. That lets us rely on a brace-balanced text walker instead of
* a TS AST.
*/
import { readdirSync, 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 LeafRecord {
readonly [locale: string]: string;
}
type LangValue = LeafRecord | { [key: string]: LangValue };
function isLeaf(node: LangValue): node is LeafRecord {
return Object.values(node).every((v) => typeof v === 'string');
}
/** Find the `translations: { … }` block opening offset (after the `{`). */
function findTranslationsBlock(source: string): { start: number; end: number } | null {
const match = source.match(/^\ttranslations:\s*\{/m);
if (!match) return null;
const openBrace = match.index! + match[0].length - 1; // index of '{'
let depth = 1;
let i = openBrace + 1;
while (i < source.length && depth > 0) {
const ch = source[i];
if (ch === '{') depth++;
else if (ch === '}') depth--;
i++;
}
if (depth !== 0) throw new Error('Unbalanced braces in translations block');
return { start: match.index!, end: i };
}
function evalLiteralBlock(snippet: string): LangValue {
// snippet looks like: `{\n\t\tkey: { es: '…', en: '…' },\n\t\t…\n\t}`
// Strip TS comments (// and /* */) and trailing commas to make it valid JS.
const cleaned = snippet
.replace(/\/\/[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/,(\s*[}\]])/g, '$1');
// Use Function to evaluate as JS object literal.
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const fn = new Function(`return (${cleaned});`);
return fn() as LangValue;
}
function flattenLeaves(
node: LangValue,
prefix: string[] = []
): Array<{ path: string[]; leaf: LeafRecord }> {
if (isLeaf(node)) return [{ path: prefix, leaf: node }];
return Object.entries(node).flatMap(([k, v]) => flattenLeaves(v as LangValue, [...prefix, k]));
}
function pickFallback(leaf: LeafRecord): string {
return leaf.en ?? leaf.es ?? Object.values(leaf)[0] ?? '';
}
function kebabToCamel(kebab: string): string {
return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
function renderLangsTs(kebab: string, data: LangValue): string {
const camel = kebabToCamel(kebab);
const body = JSON.stringify(data, null, '\t').replace(/"([a-zA-Z_$][\w$-]*)":/g, (_, k) =>
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? `${k}:` : `'${k}':`
);
return `import type { LangNode } from '$libs/langs';
/**
* Default strings for the ${kebab} component. Merged under
* \`components.${kebab}.*\` by \`ActiveUix\` (via \`componentLangs\` barrel).
*
* Morfo references entries by absolute idlangref:
* \`'#?components.${kebab}.{key}|fallback'\`.
*/
export const ${camel}Langs = ${body} satisfies LangNode;
`;
}
function renderTextsBlock(kebab: string, data: LangValue, indent: string): string {
const leaves = flattenLeaves(data);
const lines = leaves.map(({ path, leaf }) => {
const dotted = path.join('.');
const fallback = pickFallback(leaf);
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(dotted) ? dotted : `'${dotted}'`;
return `${indent}\t${key}: '#?components.${kebab}.${dotted}|${fallback}'`;
});
return `${indent}texts: {\n${lines.join(',\n')}\n${indent}}`;
}
function detectKebab(source: string): string {
const m = source.match(/kebab:\s*'([^']+)'/);
if (!m) throw new Error('No kebab field');
return m[1];
}
function migrateFile(path: string): { kebab: string; data: LangValue } | null {
const source = readFileSync(path, 'utf8');
const block = findTranslationsBlock(source);
if (!block) return null;
const kebab = detectKebab(source);
const fullBlock = source.slice(block.start, block.end); // includes "translations: {…}"
const innerStart = fullBlock.indexOf('{');
const innerSnippet = fullBlock.slice(innerStart); // "{ … }"
const data = evalLiteralBlock(innerSnippet);
// Indent of the original block — match the leading tab(s) before "translations:"
const indentMatch = fullBlock.match(/^(\t*)translations:/);
const indent = indentMatch?.[1] ?? '\t';
const replacement = renderTextsBlock(kebab, data, indent);
const newSource = source.slice(0, block.start) + replacement + source.slice(block.end);
writeFileSync(path, newSource, 'utf8');
// Write langs/components/{kebab}.ts
const langsPath = join(LANGS_DIR, `${kebab}.ts`);
writeFileSync(langsPath, renderLangsTs(kebab, data), 'utf8');
return { kebab, data };
}
function regenerateBarrel(kebabs: string[]): void {
const sorted = [...kebabs].sort();
const imports = sorted.map((k) => `import { ${kebabToCamel(k)}Langs } from './${k}';`).join('\n');
const entries = sorted
.map((k) => {
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `'${k}'`;
return `\t${key}: ${kebabToCamel(k)}Langs`;
})
.join(',\n');
const body = `import type { LangNode } from '$libs/langs';
${imports}
/**
* Per-component default strings. Each morfo that uses \`texts:\` with
* idlangrefs under \`components.{kebab}.*\` ships an entry here. Merged into
* the active langs catalog by \`ActiveUix\`.
*/
export const componentLangs = {
${entries}
} satisfies LangNode;
`;
writeFileSync(join(LANGS_DIR, 'index.ts'), body, 'utf8');
}
function main() {
const files = readdirSync(MORFO_DIR)
.filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'))
.map((f) => join(MORFO_DIR, f));
const migrated: string[] = [];
for (const file of files) {
try {
const result = migrateFile(file);
if (result) migrated.push(result.kebab);
} catch (err) {
console.error(`FAIL ${file}: ${(err as Error).message}`);
throw err;
}
}
regenerateBarrel(migrated);
console.log(`Migrated ${migrated.length} components:`);
for (const k of migrated.sort()) console.log(` - ${k}`);
}
main();

Powered by TurnKey Linux.