Files
homepage/scripts/codemod-fitness-t-to-m.ts
T
Alexander ac05367ee4 refactor(fitness): adopt t.key / t[expr] syntax across fitness pages
22 files migrated from t('key', lang) function calls to direct lookups
on a derived dictionary alias: const t = $derived(m[lang]) once per
file, then t.start_period or t[card.labelKey] at the call sites.

Cleaner read at the point of use, one less argument threaded through,
and TypeScript narrows on every key access (so a typo in a literal
key now errors at the call site, not silently falls back to the key
string).

The codemod handles both ways `lang` can enter scope — derived from
the URL via detectFitnessLang, or destructured from $props() (single
or multi-line). One file aliased the i18n table to `messages` to
avoid collision with a local `const m = data.measurement`.

The deprecated `t(key, lang)` function still exists in fitnessI18n.ts
for any remaining out-of-tree call sites — can be deleted once
nothing imports it.
2026-05-01 12:25:49 +02:00

105 lines
3.5 KiB
TypeScript

/**
* Migrate fitness call sites from t('key', lang) to t.key (or t[expr] for
* dynamic keys), where t = m[lang] derived once per file.
*
* Run: pnpm exec vite-node scripts/codemod-fitness-t-to-m.ts [--dry]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const DRY = process.argv.includes('--dry');
const ROOTS = ['src/routes/fitness', 'src/lib/components/fitness'];
const IMPORT_RE =
/import\s*\{([^}]+)\}\s*from\s*['"]\$lib\/js\/fitnessI18n['"]\s*;?/;
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, out);
else if (extname(p) === '.svelte' || extname(p) === '.ts') out.push(p);
}
return out;
}
function migrate(src: string): { code: string; changed: boolean } {
const m0 = IMPORT_RE.exec(src);
if (!m0) return { code: src, changed: false };
const items = m0[1].split(',').map((s) => s.trim()).filter(Boolean);
if (!items.includes('t')) return { code: src, changed: false };
// 1. Rewrite import: drop `t`, ensure `m` present.
const tIdx = items.indexOf('t');
items.splice(tIdx, 1);
if (!items.includes('m')) items.push('m');
let out = src.replace(IMPORT_RE, `import { ${items.join(', ')} } from '$lib/js/fitnessI18n';`);
// 2. Insert `const t = $derived(m[lang]);` at the right spot, depending
// on how `lang` enters scope.
// Pattern A: `const lang = $derived(...)` — derived from URL
// Pattern B: `let { ... lang ... } = $props()` — passed as prop (single or multi-line)
let inserted = false;
// Pattern A: derived. Allow up to two levels of nested parens inside
// $derived(...) so detectFitnessLang(page.url.pathname) matches.
const langDerivedRe =
/^([ \t]*)(const\s+lang\s*=\s*\$derived\((?:[^()]|\([^()]*\))+\)\s*;?)([ \t]*\n)/m;
if (langDerivedRe.test(out)) {
out = out.replace(langDerivedRe, (_, indent, decl, nl) => {
inserted = true;
return `${indent}${decl}${nl}${indent}const t = $derived(m[lang]);${nl}`;
});
}
// Pattern B: $props() destructure, possibly spanning multiple lines.
// Match any `let { ... } = $props()` and only insert if `lang` is in it.
if (!inserted) {
const propsRe =
/^([ \t]*)(let\s*\{[\s\S]*?\}\s*=\s*\$props\(\)\s*;?)([ \t]*\n)/m;
out = out.replace(propsRe, (full, indent, decl, nl) => {
if (!/\blang\b/.test(decl)) return full;
inserted = true;
return `${indent}${decl}${nl}${indent}const t = $derived(m[lang]);${nl}`;
});
}
if (!inserted) {
console.warn(` WARN: could not auto-insert \`const t = $derived(m[lang])\` — manual fix needed`);
}
// 3. Replace t('static_key', lang) → t.static_key
out = out.replace(
/\bt\(\s*['"]([a-z_][a-z0-9_]*)['"]\s*,\s*lang\s*\)/g,
't.$1'
);
// 4. Replace t(<expr>, lang) → t[<expr>] for any remaining call.
// Expression captured allows up to single-level nested parens, which
// covers our /** @type {FitnessKey} */ (expr) patterns.
out = out.replace(
/\bt\(((?:[^()]|\([^()]*\))+?)\s*,\s*lang\s*\)/g,
(match, expr) => {
const trimmed = expr.trim();
return `t[${trimmed}]`;
}
);
return { code: out, changed: out !== src };
}
let total = 0;
for (const root of ROOTS) {
for (const f of walk(root)) {
const orig = readFileSync(f, 'utf8');
const { code, changed } = migrate(orig);
if (!changed) continue;
if (!DRY) writeFileSync(f, code);
total++;
console.log(` ${f}`);
}
}
console.log(`\n${DRY ? '[dry] ' : ''}${total} files migrated`);