diff --git a/package.json b/package.json
index 57cd1e3b..47aef5ae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homepage",
- "version": "1.54.0",
+ "version": "1.54.1",
"private": true,
"type": "module",
"scripts": {
diff --git a/scripts/codemod-fitness-t-to-m.ts b/scripts/codemod-fitness-t-to-m.ts
new file mode 100644
index 00000000..082ac01d
--- /dev/null
+++ b/scripts/codemod-fitness-t-to-m.ts
@@ -0,0 +1,104 @@
+/**
+ * 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(, lang) → t[] 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`);
diff --git a/src/lib/components/fitness/ExercisePicker.svelte b/src/lib/components/fitness/ExercisePicker.svelte
index 628e7b6f..4293d746 100644
--- a/src/lib/components/fitness/ExercisePicker.svelte
+++ b/src/lib/components/fitness/ExercisePicker.svelte
@@ -10,9 +10,10 @@
import Shapes from '@lucide/svelte/icons/shapes';
import Weight from '@lucide/svelte/icons/weight';
import { page } from '$app/state';
- import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
+ import { detectFitnessLang, m } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang(page.url.pathname));
+ const t = $derived(m[lang]);
const isEn = $derived(lang === 'en');
/**
@@ -81,7 +82,7 @@
@@ -154,7 +155,7 @@
{/each}
{#if filtered.length === 0}
-
{t('no_exercises_found', lang)}
+ {t.no_exercises_found}
{/if}
diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte
index 52f0feb8..b3356e84 100644
--- a/src/lib/components/fitness/FoodSearch.svelte
+++ b/src/lib/components/fitness/FoodSearch.svelte
@@ -7,7 +7,7 @@
import ExternalLink from '@lucide/svelte/icons/external-link';
import ScanBarcode from '@lucide/svelte/icons/scan-barcode';
import X from '@lucide/svelte/icons/x';
- import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
+ import { detectFitnessLang, fitnessSlugs, m } from '$lib/js/fitnessI18n';
import MacroBreakdown from './MacroBreakdown.svelte';
/**
@@ -51,9 +51,10 @@
} = $props();
const lang = $derived(detectFitnessLang(page.url.pathname));
+ const t = $derived(m[lang]);
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
- const btnLabel = $derived(confirmLabel ?? t('log_food', lang));
+ const btnLabel = $derived(confirmLabel ?? t.log_food);
// --- Search state ---
let query = $state('');
@@ -434,7 +435,7 @@
{scanError}
{/if}
{#if loading}
- {t('loading', lang)}
+ {t.loading}
{/if}
{#if displayResults.length > 0}
@@ -487,7 +488,7 @@
{/if}
{#if oncancel}
-
+
{/if}
{:else}
@@ -546,7 +547,7 @@
{/if}
-
+
diff --git a/src/lib/components/fitness/MealTypePicker.svelte b/src/lib/components/fitness/MealTypePicker.svelte
index 3628a378..d1794331 100644
--- a/src/lib/components/fitness/MealTypePicker.svelte
+++ b/src/lib/components/fitness/MealTypePicker.svelte
@@ -3,7 +3,7 @@
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import Cookie from '@lucide/svelte/icons/cookie';
- import { t } from '$lib/js/fitnessI18n';
+ import { m } from '$lib/js/fitnessI18n';
/** @type {{ value?: 'breakfast' | 'lunch' | 'dinner' | 'snack', lang?: 'en' | 'de', onchange?: (meal: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void }} */
let {
@@ -11,6 +11,7 @@
lang = 'de',
onchange = () => {},
} = $props();
+ const t = $derived(m[lang]);
/** @type {Array<'breakfast' | 'lunch' | 'dinner' | 'snack'>} */
const mealTypes = ['breakfast', 'lunch', 'dinner', 'snack'];
@@ -32,7 +33,7 @@
class:active={value === meal}
style="--mc: {meta.color}"
onclick={() => { onchange(meal); }}
- title={t(meal, lang)}
+ title={t[meal]}
>
diff --git a/src/lib/components/fitness/PeriodTracker.svelte b/src/lib/components/fitness/PeriodTracker.svelte
index a0a14779..7013c44d 100644
--- a/src/lib/components/fitness/PeriodTracker.svelte
+++ b/src/lib/components/fitness/PeriodTracker.svelte
@@ -1,5 +1,5 @@
-{t('body_parts', lang)} - Bocken
+{t.body_parts} - Bocken
-
-