diff --git a/messages/de.json b/messages/de.json
index 890fe581f..f46fcbbc0 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Noch keine Erfolge freigeschaltet.",
"pointsValue": "{points} Punkte",
"metricCount": "{count}",
- "metricDays": "{count} Tage",
+ "metricDaysOne": "{count} Tag",
+ "metricDaysFew": "{count} Tage",
+ "metricDaysOther": "{count} Tage",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Diese Episode konnte nicht geladen werden.",
"backToList": "Zurück zum Journal",
"detail": {
- "note": "Note"
+ "note": "Notiz"
},
"edit": {
"title": "Episode bearbeiten",
@@ -12019,8 +12021,7 @@
"title": "Praxen und Ärzte",
"description": "Dein eigenes Adressbuch — einmal tippen, überall auswählen.",
"linkLabel": "Adressbuch",
- "add": "Eintrag",
- "searchPlaceholder": "Name, Praxis oder Fachrichtung suchen",
+ "searchPlaceholder": "Name, Praxis oder Fach suchen",
"loadError": "Adressbuch konnte nicht geladen werden.",
"emptyTitle": "Noch kein Eintrag",
"emptyDescription": "Lege eine Praxis an, damit du sie beim Termin auswählen kannst.",
diff --git a/messages/en.json b/messages/en.json
index c502bab8a..d4d5ba3b5 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "No achievements unlocked yet.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} days",
+ "metricDaysOne": "{count} day",
+ "metricDaysFew": "{count} days",
+ "metricDaysOther": "{count} days",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "Practices and doctors",
"description": "Your own address book — type a practice once, pick it everywhere.",
"linkLabel": "Address book",
- "add": "Entry",
"searchPlaceholder": "Search name, practice or specialty",
"loadError": "The address book could not be loaded.",
"emptyTitle": "No entry yet",
diff --git a/messages/es.json b/messages/es.json
index 759191ed4..d9e70476c 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Aún no has desbloqueado ningún logro.",
"pointsValue": "{points} puntos",
"metricCount": "{count}",
- "metricDays": "{count} días",
+ "metricDaysOne": "{count} día",
+ "metricDaysFew": "{count} días",
+ "metricDaysOther": "{count} días",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "No se pudo cargar este episodio.",
"backToList": "Volver al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Editar episodio",
@@ -12019,7 +12021,6 @@
"title": "Consultas y médicos",
"description": "Tu propia agenda: escribe una consulta una vez y elígela en todas partes.",
"linkLabel": "Agenda",
- "add": "Entrada",
"searchPlaceholder": "Buscar nombre, consulta o especialidad",
"loadError": "No se pudo cargar la agenda.",
"emptyTitle": "Todavía no hay entradas",
diff --git a/messages/fr.json b/messages/fr.json
index 198f10fce..ee4cb1ec6 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Aucune réussite débloquée pour l’instant.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} jours",
+ "metricDaysOne": "{count} jour",
+ "metricDaysFew": "{count} jours",
+ "metricDaysOther": "{count} jours",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "Cabinets et médecins",
"description": "Votre carnet d'adresses : saisissez un cabinet une fois, choisissez-le partout.",
"linkLabel": "Carnet d'adresses",
- "add": "Entrée",
"searchPlaceholder": "Rechercher un nom, un cabinet ou une spécialité",
"loadError": "Impossible de charger le carnet d'adresses.",
"emptyTitle": "Aucune entrée",
diff --git a/messages/it.json b/messages/it.json
index d6dad2aac..62fbdb924 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Nessun obiettivo ancora sbloccato.",
"pointsValue": "{points} punti",
"metricCount": "{count}",
- "metricDays": "{count} giorni",
+ "metricDaysOne": "{count} giorno",
+ "metricDaysFew": "{count} giorni",
+ "metricDaysOther": "{count} giorni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Impossibile caricare questo episodio.",
"backToList": "Torna al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Modifica episodio",
@@ -12019,7 +12021,6 @@
"title": "Studi e medici",
"description": "La tua rubrica: scrivi uno studio una volta e selezionalo ovunque.",
"linkLabel": "Rubrica",
- "add": "Voce",
"searchPlaceholder": "Cerca nome, studio o specializzazione",
"loadError": "Impossibile caricare la rubrica.",
"emptyTitle": "Nessuna voce",
diff --git a/messages/ko.json b/messages/ko.json
index edf1f1df2..e0a23c4be 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "아직 달성한 업적이 없어요.",
"pointsValue": "{points}포인트",
"metricCount": "{count}",
- "metricDays": "{count}일",
+ "metricDaysOne": "{count}일",
+ "metricDaysFew": "{count}일",
+ "metricDaysOther": "{count}일",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "병원과 의사",
"description": "나만의 주소록이에요 — 병원을 한 번만 입력하면 어디서든 골라 쓸 수 있어요.",
"linkLabel": "주소록",
- "add": "항목",
"searchPlaceholder": "이름, 병원, 진료과 검색",
"loadError": "주소록을 불러오지 못했어요.",
"emptyTitle": "아직 항목이 없어요",
diff --git a/messages/pl.json b/messages/pl.json
index b060b3a48..5029c9cbf 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Nie odblokowano jeszcze żadnego osiągnięcia.",
"pointsValue": "{points} punktów",
"metricCount": "{count}",
- "metricDays": "{count} dni",
+ "metricDaysOne": "{count} dzień",
+ "metricDaysFew": "{count} dni",
+ "metricDaysOther": "{count} dni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Nie udało się wczytać tego epizodu.",
"backToList": "Powrót do dziennika",
"detail": {
- "note": "Note"
+ "note": "Notatka"
},
"edit": {
"title": "Edytuj epizod",
@@ -12019,7 +12021,6 @@
"title": "Gabinety i lekarze",
"description": "Twoja własna książka adresowa — wpisz gabinet raz, wybieraj go wszędzie.",
"linkLabel": "Książka adresowa",
- "add": "Wpis",
"searchPlaceholder": "Szukaj nazwiska, gabinetu lub specjalizacji",
"loadError": "Nie udało się wczytać książki adresowej.",
"emptyTitle": "Brak wpisów",
diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx
index 257acee02..6c0fe99b0 100644
--- a/src/app/achievements/page.tsx
+++ b/src/app/achievements/page.tsx
@@ -35,7 +35,7 @@ import { PageHeader } from "@/components/ui/page-header";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { useAuth } from "@/hooks/use-auth";
-import { useTranslations } from "@/lib/i18n/context";
+import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { formatDate } from "@/lib/format";
import { useAchievementsQuery } from "@/lib/queries/use-achievements-query";
import {
@@ -81,12 +81,13 @@ const CATEGORY_LABEL_KEY: Record = {
function formatMetric(
format: AchievementProgress["format"],
value: number,
- t: (key: string, params?: Record) => string,
+ i18n: Pick, "t" | "tCount">,
) {
if (format === "percent")
- return t("achievements.metricPercent", { count: value });
- if (format === "days") return t("achievements.metricDays", { count: value });
- return t("achievements.metricCount", { count: value });
+ return i18n.t("achievements.metricPercent", { count: value });
+ // "1 Tag", not "1 Tage": the day count takes the plural form.
+ if (format === "days") return i18n.tCount("achievements.metricDays", value);
+ return i18n.t("achievements.metricCount", { count: value });
}
/**
@@ -127,9 +128,11 @@ export function groupByCategory(
interface AchievementCardProps {
achievement: AchievementProgress;
t: ReturnType["t"];
+ tCount: ReturnType["tCount"];
}
-function AchievementCard({ achievement, t }: AchievementCardProps) {
+function AchievementCard({ achievement, t, tCount }: AchievementCardProps) {
+ const i18n = { t, tCount };
const Icon = iconMap[achievement.icon] ?? Star;
const unlocked = achievement.unlocked;
@@ -191,7 +194,7 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
{t(achievement.titleKey)}
-
+
{t(achievement.descriptionKey)}
@@ -215,8 +218,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
{t("achievements.criterionHint", {
- current: formatMetric(achievement.format, achievement.current, t),
- target: formatMetric(achievement.format, achievement.target, t),
+ current: formatMetric(
+ achievement.format,
+ achievement.current,
+ i18n,
+ ),
+ target: formatMetric(
+ achievement.format,
+ achievement.target,
+ i18n,
+ ),
})}
{/* v1.4.33 IW9 — aria-label so the bar carries an accessible name. */}
@@ -224,8 +235,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
value={achievement.progressPercent}
className="h-1.5"
aria-label={t("achievements.criterionHint", {
- current: formatMetric(achievement.format, achievement.current, t),
- target: formatMetric(achievement.format, achievement.target, t),
+ current: formatMetric(
+ achievement.format,
+ achievement.current,
+ i18n,
+ ),
+ target: formatMetric(
+ achievement.format,
+ achievement.target,
+ i18n,
+ ),
})}
/>
@@ -259,7 +278,9 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
export default function AchievementsPage() {
const { user, isAuthenticated, isLoading: authLoading } = useAuth();
- const { t } = useTranslations();
+ const { t, tCount } = useTranslations();
+ const fmt = useFormatters();
+ const i18n = { t, tCount };
// v1.18.0 — the achievements module gate. When the account has the
// module turned off the whole page disappears: an empty state stands in
@@ -335,9 +356,9 @@ export default function AchievementsPage() {
{t("achievements.points")}
- {summary?.earnedPoints ?? 0}
+ {fmt.integer(summary?.earnedPoints ?? 0)}
- / {summary?.totalPoints ?? 0}
+ / {fmt.integer(summary?.totalPoints ?? 0)}