mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
i18n: migrate from German-first to English-first default locale
Switch defaultLocale from "de" to "en" and make English the code-level language throughout. All ~145 hardcoded German strings translated: - API error messages (52 routes) now return English - Zod validation messages in English - Classification/analytics functions return English categories - Notification type labels in English - Component strings use i18n t() calls (heatmap, mood chart, medication form, intake history, password strength, insights card) - ~50 new i18n keys in both de.json and en.json Route migration: - /stimmung → /mood, /zielwerte → /targets (new pages) - 301 redirects in proxy.ts for legacy URLs - Sidebar + bottom nav updated Bump version to 1.0.1.
This commit is contained in:
1 parent
38309932f3
commit
0c0c345e82
74 files changed
+871
-402
No files matched your search
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect, useRef } from "react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
interface DailyData {
|
||||
expected: number;
|
||||
@@ -19,21 +20,6 @@ interface ComplianceHeatmapProps {
|
||||
|
||||
const CELL_SIZE = 18;
|
||||
const GAP = 3;
|
||||
const WEEKDAY_LABELS = ["Mo", "", "Mi", "", "Fr", "", "So"];
|
||||
const MONTH_LABELS = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mär",
|
||||
"Apr",
|
||||
"Mai",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Okt",
|
||||
"Nov",
|
||||
"Dez",
|
||||
];
|
||||
|
||||
function getColor(data: DailyData): string {
|
||||
if (data.expected === 0) return "var(--secondary)";
|
||||
@@ -80,6 +66,7 @@ export function ComplianceHeatmap({
|
||||
days = 90,
|
||||
stretch = false,
|
||||
}: ComplianceHeatmapProps) {
|
||||
const { t } = useTranslations();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const [tooltip, setTooltip] = useState<{
|
||||
@@ -88,6 +75,9 @@ export function ComplianceHeatmap({
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
const WEEKDAY_LABELS = [
|
||||
t("charts.weekdays.mon"), "", t("charts.weekdays.wed"), "", t("charts.weekdays.fri"), "", t("charts.weekdays.sun"),
|
||||
];
|
||||
useEffect(() => {
|
||||
if (!stretch) return;
|
||||
const element = containerRef.current;
|
||||
@@ -107,6 +97,12 @@ export function ComplianceHeatmap({
|
||||
}, [stretch]);
|
||||
|
||||
const { cells, weeks, monthMarkers } = useMemo(() => {
|
||||
const MONTH_LABELS = [
|
||||
t("charts.months.jan"), t("charts.months.feb"), t("charts.months.mar"),
|
||||
t("charts.months.apr"), t("charts.months.may"), t("charts.months.jun"),
|
||||
t("charts.months.jul"), t("charts.months.aug"), t("charts.months.sep"),
|
||||
t("charts.months.oct"), t("charts.months.nov"), t("charts.months.dec"),
|
||||
];
|
||||
const now = new Date();
|
||||
const cellList: Array<{
|
||||
dateKey: string;
|
||||
@@ -162,7 +158,7 @@ export function ComplianceHeatmap({
|
||||
}
|
||||
|
||||
return { cells: cellList, weeks: col + 1, monthMarkers: markers };
|
||||
}, [dailyCompliance, days]);
|
||||
}, [dailyCompliance, days, t]);
|
||||
|
||||
const labelWidth = stretch ? 0 : 76;
|
||||
const headerHeight = 18;
|
||||
@@ -244,7 +240,7 @@ export function ComplianceHeatmap({
|
||||
cell.data.late !== undefined ||
|
||||
cell.data.veryLate !== undefined;
|
||||
const timingInfo = hasTimingData
|
||||
? ` | ${cell.data.onTime ?? 0} pünktlich, ${cell.data.late ?? 0} spät, ${cell.data.veryLate ?? 0} sehr spät`
|
||||
? ` | ${cell.data.onTime ?? 0} ${t("charts.heatmapOnTime")}, ${cell.data.late ?? 0} ${t("charts.heatmapLate")}, ${cell.data.veryLate ?? 0} ${t("charts.heatmapVeryLate")}`
|
||||
: "";
|
||||
setTooltip({
|
||||
x: e.clientX,
|
||||
@@ -274,10 +270,10 @@ export function ComplianceHeatmap({
|
||||
style={{ marginLeft: stretch ? 0 : labelWidth }}
|
||||
>
|
||||
{[
|
||||
{ color: "var(--dracula-green)", label: "Pünktlich" },
|
||||
{ color: "var(--dracula-yellow)", label: "Spät" },
|
||||
{ color: "var(--dracula-orange)", label: "Sehr spät" },
|
||||
{ color: "var(--dracula-red)", label: "Verpasst" },
|
||||
{ color: "var(--dracula-green)", label: t("charts.legendOnTime") },
|
||||
{ color: "var(--dracula-yellow)", label: t("charts.legendLate") },
|
||||
{ color: "var(--dracula-orange)", label: t("charts.legendVeryLate") },
|
||||
{ color: "var(--dracula-red)", label: t("charts.legendMissed") },
|
||||
].map(({ color, label }) => (
|
||||
<span key={label} className="flex items-center gap-1">
|
||||
<div
|
||||
|
||||
@@ -56,13 +56,7 @@ const TIME_RANGES_KEYS = [
|
||||
{ labelKey: "charts.pointsAllLabel", points: 0, titleKey: "charts.pointsAllTitle" },
|
||||
] as const;
|
||||
|
||||
const MOOD_LABELS: Record<number, string> = {
|
||||
1: "Lausig",
|
||||
2: "Schlecht",
|
||||
3: "Okay",
|
||||
4: "Gut",
|
||||
5: "Super",
|
||||
};
|
||||
// MOOD_LABELS built dynamically via t() in the component
|
||||
|
||||
const VALUE_BANDS = [
|
||||
{ min: 1, max: 2, color: "#ff5555", opacity: 0.16 },
|
||||
@@ -231,13 +225,21 @@ export function MoodChart({ title }: MoodChartProps) {
|
||||
|
||||
const maxPointIndex = Math.max(0, (chartData?.length ?? 1) - 1);
|
||||
|
||||
const moodLabels: Record<number, string> = {
|
||||
1: t("charts.moodLabel1"),
|
||||
2: t("charts.moodLabel2"),
|
||||
3: t("charts.moodLabel3"),
|
||||
4: t("charts.moodLabel4"),
|
||||
5: t("charts.moodLabel5"),
|
||||
};
|
||||
|
||||
const formatMoodTick = (value: number): string => {
|
||||
return MOOD_LABELS[value] ?? String(value);
|
||||
return moodLabels[value] ?? String(value);
|
||||
};
|
||||
|
||||
const formatTooltipValue = (value: number): string => {
|
||||
const rounded = Math.round(value * 10) / 10;
|
||||
const label = MOOD_LABELS[Math.round(value)];
|
||||
const label = moodLabels[Math.round(value)];
|
||||
return label ? `${rounded} (${label})` : String(rounded);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Sparkles, RefreshCw, Loader2, AlertTriangle } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
interface InsightsOutput {
|
||||
changed: string;
|
||||
@@ -32,6 +33,7 @@ const CONFIDENCE_COLORS: Record<string, string> = {
|
||||
export function InsightsCard() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Check if insights are configured
|
||||
@@ -81,7 +83,7 @@ export function InsightsCard() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="text-dracula-purple h-5 w-5" />
|
||||
<CardTitle className="text-lg">KI-Insights</CardTitle>
|
||||
<CardTitle className="text-lg">{t("insights.aiInsights")}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -94,7 +96,7 @@ export function InsightsCard() {
|
||||
) : (
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
{insights ? "Aktualisieren" : "Generieren"}
|
||||
{insights ? t("insights.refreshButton") : t("insights.generateButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -111,7 +113,7 @@ export function InsightsCard() {
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-dracula-purple h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
Analysiere Gesundheitsdaten...
|
||||
{t("insights.analyzing")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -120,19 +122,19 @@ export function InsightsCard() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={CONFIDENCE_COLORS[insights.confidence] ?? ""}>
|
||||
Konfidenz: {insights.confidence}
|
||||
{t("insights.confidence")}: {t(`insights.confidence_${insights.confidence}`)}
|
||||
</Badge>
|
||||
{generate.data?.cached && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Gecacht
|
||||
{t("insights.cached")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Section title="Was hat sich verändert" text={insights.changed} />
|
||||
<Section title="Was ist stabil" text={insights.stable} />
|
||||
<Section title="Mögliche Zusammenhänge" text={insights.drivers} />
|
||||
<Section title="Nächste Schritte" text={insights.nextSteps} />
|
||||
<Section title={t("insights.sectionChanged")} text={insights.changed} />
|
||||
<Section title={t("insights.sectionStable")} text={insights.stable} />
|
||||
<Section title={t("insights.sectionDrivers")} text={insights.drivers} />
|
||||
<Section title={t("insights.sectionNextSteps")} text={insights.nextSteps} />
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
@@ -145,8 +147,7 @@ export function InsightsCard() {
|
||||
|
||||
{!insights && !generate.isPending && !error && (
|
||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||
Klicke auf "Generieren" für KI-basierte Insights deiner
|
||||
Gesundheitsdaten.
|
||||
{t("insights.generatePrompt")}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -17,10 +17,10 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
const navItems = [
|
||||
{ href: "/", tKey: "nav.dashboard", icon: Home },
|
||||
{ href: "/measurements", tKey: "nav.measurements", icon: Activity },
|
||||
{ href: "/stimmung", tKey: "nav.mood", icon: Waves },
|
||||
{ href: "/mood", tKey: "nav.mood", icon: Waves },
|
||||
{ href: "/medications", tKey: "nav.medications", icon: Pill },
|
||||
{ href: "/insights", tKey: "nav.insights", icon: Lightbulb },
|
||||
{ href: "/zielwerte", tKey: "nav.targets", icon: Target },
|
||||
{ href: "/targets", tKey: "nav.targets", icon: Target },
|
||||
{ href: "/achievements", tKey: "nav.achievements", icon: Trophy },
|
||||
];
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ const STORAGE_KEY = "healthlog-sidebar-collapsed";
|
||||
const navItems = [
|
||||
{ href: "/", tKey: "nav.dashboard", icon: Home },
|
||||
{ href: "/measurements", tKey: "nav.measurements", icon: Activity },
|
||||
{ href: "/stimmung", tKey: "nav.mood", icon: Waves },
|
||||
{ href: "/mood", tKey: "nav.mood", icon: Waves },
|
||||
{ href: "/medications", tKey: "nav.medications", icon: Pill },
|
||||
{ href: "/insights", tKey: "nav.insights", icon: Lightbulb },
|
||||
{ href: "/zielwerte", tKey: "nav.targets", icon: Target },
|
||||
{ href: "/targets", tKey: "nav.targets", icon: Target },
|
||||
{ href: "/achievements", tKey: "nav.achievements", icon: Trophy },
|
||||
];
|
||||
|
||||
|
||||
@@ -72,12 +72,7 @@ interface IntakeHistoryListProps {
|
||||
onCreateOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
WEB: "Webseite",
|
||||
API: "Externe API",
|
||||
REMINDER: "Telegram / Erinnerung",
|
||||
IMPORT: "CSV-Import",
|
||||
};
|
||||
// SOURCE_LABELS built dynamically via t() in the component
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
@@ -97,6 +92,13 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
WEB: t("medications.sourceWeb"),
|
||||
API: t("medications.sourceApi"),
|
||||
REMINDER: t("medications.sourceReminder"),
|
||||
IMPORT: t("medications.sourceImport"),
|
||||
};
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [sortBy, setSortBy] = useState<string>("scheduledFor");
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
||||
@@ -219,7 +221,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
},
|
||||
onError: (err) => {
|
||||
setEditError(
|
||||
err instanceof Error ? err.message : "Fehler beim Speichern",
|
||||
err instanceof Error ? err.message : t("medications.saveError"),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -257,7 +259,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
},
|
||||
onError: (err) => {
|
||||
setCreateError(
|
||||
err instanceof Error ? err.message : "Fehler beim Speichern",
|
||||
err instanceof Error ? err.message : t("medications.saveError"),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -293,7 +295,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
await deleteMutation.mutateAsync(editing.id);
|
||||
closeEdit();
|
||||
} catch {
|
||||
setEditError("Fehler beim Löschen");
|
||||
setEditError(t("medications.deleteError"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +305,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
|
||||
const scheduledDate = new Date(editScheduledFor);
|
||||
if (Number.isNaN(scheduledDate.getTime())) {
|
||||
setEditError("Bitte einen gültigen Zeitpunkt wählen.");
|
||||
setEditError(t("medications.invalidTimestamp"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,7 +325,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
|
||||
const scheduledDate = new Date(createScheduledFor);
|
||||
if (Number.isNaN(scheduledDate.getTime())) {
|
||||
setCreateError("Bitte einen gültigen Zeitpunkt wählen.");
|
||||
setCreateError(t("medications.invalidTimestamp"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -365,7 +367,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Bitte anmelden, um Einnahmen zu sehen.
|
||||
{t("medications.loginRequiredIntakes")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -387,7 +389,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</div>
|
||||
) : !data?.events.length ? (
|
||||
<div className="text-muted-foreground flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||
Noch keine Einnahmen vorhanden
|
||||
{t("medications.noIntakesYet")}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -420,7 +422,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
className="w-28"
|
||||
/>
|
||||
<TableHead className="w-20 pr-4 text-right">
|
||||
Aktionen
|
||||
{t("medications.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -438,7 +440,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
<TableCell>{getStatusBadge(event)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{SOURCE_LABELS[event.source] ?? event.source}
|
||||
{sourceLabels[event.source] ?? event.source}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="pr-4 text-right">
|
||||
@@ -485,7 +487,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
<div className="flex items-center gap-1.5">
|
||||
{getStatusBadge(event)}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{SOURCE_LABELS[event.source] ?? event.source}
|
||||
{sourceLabels[event.source] ?? event.source}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -516,7 +518,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages}
|
||||
{t("medications.pageInfo", { page, total: totalPages })}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
@@ -614,7 +616,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
onClick={() => setEditDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Löschen
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -628,7 +630,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
updateMutation.isPending || deleteMutation.isPending
|
||||
}
|
||||
>
|
||||
Abbrechen
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -639,7 +641,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Speichern
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -659,7 +661,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteMutation.isPending}>
|
||||
Abbrechen
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
@@ -669,7 +671,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
{deleteMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Löschen
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -737,13 +739,13 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
onClick={closeCreate}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
Abbrechen
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Speichern
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -800,6 +802,7 @@ function DeleteButton({
|
||||
description: string;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
@@ -807,7 +810,7 @@ function DeleteButton({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive h-8 w-8"
|
||||
aria-label="Löschen"
|
||||
aria-label={label}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -818,12 +821,12 @@ function DeleteButton({
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Löschen
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -51,17 +51,7 @@ import { toast } from "sonner";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { PhaseConfigDialog } from "@/components/medications/phase-config-dialog";
|
||||
|
||||
const DOSE_UNITS = [
|
||||
"mg",
|
||||
"g",
|
||||
"ml",
|
||||
"Tropfen",
|
||||
"Tablette(n)",
|
||||
"Kapsel(n)",
|
||||
"Stück",
|
||||
"IE",
|
||||
"µg",
|
||||
] as const;
|
||||
// DOSE_UNITS built dynamically via t() in the component
|
||||
|
||||
type TranslateFn = (
|
||||
key: string,
|
||||
@@ -245,6 +235,16 @@ export function MedicationForm({
|
||||
}: MedicationFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
|
||||
const doseUnits = [
|
||||
"mg", "g", "ml",
|
||||
t("medications.unitDrops"),
|
||||
t("medications.unitTablets"),
|
||||
t("medications.unitCapsules"),
|
||||
t("medications.unitPieces"),
|
||||
"IE", "µg",
|
||||
];
|
||||
|
||||
const [name, setName] = useState(initial?.name ?? "");
|
||||
|
||||
const initialDose = parseDose(initial?.dose ?? "");
|
||||
@@ -449,7 +449,7 @@ export function MedicationForm({
|
||||
id="med-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="z.B. Ibuprofen"
|
||||
placeholder={t("medications.namePlaceholder")}
|
||||
required
|
||||
maxLength={100}
|
||||
/>
|
||||
@@ -508,7 +508,7 @@ export function MedicationForm({
|
||||
inputMode="decimal"
|
||||
value={doseAmount}
|
||||
onChange={(e) => setDoseAmount(e.target.value)}
|
||||
placeholder="z.B. 400"
|
||||
placeholder={t("medications.dosePlaceholder")}
|
||||
required
|
||||
maxLength={20}
|
||||
/>
|
||||
@@ -527,7 +527,7 @@ export function MedicationForm({
|
||||
</div>
|
||||
</div>
|
||||
<datalist id="dose-units">
|
||||
{DOSE_UNITS.map((u) => (
|
||||
{doseUnits.map((u) => (
|
||||
<option key={u} value={u} />
|
||||
))}
|
||||
</datalist>
|
||||
@@ -641,7 +641,7 @@ export function MedicationForm({
|
||||
value={s.label}
|
||||
className="h-8 text-xs md:text-xs"
|
||||
onChange={(e) => updateSchedule(i, "label", e.target.value)}
|
||||
placeholder="z.B. Morgens"
|
||||
placeholder={t("medications.labelPlaceholder")}
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
@@ -653,7 +653,7 @@ export function MedicationForm({
|
||||
value={s.dose}
|
||||
className="h-8 text-xs md:text-xs"
|
||||
onChange={(e) => updateSchedule(i, "dose", e.target.value)}
|
||||
placeholder={dose || "Standard"}
|
||||
placeholder={dose || t("medications.defaultDose")}
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
@@ -721,8 +721,8 @@ export function MedicationForm({
|
||||
}`}
|
||||
title={
|
||||
s.daysOfWeek.includes(dayIndex)
|
||||
? `${label} deaktivieren`
|
||||
: `${label} aktivieren`
|
||||
? t("medications.dayDeactivate", { day: label })
|
||||
: t("medications.dayActivate", { day: label })
|
||||
}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -3,32 +3,26 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translateZxcvbn } from "@/lib/zxcvbn-de";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
interface PasswordStrengthProps {
|
||||
password: string;
|
||||
minLength?: number;
|
||||
}
|
||||
|
||||
const SCORE_CONFIG = [
|
||||
{ label: "Sehr schwach", color: "bg-red-500", textColor: "text-red-500" },
|
||||
{ label: "Schwach", color: "bg-orange-500", textColor: "text-orange-500" },
|
||||
{
|
||||
label: "Akzeptabel",
|
||||
color: "bg-yellow-500",
|
||||
textColor: "text-yellow-500",
|
||||
},
|
||||
{ label: "Stark", color: "bg-green-500", textColor: "text-green-500" },
|
||||
{
|
||||
label: "Sehr stark",
|
||||
color: "bg-emerald-600",
|
||||
textColor: "text-emerald-600",
|
||||
},
|
||||
const SCORE_COLORS = [
|
||||
{ color: "bg-red-500", textColor: "text-red-500" },
|
||||
{ color: "bg-orange-500", textColor: "text-orange-500" },
|
||||
{ color: "bg-yellow-500", textColor: "text-yellow-500" },
|
||||
{ color: "bg-green-500", textColor: "text-green-500" },
|
||||
{ color: "bg-emerald-600", textColor: "text-emerald-600" },
|
||||
];
|
||||
|
||||
export function PasswordStrength({
|
||||
password,
|
||||
minLength = 12,
|
||||
}: PasswordStrengthProps) {
|
||||
const { t } = useTranslations();
|
||||
const [result, setResult] = useState<{
|
||||
score: number;
|
||||
feedback: { warning: string; suggestions: string[] };
|
||||
@@ -60,13 +54,20 @@ export function PasswordStrength({
|
||||
if (!password) return null;
|
||||
|
||||
const score = result?.score ?? 0;
|
||||
const config = SCORE_CONFIG[score];
|
||||
const colors = SCORE_COLORS[score];
|
||||
const scoreLabels = [
|
||||
t("passwordStrength.veryWeak"),
|
||||
t("passwordStrength.weak"),
|
||||
t("passwordStrength.acceptable"),
|
||||
t("passwordStrength.strong"),
|
||||
t("passwordStrength.veryStrong"),
|
||||
];
|
||||
const tooShort = password.length < minLength;
|
||||
|
||||
// Collect feedback
|
||||
const feedback: string[] = [];
|
||||
if (tooShort) {
|
||||
feedback.push(`Mindestens ${minLength} Zeichen erforderlich.`);
|
||||
feedback.push(t("passwordStrength.minLength", { count: minLength }));
|
||||
}
|
||||
if (result?.feedback.warning) {
|
||||
feedback.push(translateZxcvbn(result.feedback.warning));
|
||||
@@ -84,7 +85,7 @@ export function PasswordStrength({
|
||||
key={i}
|
||||
className={cn(
|
||||
"h-1.5 flex-1 rounded-full transition-colors",
|
||||
i <= score && !tooShort ? config.color : "bg-muted",
|
||||
i <= score && !tooShort ? colors.color : "bg-muted",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -92,11 +93,11 @@ export function PasswordStrength({
|
||||
|
||||
{/* Label */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn("text-xs font-medium", config.textColor)}>
|
||||
{tooShort ? "Zu kurz" : config.label}
|
||||
<span className={cn("text-xs font-medium", colors.textColor)}>
|
||||
{tooShort ? t("passwordStrength.tooShort") : scoreLabels[score]}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{password.length}/{minLength}+ Zeichen
|
||||
{password.length}/{minLength}+ {t("passwordStrength.characters")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user