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:
Marc-André Bombeck committed 2026-03-12 21:31:48 +01:00
1 parent 38309932f3
commit 0c0c345e82
74 files changed
+871 -402

No files matched your search

+2 -2
View File
@@ -30,11 +30,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const body = await request.json();
confirm = typeof body?.confirm === "string" ? body.confirm : "";
} catch {
return apiError("Ungültige Anfrage", 422);
return apiError("Invalid request", 422);
}
if (confirm !== "DELETE ALL") {
return apiError("Bestätigung fehlt", 422);
return apiError("Confirmation missing", 422);
}
const result = await prisma.$transaction(async (tx) => {
@@ -12,10 +12,10 @@ export const POST = apiHandler(async () => {
const settings = await getGlitchtipSettings();
if (!settings.glitchtipEnabled) {
return apiError("Glitchtip ist deaktiviert", 422);
return apiError("Glitchtip is disabled", 422);
}
if (!settings.glitchtipDsn) {
return apiError("Glitchtip DSN fehlt", 422);
return apiError("Glitchtip DSN is missing", 422);
}
const delivery = await sendGlitchtipEvent({
@@ -32,13 +32,13 @@ export const POST = apiHandler(async () => {
if (!delivery.ok) {
getEvent()?.addWarning("Glitchtip test event rejected: " + delivery.method + " " + delivery.status + " " + delivery.details);
return apiError(
`Glitchtip-Testevent wurde abgelehnt (HTTP ${delivery.status ?? 502})`,
`Glitchtip test event rejected (HTTP ${delivery.status ?? 502})`,
502,
);
}
return apiSuccess({
sent: true,
message: "Glitchtip-Testevent wurde gesendet",
message: "Glitchtip test event sent",
});
});
@@ -51,18 +51,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
const settings = await getPublicMonitoringSettings();
if (!settings.umamiEnabled) {
return apiError("Umami ist deaktiviert", 422);
return apiError("Umami is disabled", 422);
}
if (!settings.umamiScriptUrl || !settings.umamiWebsiteId) {
return apiError(
"Umami Script-URL und Website-ID müssen konfiguriert sein",
"Umami script URL and website ID must be configured",
422,
);
}
const targetUrls = resolveUmamiSendUrls(settings.umamiScriptUrl);
if (targetUrls.length === 0) {
return apiError("Umami Script-URL ist ungültig", 422);
return apiError("Umami script URL is invalid", 422);
}
const appUrl = resolveAppUrl(request);
@@ -100,7 +100,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (upstream.ok) {
return apiSuccess({
sent: true,
message: "Umami-Testevent wurde gesendet",
message: "Umami test event sent",
});
}
@@ -117,7 +117,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
getEvent()?.addWarning("Umami test event rejected: " + lastStatus);
return apiError(
`Umami-Testevent wurde abgelehnt (HTTP ${lastStatus})`,
`Umami test event rejected (HTTP ${lastStatus})`,
502,
);
});
+10 -10
View File
@@ -79,7 +79,7 @@ export const POST = apiHandler(async () => {
if (channels.length === 0) {
return apiSuccess({
sent: false,
message: "Keine aktivierten Benachrichtigungskanäle gefunden",
message: "No enabled notification channels found",
results: [],
});
}
@@ -89,7 +89,7 @@ export const POST = apiHandler(async () => {
userId: user.id,
title: "Test-Notification",
message:
"<b>HealthLog Test:</b> Wenn du diese Nachricht siehst, funktionieren deine Benachrichtigungen!",
"<b>HealthLog Test:</b> If you see this message, your notifications are working!",
};
const results: Array<{
@@ -104,7 +104,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: channel.type,
success: false,
error: "SYSTEM_ALERT deaktiviert in Einstellungen",
error: "SYSTEM_ALERT disabled in settings",
});
continue;
}
@@ -124,7 +124,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: "TELEGRAM",
success: false,
error: `Telegram API hat Fehler gemeldet (chatId: ${config.chatId})`,
error: `Telegram API reported error (chatId: ${config.chatId})`,
});
continue;
}
@@ -139,7 +139,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: "NTFY",
success: false,
error: `Senden fehlgeschlagen (topic: ${config.topic})`,
error: `Send failed (topic: ${config.topic})`,
});
continue;
}
@@ -153,7 +153,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: type,
success: false,
error: "Unbekannter Kanaltyp",
error: "Unknown channel type",
});
continue;
}
@@ -163,7 +163,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: channel.type,
success: false,
error: err instanceof Error ? err.message : "Unbekannter Fehler",
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
@@ -173,11 +173,11 @@ export const POST = apiHandler(async () => {
let message: string;
if (failCount === 0) {
message = `Alle ${successCount} Kanäle erfolgreich`;
message = `All ${successCount} channels successful`;
} else if (successCount === 0) {
message = `Alle ${failCount} Kanäle fehlgeschlagen`;
message = `All ${failCount} channels failed`;
} else {
message = `${successCount} erfolgreich, ${failCount} fehlgeschlagen`;
message = `${successCount} successful, ${failCount} failed`;
}
return apiSuccess({ sent: successCount > 0, message, results });
+1 -1
View File
@@ -180,7 +180,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
}
if (Object.keys(updates).length === 0) {
return apiError("Keine gültigen Felder", 422);
return apiError("No valid fields", 422);
}
const settings = await prisma.appSettings.upsert({
@@ -32,16 +32,16 @@ export const POST = apiHandler(async (
const { password } = body as { password?: string };
if (!password || typeof password !== "string") {
return apiError("Passwort erforderlich", 422);
return apiError("Password required", 422);
}
const target = await prisma.user.findUnique({ where: { id } });
if (!target) return apiError("Benutzer nicht gefunden", 404);
if (!target) return apiError("User not found", 404);
const strength = checkPasswordStrength(password, [target.username]);
if (!strength.isAcceptable) {
return apiError(
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
strength.feedback[0] || "Password too weak (score < 3)",
422,
);
}
+2 -2
View File
@@ -30,13 +30,13 @@ export const PUT = apiHandler(async (
}
const target = await prisma.user.findUnique({ where: { id } });
if (!target) return apiError("Benutzer nicht gefunden", 404);
if (!target) return apiError("User not found", 404);
// Prevent removing the last admin
if (parsed.data.role === "USER" && target.role === "ADMIN") {
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
if (adminCount <= 1) {
return apiError("Der letzte Admin kann nicht degradiert werden", 400);
return apiError("The last admin cannot be demoted", 400);
}
}