refactor(cycles): make date formatting locale-aware

Replace hardcoded 'de-DE' toLocaleDateString calls across ListView,
CyclesWidget, and pure helpers with the active svelte-i18n locale.

Pure helpers in queries.ts now take their locale (and for relative
dates, their labels) as parameters so they stay pure and testable:

- formatLogDate(iso, labels, dateLocale)
- groupLogsByMonth(logs, dateLocale)
- New RelativeDateLabels type, exported from the module barrel

ListView builds relativeLabels from $_ and threads dateLocale through;
CyclesWidget does the same using a tiny $locale-derived helper.

New i18n keys cycles.relativeDate.{today,yesterday,daysAgo} across
all five locales (real de/en translations, stubs for it/fr/es).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-07 18:23:58 +02:00
parent 1ba5948ce5
commit 343804b25c
9 changed files with 79 additions and 15 deletions

View file

@ -57,6 +57,11 @@
"nextPeriod": "Nächste Periode:",
"fertileWindow": "Fruchtbares Fenster:"
},
"relativeDate": {
"today": "Heute",
"yesterday": "Gestern",
"daysAgo": "vor {days} Tagen"
},
"empty": "Tippe oben auf eine Blutungsstärke, um deinen ersten Tag festzuhalten — oder starte direkt eine Periode.",
"confirm": {
"deleteEntry": "Tageseintrag vom {date} wirklich löschen?",

View file

@ -57,6 +57,11 @@
"nextPeriod": "Next period:",
"fertileWindow": "Fertile window:"
},
"relativeDate": {
"today": "Today",
"yesterday": "Yesterday",
"daysAgo": "{days} days ago"
},
"empty": "Tap a flow level above to log your first day — or start a period directly.",
"confirm": {
"deleteEntry": "Really delete the entry from {date}?",

View file

@ -57,6 +57,11 @@
"nextPeriod": "Next period:",
"fertileWindow": "Fertile window:"
},
"relativeDate": {
"today": "Today",
"yesterday": "Yesterday",
"daysAgo": "{days} days ago"
},
"empty": "Tap a flow level above to log your first day — or start a period directly.",
"confirm": {
"deleteEntry": "Really delete the entry from {date}?",

View file

@ -57,6 +57,11 @@
"nextPeriod": "Next period:",
"fertileWindow": "Fertile window:"
},
"relativeDate": {
"today": "Today",
"yesterday": "Yesterday",
"daysAgo": "{days} days ago"
},
"empty": "Tap a flow level above to log your first day — or start a period directly.",
"confirm": {
"deleteEntry": "Really delete the entry from {date}?",

View file

@ -57,6 +57,11 @@
"nextPeriod": "Next period:",
"fertileWindow": "Fertile window:"
},
"relativeDate": {
"today": "Today",
"yesterday": "Yesterday",
"daysAgo": "{days} days ago"
},
"empty": "Tap a flow level above to log your first day — or start a period directly.",
"confirm": {
"deleteEntry": "Really delete the entry from {date}?",

View file

@ -6,7 +6,7 @@
* + Vorhersage pro Render ab. Linkt zur /cycles Route.
*/
import { _ } from 'svelte-i18n';
import { _, locale } from 'svelte-i18n';
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { derivePhase, getCycleDayNumber } from '$lib/modules/cycles/utils/phase';
@ -48,9 +48,14 @@
const daysUntil = $derived(daysUntilNextPeriod(cycles));
const nextPeriod = $derived(predictNextPeriodStart(cycles));
const dateLocale = $derived.by(() => {
const l = $locale ?? 'de';
return l === 'de' ? 'de-DE' : l;
});
function formatShortDate(iso: string | null): string {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
return new Date(iso).toLocaleDateString(dateLocale, { day: '2-digit', month: '2-digit' });
}
</script>

View file

@ -3,13 +3,14 @@
Aktueller Zyklus, heutiger Quick-Log, einfache Statistiken.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { _, locale } from 'svelte-i18n';
import {
formatLogDate,
useAllCycles,
useAllDayLogs,
useAllSymptoms,
useCurrentCycle,
type RelativeDateLabels,
} from './queries';
import { cyclesStore } from './stores/cycles.svelte';
import { dayLogsStore } from './stores/dayLogs.svelte';
@ -40,6 +41,19 @@
let symptoms = $derived(symptoms$.value);
let currentCycle = $derived(current$.value);
// Locale-aware date formatting: use the active svelte-i18n locale, with
// 'de-DE' as a fallback since the project defaults to German.
const dateLocale = $derived.by(() => {
const l = $locale ?? 'de';
return l === 'de' ? 'de-DE' : l;
});
const relativeLabels = $derived<RelativeDateLabels>({
today: $_('cycles.relativeDate.today'),
yesterday: $_('cycles.relativeDate.yesterday'),
daysAgo: (n: number) => $_('cycles.relativeDate.daysAgo', { values: { days: n } }),
});
let phase = $derived(derivePhase(todayIso, cycles));
let cycleDay = $derived(currentCycle ? getCycleDayNumber(todayIso, currentCycle) : null);
let stats = $derived(computeCycleStats(cycles));
@ -115,7 +129,7 @@
backToToday();
return;
}
const dateStr = new Date(editingDate).toLocaleDateString('de-DE');
const dateStr = new Date(editingDate).toLocaleDateString(dateLocale);
const ok = confirm($_('cycles.confirm.deleteEntry', { values: { date: dateStr } }));
if (!ok) return;
await dayLogsStore.deleteLog(editingLog.id);
@ -135,7 +149,7 @@
function formatDate(iso: string | null): string {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('de-DE', {
return new Date(iso).toLocaleDateString(dateLocale, {
day: '2-digit',
month: '2-digit',
});
@ -199,7 +213,7 @@
<div class="edit-banner">
<span class="edit-banner-label">
{$_('cycles.label.editing')}
<strong>{new Date(editingDate).toLocaleDateString('de-DE')}</strong>
<strong>{new Date(editingDate).toLocaleDateString(dateLocale)}</strong>
</span>
<div class="edit-banner-actions">
{#if editingLog}
@ -347,7 +361,9 @@
<span class="log-flow" style="background: {FLOW_COLORS[log.flow]}"></span>
<div class="log-content">
<div class="log-top">
<span class="log-date">{formatLogDate(log.logDate)}</span>
<span class="log-date"
>{formatLogDate(log.logDate, relativeLabels, dateLocale)}</span
>
{#if log.flow !== 'none'}
<span class="log-tag">{$_(`cycles.flow.${log.flow}`)}</span>
{/if}

View file

@ -20,6 +20,7 @@ export {
groupLogsByMonth,
formatLogDate,
} from './queries';
export type { RelativeDateLabels } from './queries';
// ─── Utils ───────────────────────────────────────────────
export { derivePhase, findCycleForDate, getCycleDayNumber, daysBetween } from './utils/phase';

View file

@ -122,14 +122,23 @@ export function useAllSymptoms() {
// ─── Pure Helpers ──────────────────────────────────────────
/** Group day logs by ISO month label. */
/** Labels a caller must provide to formatLogDate so it can be locale-aware. */
export interface RelativeDateLabels {
today: string;
yesterday: string;
/** Template for "N days ago", receives the numeric count. */
daysAgo: (days: number) => string;
}
/** Group day logs by localized month label. */
export function groupLogsByMonth(
logs: CycleDayLog[]
logs: CycleDayLog[],
dateLocale: string = 'de-DE'
): Array<{ label: string; logs: CycleDayLog[] }> {
const groups = new Map<string, CycleDayLog[]>();
for (const l of logs) {
const date = new Date(l.logDate);
const label = date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
const label = date.toLocaleDateString(dateLocale, { month: 'long', year: 'numeric' });
const bucket = groups.get(label) ?? [];
bucket.push(l);
groups.set(label, bucket);
@ -137,12 +146,20 @@ export function groupLogsByMonth(
return Array.from(groups, ([label, logs]) => ({ label, logs }));
}
export function formatLogDate(iso: string): string {
/**
* Format a log date relative to today using caller-provided labels.
* Falls back to absolute date formatting via `dateLocale` when >= 7 days ago.
*/
export function formatLogDate(
iso: string,
labels: RelativeDateLabels,
dateLocale: string = 'de-DE'
): string {
const date = new Date(iso);
const today = new Date();
const diffDays = Math.floor((today.getTime() - date.getTime()) / 86_400_000);
if (diffDays === 0) return 'Heute';
if (diffDays === 1) return 'Gestern';
if (diffDays < 7) return `vor ${diffDays} Tagen`;
return date.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' });
if (diffDays === 0) return labels.today;
if (diffDays === 1) return labels.yesterday;
if (diffDays < 7) return labels.daysAgo(diffDays);
return date.toLocaleDateString(dateLocale, { day: 'numeric', month: 'short', year: 'numeric' });
}