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.
This commit is contained in:
2026-05-01 12:25:49 +02:00
parent 609405da81
commit ac05367ee4
24 changed files with 561 additions and 435 deletions
@@ -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 @@
<div class="picker-backdrop" onclick={onClose}></div>
<div class="picker-panel">
<div class="picker-header">
<h2>{t('picker_title', lang)}</h2>
<h2>{t.picker_title}</h2>
<button class="close-btn" onclick={onClose} aria-label="Close">
<X size={20} />
</button>
@@ -91,7 +92,7 @@
<Search size={16} />
<input
type="text"
placeholder={t('search_exercises', lang)}
placeholder={t.search_exercises}
bind:value={query}
/>
</div>
@@ -154,7 +155,7 @@
</li>
{/each}
{#if filtered.length === 0}
<li class="no-results">{t('no_exercises_found', lang)}</li>
<li class="no-results">{t.no_exercises_found}</li>
{/if}
</ul>
</div>
+7 -6
View File
@@ -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 @@
<input
type="text"
class="fs-search-input"
placeholder={t('search_food', lang)}
placeholder={t.search_food}
bind:value={query}
oninput={doSearch}
autofocus={autofocus}
@@ -455,7 +456,7 @@
<p class="fs-scan-error">{scanError}</p>
{/if}
{#if loading}
<p class="fs-status">{t('loading', lang)}</p>
<p class="fs-status">{t.loading}</p>
{/if}
{#if displayResults.length > 0}
<div class="fs-results">
@@ -487,7 +488,7 @@
</div>
{/if}
{#if oncancel}
<button class="fs-btn-cancel" onclick={oncancel}>{t('cancel', lang)}</button>
<button class="fs-btn-cancel" onclick={oncancel}>{t.cancel}</button>
{/if}
{:else}
<!-- Selected food — detail & amount -->
@@ -546,7 +547,7 @@
{/if}
<div class="fs-actions">
<button class="fs-btn-cancel" onclick={() => { selected = null; }}>{t('cancel', lang)}</button>
<button class="fs-btn-cancel" onclick={() => { selected = null; }}>{t.cancel}</button>
<button class="fs-btn-confirm" onclick={confirm}>{btnLabel}</button>
</div>
</div>
@@ -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]}
>
<MealIcon size={14} />
</button>
+51 -50
View File
@@ -1,5 +1,5 @@
<script>
import { t } from '$lib/js/fitnessI18n';
import { m } from '$lib/js/fitnessI18n';
import Trash2 from '@lucide/svelte/icons/trash-2';
import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -17,6 +17,7 @@
* @type {{ periods: any[], lang: 'en' | 'de', sharedWith?: string[], readOnly?: boolean, ownerName?: string, mode?: 'entry' | 'projection' | 'full' }}
*/
let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '', mode = 'full' } = $props();
const t = $derived(m[lang]);
const showEntry = $derived(mode !== 'projection');
const showProjection = $derived(mode !== 'entry');
@@ -116,7 +117,7 @@
const startDay = start.toLocaleDateString(locale, { weekday: 'long' });
const endDay = end.toLocaleDateString(locale, { weekday: 'long' });
const diffDays = Math.round((midnight(start) - todayMidnight) / 86400000);
const toWord = t('to', lang);
const toWord = t.to;
if (diffDays >= 0 && diffDays < 7) {
return lang === 'de'
@@ -653,7 +654,7 @@
/** @param {string} id */
async function deletePeriod(id) {
if (!await confirm(t('delete_period_confirm', lang))) return;
if (!await confirm(t.delete_period_confirm)) return;
try {
const res = await fetch(`/api/fitness/period/${id}`, { method: 'DELETE' });
if (res.ok) {
@@ -709,7 +710,7 @@
{ownerName}
</span>
{:else}
{t('period_tracker', lang)}
{t.period_tracker}
{/if}
</h2>
@@ -718,29 +719,29 @@
{#if ongoing}
<div class="status-split">
<div class="status-main">
<span class="status-pill period-pill">{t('current_period', lang)}</span>
<span class="status-hero ongoing-hero">{t('period_day', lang)} {ongoingDay}</span>
<span class="status-pill period-pill">{t.current_period}</span>
<span class="status-hero ongoing-hero">{t.period_day} {ongoingDay}</span>
{#if showProjection && predictions.predictedEndOfOngoing}
<span class="status-detail">{t('predicted_end', lang)}</span>
<span class="status-detail">{t.predicted_end}</span>
<span class="status-relative">{relativeDate(predictions.predictedEndOfOngoing)}</span>
<span class="status-date">{formatDate(predictions.predictedEndOfOngoing)}</span>
{/if}
{#if showEntry && !readOnly}
<button class="end-btn" onclick={endPeriod} disabled={loading}>
<span class="end-btn-icon"><Check size={18} strokeWidth={2.5} /></span>
<span class="end-btn-label">{t('end_period', lang)}</span>
<span class="end-btn-label">{t.end_period}</span>
</button>
{/if}
</div>
{#if showProjection && nextCycle}
<div class="status-side">
<div class="status-side-item ovulation-accent">
<span class="status-side-label">{t('ovulation', lang)}</span>
<span class="status-side-label">{t.ovulation}</span>
<span class="status-side-relative">{relativeDate(nextCycle.fertileEnd)}</span>
<span class="status-side-date">{formatDate(nextCycle.fertileEnd)}</span>
</div>
<div class="status-side-item fertile-accent">
<span class="status-side-label">{t('fertile', lang)}</span>
<span class="status-side-label">{t.fertile}</span>
<span class="status-side-date">{formatDate(nextCycle.fertileStart)}{formatDate(nextCycle.fertileEnd)}</span>
</div>
</div>
@@ -749,33 +750,33 @@
{:else if showProjection && nextCycle}
<div class="status-split">
<div class="status-main">
<span class="status-pill period-pill">{t('next_period', lang)}</span>
<span class="status-pill period-pill">{t.next_period}</span>
<span class="status-hero">{relativeRange(nextCycle.start, nextCycle.end)}</span>
<span class="status-date">{formatDate(nextCycle.start)}{formatDate(nextCycle.end)}</span>
{#if showEntry && !readOnly}
<button class="start-btn" onclick={startPeriod} disabled={loading}>
{t('start_period', lang)}
{t.start_period}
</button>
{/if}
</div>
<div class="status-side">
<div class="status-side-item ovulation-accent">
<span class="status-side-label">{t('ovulation', lang)}</span>
<span class="status-side-label">{t.ovulation}</span>
<span class="status-side-relative">{relativeDate(nextCycle.fertileEnd)}</span>
<span class="status-side-date">{formatDate(nextCycle.fertileEnd)}</span>
</div>
<div class="status-side-item fertile-accent">
<span class="status-side-label">{t('fertile', lang)}</span>
<span class="status-side-label">{t.fertile}</span>
<span class="status-side-date">{formatDate(nextCycle.fertileStart)}{formatDate(nextCycle.fertileEnd)}</span>
</div>
</div>
</div>
{:else if showEntry}
<div class="status-block">
<span class="status-empty">{sorted.length === 0 ? t('no_period_data', lang) : t('no_active_period', lang)}</span>
<span class="status-empty">{sorted.length === 0 ? t.no_period_data : t.no_active_period}</span>
{#if !readOnly}
<button class="start-btn" onclick={startPeriod} disabled={loading}>
{t('start_period', lang)}
{t.start_period}
</button>
{/if}
</div>
@@ -817,10 +818,10 @@
<div class="cal-legend">
<span class="legend-item"><span class="legend-dot period"></span> {lang === 'de' ? 'Periode' : 'Period'}</span>
<span class="legend-item"><span class="legend-dot predicted"></span> {lang === 'de' ? 'Prognose' : 'Predicted'}</span>
<span class="legend-item"><span class="legend-dot fertile"></span> {t('fertile', lang)}</span>
<span class="legend-item"><span class="legend-dot peak-fertile"></span> {t('peak_fertility', lang)}</span>
<span class="legend-item"><span class="legend-dot ovulation"></span> {t('ovulation', lang)}</span>
<span class="legend-item"><span class="legend-dot luteal"></span> {t('luteal_phase', lang)}</span>
<span class="legend-item"><span class="legend-dot fertile"></span> {t.fertile}</span>
<span class="legend-item"><span class="legend-dot peak-fertile"></span> {t.peak_fertility}</span>
<span class="legend-item"><span class="legend-dot ovulation"></span> {t.ovulation}</span>
<span class="legend-item"><span class="legend-dot luteal"></span> {t.luteal_phase}</span>
</div>
</div>
@@ -829,17 +830,17 @@
{#if showProjection && completed.length >= 2}
<div class="cycle-stats">
<div class="cycle-stat">
<span class="cycle-stat-label">{t('cycle_length', lang)}</span>
<span class="cycle-stat-value">{Math.round(predictions.avgCycle)} {t('days', lang)}</span>
<span class="cycle-stat-label">{t.cycle_length}</span>
<span class="cycle-stat-value">{Math.round(predictions.avgCycle)} {t.days}</span>
{#if predictions.cycleVariance > 0}
<span class="cycle-stat-variance">± {predictions.cycleVariance} {t('days', lang)} (95% CI)</span>
<span class="cycle-stat-variance">± {predictions.cycleVariance} {t.days} (95% CI)</span>
{/if}
</div>
<div class="cycle-stat">
<span class="cycle-stat-label">{t('period_length', lang)}</span>
<span class="cycle-stat-value">{Math.round(predictions.avgPeriod)} {t('days', lang)}</span>
<span class="cycle-stat-label">{t.period_length}</span>
<span class="cycle-stat-value">{Math.round(predictions.avgPeriod)} {t.days}</span>
{#if predictions.periodVariance > 0}
<span class="cycle-stat-variance">± {predictions.periodVariance} {t('days', lang)} (95% CI)</span>
<span class="cycle-stat-variance">± {predictions.periodVariance} {t.days} (95% CI)</span>
{/if}
</div>
</div>
@@ -851,13 +852,13 @@
<div class="history">
<div class="history-share-row">
<button class="history-toggle" onclick={() => showHistory = !showHistory}>
<h3>{t('history', lang)}</h3>
<h3>{t.history}</h3>
<ChevronRight size={14} class={showHistory ? 'chevron open' : 'chevron'} />
</button>
<div class="share-bar">
{#if shareList.length > 0}
<div class="shared-avatars">
<span class="shared-label">{t('shared_with', lang)}</span>
<span class="shared-label">{t.shared_with}</span>
{#each shareList as user}
<div class="shared-avatar" title={user}>
<ProfilePicture username={user} size={28} />
@@ -865,7 +866,7 @@
{/each}
</div>
{/if}
<button class="share-btn" onclick={() => showShare = true} aria-label={t('share', lang)}>
<button class="share-btn" onclick={() => showShare = true} aria-label={t.share}>
<UserPlus size={16} />
</button>
</div>
@@ -875,7 +876,7 @@
<div class="history-header">
<button class="add-past-btn" onclick={() => showAddForm = !showAddForm}>
<Plus size={14} />
{t('add_past_period', lang)}
{t.add_past_period}
</button>
</div>
@@ -883,20 +884,20 @@
<div class="add-form">
<div class="add-row">
<label>
{t('period_start', lang)}
{t.period_start}
<DatePicker bind:value={addStart} max={todayStr} {lang} />
</label>
<label>
{t('period_end', lang)}
{t.period_end}
<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
</label>
</div>
<div class="add-actions">
<button class="save-btn" onclick={addPastPeriod} disabled={!addStart || loading}>
{t('save', lang)}
{t.save}
</button>
<button class="cancel-btn" onclick={() => { showAddForm = false; addStart = ''; addEnd = ''; }}>
{t('cancel', lang)}
{t.cancel}
</button>
</div>
</div>
@@ -908,20 +909,20 @@
<div class="history-item editing">
<div class="add-row">
<label>
{t('period_start', lang)}
{t.period_start}
<DatePicker bind:value={editStart} {lang} />
</label>
<label>
{t('period_end', lang)}
{t.period_end}
<DatePicker bind:value={editEnd} min={editStart} {lang} />
</label>
</div>
<div class="add-actions">
<button class="save-btn" onclick={saveEdit} disabled={!editStart || loading}>
{t('save', lang)}
{t.save}
</button>
<button class="cancel-btn" onclick={cancelEdit}>
{t('cancel', lang)}
{t.cancel}
</button>
</div>
</div>
@@ -933,12 +934,12 @@
{#if p.endDate}
{formatDate(p.endDate)}
{:else}
<span class="ongoing-badge">{t('ongoing', lang)}</span>
<span class="ongoing-badge">{t.ongoing}</span>
{/if}
</span>
{#if p.endDate}
{@const dur = Math.round((new Date(p.endDate).getTime() - new Date(p.startDate).getTime()) / 86400000) + 1}
<span class="history-dur">{dur} {t('days', lang)}</span>
<span class="history-dur">{dur} {t.days}</span>
{/if}
</div>
<div class="history-actions">
@@ -958,33 +959,33 @@
{:else}
<div class="empty-state">
<div class="share-bar">
<p>{t('no_period_data', lang)}</p>
<button class="share-btn" onclick={() => showShare = true} aria-label={t('share', lang)}>
<p>{t.no_period_data}</p>
<button class="share-btn" onclick={() => showShare = true} aria-label={t.share}>
<UserPlus size={16} />
</button>
</div>
<button class="add-past-btn" onclick={() => showAddForm = !showAddForm}>
<Plus size={14} />
{t('add_past_period', lang)}
{t.add_past_period}
</button>
{#if showAddForm}
<div class="add-form">
<div class="add-row">
<label>
{t('period_start', lang)}
{t.period_start}
<DatePicker bind:value={addStart} max={todayStr} {lang} />
</label>
<label>
{t('period_end', lang)}
{t.period_end}
<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
</label>
</div>
<div class="add-actions">
<button class="save-btn" onclick={addPastPeriod} disabled={!addStart || loading}>
{t('save', lang)}
{t.save}
</button>
<button class="cancel-btn" onclick={() => { showAddForm = false; addStart = ''; addEnd = ''; }}>
{t('cancel', lang)}
{t.cancel}
</button>
</div>
</div>
@@ -1002,7 +1003,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="share-modal" role="presentation" onclick={(e) => e.stopPropagation()}>
<div class="share-modal-header">
<h3>{t('share', lang)}</h3>
<h3>{t.share}</h3>
<button class="share-modal-close" onclick={() => showShare = false}>
<X size={16} />
</button>
@@ -1020,13 +1021,13 @@
{/each}
</div>
{:else}
<span class="share-empty">{t('no_shared', lang)}</span>
<span class="share-empty">{t.no_shared}</span>
{/if}
<form class="share-add" onsubmit={(e) => { e.preventDefault(); addShareUser(); }}>
<input
type="text"
bind:value={shareInput}
placeholder={t('add_user', lang)}
placeholder={t.add_user}
disabled={shareSaving}
/>
<button type="submit" class="share-add-btn" disabled={!shareInput.trim() || shareSaving}>
@@ -7,7 +7,7 @@
import Wheat from '@lucide/svelte/icons/wheat';
import { untrack } from 'svelte';
import { toast } from '$lib/js/toast.svelte';
import { t } from '$lib/js/fitnessI18n';
import { m } from '$lib/js/fitnessI18n';
import MealTypePicker from '$lib/components/fitness/MealTypePicker.svelte';
/** @typedef {import('$lib/server/roundOffScoring').ComboSuggestion} ComboSuggestion */
@@ -35,6 +35,7 @@
initialSuggestions = null,
onlogged = () => {},
} = $props();
const t = $derived(m[lang]);
const isEn = $derived(lang === 'en');
@@ -157,7 +158,7 @@
<div class="round-off-header">
<Sparkles size={16} />
<h3>{isEn ? 'Round off this day' : 'Tag abrunden'}</h3>
<span class="round-off-remaining">{Math.round(remainingKcal)} kcal {t('remaining', lang)}</span>
<span class="round-off-remaining">{Math.round(remainingKcal)} kcal {t.remaining}</span>
</div>
{#if loading}
+5 -4
View File
@@ -6,9 +6,10 @@
import { METRIC_LABELS } from '$lib/data/exercises';
import RestTimer from './RestTimer.svelte';
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]);
/**
* @type {{
@@ -97,16 +98,16 @@
{#if editable && onRemove}
<th class="col-remove"></th>
{/if}
<th class="col-set">{t('set_header', lang)}</th>
<th class="col-set">{t.set_header}</th>
{#if previousSets}
<th class="col-prev">{t('prev_header', lang)}</th>
<th class="col-prev">{t.prev_header}</th>
{/if}
{#each mainMetrics as metric (metric)}
<th class="col-metric">{timedHold && metric === 'duration' ? 'SEC' : METRIC_LABELS[metric]}</th>
{/each}
{#if editable && hasRpe}
<th class="col-at"></th>
<th class="col-rpe">{t('rpe', lang)}</th>
<th class="col-rpe">{t.rpe}</th>
{/if}
{#if editable}
<th class="col-check"></th>
@@ -3,9 +3,10 @@
import EllipsisVertical from '@lucide/svelte/icons/ellipsis-vertical';
import MapPin from '@lucide/svelte/icons/map-pin';
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]);
/**
* @type {{
@@ -23,8 +24,8 @@
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) return t('today', lang);
if (diffDays === 1) return t('yesterday', lang);
if (diffDays === 0) return t.today;
if (diffDays === 1) return t.yesterday;
if (diffDays < 7) return lang === 'en' ? `${diffDays} days ago` : `vor ${diffDays} Tagen`;
return d.toLocaleDateString(lang === 'en' ? 'en' : 'de', { month: 'short', day: 'numeric' });
}
@@ -52,12 +53,12 @@
<li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each}
{#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
<li class="more">+{template.exercises.length - 4} {t.more}</li>
{/if}
</ul>
{/if}
{#if lastUsed}
<p class="last-used">{t('last_performed', lang)} {formatDate(lastUsed)}</p>
<p class="last-used">{t.last_performed} {formatDate(lastUsed)}</p>
{/if}
</div>
+4 -3
View File
@@ -5,9 +5,10 @@ import Pause from '@lucide/svelte/icons/pause';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte';
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]);
let { href, elapsed = '0:00', paused = false, syncStatus = 'idle', onPauseToggle,
restSeconds = 0, restTotal = 0, onRestAdjust = null, onRestSkip = null } = $props();
@@ -28,7 +29,7 @@ const restProgress = $derived(restTotal > 0 ? restSeconds / restTotal : 0);
class:rest-active={restActive}
role="button"
tabindex="0"
aria-label={t('active_workout', lang)}
aria-label={t.active_workout}
onclick={() => goto(href)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); goto(href); } }}
>
@@ -54,7 +55,7 @@ const restProgress = $derived(restTotal > 0 ? restSeconds / restTotal : 0);
<button class="rest-adj" onclick={(e) => { e.stopPropagation(); onRestAdjust?.(30); }} aria-label="Add 30 seconds">+30s</button>
</div>
{:else}
<span class="fab-label">{t('active_workout', lang)}</span>
<span class="fab-label">{t.active_workout}</span>
<ChevronRight size={14} strokeWidth={2.4} class="fab-chevron" />
{/if}
</div>