diff --git a/messages/de.json b/messages/de.json index 45748f9..92405f8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -234,7 +234,8 @@ "toastDeleted": "Eintrag gelöscht", "toastError": "Gewichtseintrag konnte nicht gespeichert werden", "toastDeleteError": "Eintrag konnte nicht gelöscht werden", - "toastValidation": "Bitte Datum und gültiges Gewicht eingeben" + "toastValidation": "Bitte Datum und gültiges Gewicht eingeben", + "chartHistory": "Verlauf" }, "vet": { "add": "Besuch hinzufügen", @@ -295,6 +296,134 @@ "toastDeleted": "Fütterungsinfo entfernt", "toastError": "Fütterungsinfo konnte nicht gespeichert werden", "toastDeleteError": "Fütterungsinfo konnte nicht entfernt werden" + }, + "share": { + "title": "Health Card teilen — {petName}", + "description": "Jeder mit diesem Link kann {petName}s Gesundheitsdaten einsehen — Gewicht, Impfungen und Tierarztbesuche. Kein Login erforderlich.", + "validityLabel": "Gültigkeitsdauer", + "expiryDays": "{count} Tage", + "generate": "Generieren", + "validUntil": "Gültig bis", + "openPdf": "Als PDF öffnen / drucken", + "regenerateTitle": "Link neu generieren", + "regenerateDesc": "Erstellt einen neuen Link und macht den aktuellen ungültig. Nützlich zum Widerrufen von Zugriffen.", + "toastGenerateError": "Link konnte nicht generiert werden", + "toastRegenerateSuccess": "Neuer Link erstellt — alter Link ist jetzt ungültig", + "toastRegenerateError": "Link konnte nicht neu generiert werden" } + }, + "DeleteAccount": { + "trigger": "Konto löschen", + "warningTitle": "Konto wirklich löschen?", + "warningIntro": "Diese Aktion ist unwiderruflich. Folgendes wird dauerhaft gelöscht:", + "itemPets": "Alle Tierprofile", + "itemPosts": "Alle Posts, Kommentare und Reaktionen", + "itemMedia": "Alle Bilder und Videos", + "itemMessages": "Alle Chats und Nachrichten", + "itemHealth": "Alle Gesundheitsdaten", + "itemAccount": "Dein PawFeed-Konto", + "cancel": "Abbrechen", + "understand": "Ich verstehe, weiter", + "confirmTitle": "Bestätigung erforderlich", + "confirmIntro": "Gib den Namen eines deiner Haustiere ein, um die Löschung zu bestätigen:", + "inputPlaceholder": "Tiername eingeben…", + "confirmDelete": "Konto endgültig löschen", + "deleting": "Konto wird gelöscht…", + "doneTitle": "Konto erfolgreich gelöscht.", + "doneRedirect": "Du wirst weitergeleitet…", + "unknownError": "Unbekannter Fehler", + "genericError": "Etwas ist schiefgelaufen" + }, + "NotificationsPage": { + "title": "Benachrichtigungen", + "emptyTitle": "Noch keine Benachrichtigungen", + "emptyDesc": "Wenn Tiere dir folgen oder auf deine Posts reagieren, siehst du es hier.", + "typeFollow": "folgt dir jetzt", + "typeReaction": "hat auf deinen Post reagiert", + "commentedPrefix": "kommentierte:", + "typeCommentNoBody": "hat deinen Post kommentiert", + "typeMessage": "hat dir eine Nachricht gesendet", + "typeMention": "hat dich in einem Kommentar erwähnt", + "typePawBack": "hat deinen Post geteilt (Paw-Back)", + "typeBirthday": "🎂 Alles Gute zum Geburtstag! Erstelle einen Jubiläums-Post.", + "typeAdoptionDay": "🏠 Heute ist der Adoptionstag! Feiere den Jahrestag." + }, + "PhotoPostForm": { + "captionPlaceholder": "Bildunterschrift schreiben…", + "captionError": "Bildunterschrift darf höchstens 500 Zeichen haben.", + "charactersRemaining": "Noch {count} Zeichen", + "shareCta": "Post teilen", + "sharing": "Wird geteilt…", + "successToast": "Post geteilt", + "errorToast": "Dein Post konnte nicht geteilt werden. Bitte versuch es erneut.", + "noPetSelected": "Kein aktives Tier ausgewählt.", + "noPhoto": "Bitte füge mindestens ein Foto hinzu." + }, + "StoryForm": { + "addPhoto": "Foto hinzufügen", + "disappearsNote": "Storys verschwinden nach 24 Stunden", + "changePhoto": "Foto ändern", + "postCta": "Story posten", + "posting": "Wird gepostet…", + "successToast": "Story gepostet", + "errorToast": "Deine Story konnte nicht gepostet werden. Bitte versuch es erneut.", + "noPetSelected": "Kein aktives Tier ausgewählt.", + "fileTooLarge": "Foto muss unter 10 MB sein.", + "invalidType": "Lade ein JPG-, PNG- oder WebP-Bild hoch.", + "uploadFailed": "Upload fehlgeschlagen — bitte versuch es erneut." + }, + "MilestoneForm": { + "questionLabel": "Was ist der Meilenstein?", + "selectPlaceholder": "Meilenstein auswählen…", + "litterDetailsLabel": "Wurf-Details", + "optional": "(optional)", + "litterPlaceholder": "z.B. 5 gesunde Kätzchen, geboren am 13. Juni…", + "charactersRemaining": "Noch {count} Zeichen", + "photoLabel": "Foto", + "addPhoto": "Foto hinzufügen", + "change": "Ändern", + "noteLabel": "Notiz hinzufügen", + "notePlaceholder": "Notiz hinzufügen… (optional)", + "shareCta": "Meilenstein teilen", + "sharing": "Wird geteilt…", + "successToast": "Meilenstein geteilt", + "errorToast": "Meilenstein konnte nicht geteilt werden. Bitte versuch es erneut.", + "fileTooLarge": "Foto muss unter 10 MB sein.", + "invalidType": "Lade ein JPG-, PNG- oder WebP-Bild hoch.", + "uploadFailed": "Upload fehlgeschlagen — bitte versuch es erneut." + }, + "VideoUploadForm": { + "selectVideo": "Video auswählen", + "formatHint": "MP4, MOV oder WebM · bis 500 MB · max. 3 Minuten", + "selectVideoAria": "Video auswählen", + "changeVideo": "Anderes Video wählen", + "captionLabel": "Bildunterschrift", + "captionPlaceholder": "Bildunterschrift schreiben…", + "charactersLeft": "Noch {count}", + "uploading": "Wird hochgeladen…", + "processingDone": "Hochgeladen — Video wird verarbeitet", + "uploadFailedGeneric": "Upload fehlgeschlagen — bitte versuch es erneut", + "cancel": "Abbrechen", + "postVideo": "Video posten", + "invalidType": "Lade ein MP4-, MOV- oder WebM-Video hoch.", + "fileTooLarge": "Video muss unter 500 MB sein.", + "uploadSuccessToast": "Video hochgeladen — wird im Hintergrund verarbeitet", + "uploadFailedToast": "Upload fehlgeschlagen: {msg}" + }, + "StoryViewer": { + "closeStory": "Story schließen", + "storyAriaLabel": "Story von {petName}", + "sheetTitleZero": "Noch keine Aufrufe", + "sheetTitleSingular": "Von 1 Tier gesehen", + "sheetTitlePlural": "Von {count} Tieren gesehen", + "noViewersYet": "Noch niemand hat diese Story angesehen." + }, + "StoryTray": { + "storiesAriaLabel": "Storys", + "addStory": "Story hinzufügen", + "newStoryTitle": "Neue Story" + }, + "RepostCard": { + "pawedBackBy": "Paw-Back von" } } diff --git a/messages/en.json b/messages/en.json index 6603212..f9e86c0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -234,7 +234,8 @@ "toastDeleted": "Entry deleted", "toastError": "Failed to save weight entry", "toastDeleteError": "Failed to delete entry", - "toastValidation": "Please fill in date and a valid weight" + "toastValidation": "Please fill in date and a valid weight", + "chartHistory": "History" }, "vet": { "add": "Add Visit", @@ -295,6 +296,134 @@ "toastDeleted": "Feeding info removed", "toastError": "Could not save feeding info", "toastDeleteError": "Could not remove feeding info" + }, + "share": { + "title": "Share Health Card — {petName}", + "description": "Anyone with this link can view {petName}'s health data — weight, vaccines, and vet visits. No login required.", + "validityLabel": "Validity period", + "expiryDays": "{count} days", + "generate": "Generate", + "validUntil": "Valid until", + "openPdf": "Open as PDF / Print", + "regenerateTitle": "Regenerate link", + "regenerateDesc": "Creates a new link and invalidates the current one. Useful for revoking access.", + "toastGenerateError": "Could not generate link", + "toastRegenerateSuccess": "New link created — old link is now invalid", + "toastRegenerateError": "Could not regenerate link" } + }, + "DeleteAccount": { + "trigger": "Delete account", + "warningTitle": "Delete your account?", + "warningIntro": "This action is irreversible. The following will be permanently deleted:", + "itemPets": "All pet profiles", + "itemPosts": "All posts, comments, and reactions", + "itemMedia": "All photos and videos", + "itemMessages": "All chats and messages", + "itemHealth": "All health records", + "itemAccount": "Your PawFeed account", + "cancel": "Cancel", + "understand": "I understand, continue", + "confirmTitle": "Confirmation required", + "confirmIntro": "Enter the name of one of your pets to confirm deletion:", + "inputPlaceholder": "Enter pet name…", + "confirmDelete": "Permanently delete account", + "deleting": "Deleting account…", + "doneTitle": "Account successfully deleted.", + "doneRedirect": "You're being redirected…", + "unknownError": "Unknown error", + "genericError": "Something went wrong" + }, + "NotificationsPage": { + "title": "Notifications", + "emptyTitle": "No notifications yet", + "emptyDesc": "When pets follow you or react to your posts, you'll see it here.", + "typeFollow": "started following you", + "typeReaction": "reacted to your post", + "commentedPrefix": "commented:", + "typeCommentNoBody": "commented on your post", + "typeMessage": "sent you a message", + "typeMention": "mentioned you in a comment", + "typePawBack": "shared your post (Paw-Back)", + "typeBirthday": "🎂 Happy Birthday! Create an anniversary post.", + "typeAdoptionDay": "🏠 Today is adoption day! Celebrate the anniversary." + }, + "PhotoPostForm": { + "captionPlaceholder": "Write a caption…", + "captionError": "Caption must be 500 characters or fewer.", + "charactersRemaining": "{count} characters remaining", + "shareCta": "Share Post", + "sharing": "Sharing…", + "successToast": "Post shared", + "errorToast": "Couldn't share your post. Please try again.", + "noPetSelected": "No active pet selected.", + "noPhoto": "Please add at least one photo." + }, + "StoryForm": { + "addPhoto": "Add a photo", + "disappearsNote": "Stories disappear after 24 hours", + "changePhoto": "Change photo", + "postCta": "Post Story", + "posting": "Posting…", + "successToast": "Story posted", + "errorToast": "Couldn't post your story. Please try again.", + "noPetSelected": "No active pet selected.", + "fileTooLarge": "Photo must be under 10 MB.", + "invalidType": "Upload a JPG, PNG, or WebP image.", + "uploadFailed": "Upload failed — please try again." + }, + "MilestoneForm": { + "questionLabel": "What's the milestone?", + "selectPlaceholder": "Select a milestone…", + "litterDetailsLabel": "Litter details", + "optional": "(optional)", + "litterPlaceholder": "e.g. 5 healthy kittens, born 13 June…", + "charactersRemaining": "{count} characters remaining", + "photoLabel": "Photo", + "addPhoto": "Add a photo", + "change": "Change", + "noteLabel": "Add a note", + "notePlaceholder": "Add a note… (optional)", + "shareCta": "Share Milestone", + "sharing": "Sharing…", + "successToast": "Milestone shared", + "errorToast": "Couldn't share this milestone. Please try again.", + "fileTooLarge": "Photo must be under 10 MB.", + "invalidType": "Upload a JPG, PNG, or WebP image.", + "uploadFailed": "Upload failed — please try again." + }, + "VideoUploadForm": { + "selectVideo": "Select a video", + "formatHint": "MP4, MOV or WebM · up to 500 MB · max 3 minutes", + "selectVideoAria": "Select video", + "changeVideo": "Choose a different video", + "captionLabel": "Caption", + "captionPlaceholder": "Write a caption…", + "charactersLeft": "{count} left", + "uploading": "Uploading…", + "processingDone": "Uploaded — video is being processed", + "uploadFailedGeneric": "Upload failed — please try again", + "cancel": "Cancel", + "postVideo": "Post video", + "invalidType": "Upload an MP4, MOV, or WebM video.", + "fileTooLarge": "Video must be under 500 MB.", + "uploadSuccessToast": "Video uploaded — processing in the background", + "uploadFailedToast": "Upload failed: {msg}" + }, + "StoryViewer": { + "closeStory": "Close story", + "storyAriaLabel": "{petName}'s story", + "sheetTitleZero": "No views yet", + "sheetTitleSingular": "Seen by 1 pet", + "sheetTitlePlural": "Seen by {count} pets", + "noViewersYet": "No one has viewed this story yet." + }, + "StoryTray": { + "storiesAriaLabel": "Stories", + "addStory": "Add story", + "newStoryTitle": "New Story" + }, + "RepostCard": { + "pawedBackBy": "Pawed-back by" } } diff --git a/src/app/(app)/notifications/page.tsx b/src/app/(app)/notifications/page.tsx index 94c3d57..2b97f44 100644 --- a/src/app/(app)/notifications/page.tsx +++ b/src/app/(app)/notifications/page.tsx @@ -34,6 +34,7 @@ export default function NotificationsPage() { const router = useRouter(); const { activePetId } = useActivePet(); const trt = useTranslations("RelativeTime"); + const t = useTranslations("NotificationsPage"); const formatRelativeTime = (date: Date | string) => formatRelativeCompact(date, trt); const { data, isLoading } = useQuery({ @@ -81,7 +82,7 @@ export default function NotificationsPage() {

{isLoading && ( @@ -101,9 +102,9 @@ export default function NotificationsPage() { {!isLoading && notifications.length === 0 && (
-

No notifications yet

+

{t("emptyTitle")}

- When pets follow you or react to your posts, you'll see it here. + {t("emptyDesc")}

)} @@ -164,22 +165,22 @@ export default function NotificationsPage() { )}{" "} - {notif.type === "FOLLOW" && "started following you"} - {notif.type === "REACTION" && "reacted to your post"} + {notif.type === "FOLLOW" && t("typeFollow")} + {notif.type === "REACTION" && t("typeReaction")} {notif.type === "COMMENT" && notif.comment && ( <> - commented: “ + {t("commentedPrefix")} “ {notif.comment.body.slice(0, 60)} {notif.comment.body.length > 60 ? "…" : ""} ” )} - {notif.type === "COMMENT" && !notif.comment && "commented on your post"} - {notif.type === "MESSAGE" && "sent you a message"} - {notif.type === "MENTION" && "mentioned you in a comment"} - {notif.type === "PAW_BACK" && "shared your post (Paw-Back)"} - {notif.type === "BIRTHDAY" && "🎂 Alles Gute zum Geburtstag! Erstelle einen Jubiläums-Post."} - {notif.type === "ADOPTION_DAY" && "🏠 Heute ist der Adoptionstag! Feiere den Jahrestag."} + {notif.type === "COMMENT" && !notif.comment && t("typeCommentNoBody")} + {notif.type === "MESSAGE" && t("typeMessage")} + {notif.type === "MENTION" && t("typeMention")} + {notif.type === "PAW_BACK" && t("typePawBack")} + {notif.type === "BIRTHDAY" && t("typeBirthday")} + {notif.type === "ADOPTION_DAY" && t("typeAdoptionDay")}

diff --git a/src/app/health-card/[token]/page.tsx b/src/app/health-card/[token]/page.tsx index 6bd70aa..7022f30 100644 --- a/src/app/health-card/[token]/page.tsx +++ b/src/app/health-card/[token]/page.tsx @@ -183,7 +183,10 @@ export default async function HealthCardPage({ params, searchParams }: PageProps {pet.weightLogs.length >= 2 && ( - + // This page is entirely hardcoded German (not part of the i18n audit + // scope — unlike HealthDashboard/WeightChart's in-app usage), so pass + // a fixed locale here rather than the viewer's locale to stay consistent. + )}

)} diff --git a/src/components/account/DeleteAccountDialog.tsx b/src/components/account/DeleteAccountDialog.tsx index 6fd7674..dbbf6f2 100644 --- a/src/components/account/DeleteAccountDialog.tsx +++ b/src/components/account/DeleteAccountDialog.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { Trash2, TriangleAlert, Loader2, CheckCircle2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -19,6 +20,7 @@ type Props = { }; export function DeleteAccountDialog({ petNames, defaultOpen }: Props) { + const t = useTranslations("DeleteAccount"); // Lazy initial state (not an effect) — lets the legal-update banner // deep-link straight into this dialog via ?deleteAccount=1. const [open, setOpen] = useState(() => !!defaultOpen); @@ -56,7 +58,7 @@ export function DeleteAccountDialog({ petNames, defaultOpen }: Props) { if (!res.ok) { const data = (await res.json()) as { error?: string }; - throw new Error(data.error ?? "Unbekannter Fehler"); + throw new Error(data.error ?? t("unknownError")); } setStep("done"); @@ -64,11 +66,20 @@ export function DeleteAccountDialog({ petNames, defaultOpen }: Props) { window.location.replace("https://pawfeed.org"); }, 1500); } catch (err) { - setError(err instanceof Error ? err.message : "Etwas ist schiefgelaufen"); + setError(err instanceof Error ? err.message : t("genericError")); setStep("confirm"); } }; + const deleteItems = [ + t("itemPets"), + t("itemPosts"), + t("itemMedia"), + t("itemMessages"), + t("itemHealth"), + t("itemAccount"), + ]; + return ( <> @@ -86,23 +97,16 @@ export function DeleteAccountDialog({ petNames, defaultOpen }: Props) { - Konto wirklich löschen? + {t("warningTitle")}

- Diese Aktion ist unwiderruflich. Folgendes wird dauerhaft gelöscht: + {t("warningIntro")}