diff --git a/src/presentation/components/features/event/ConsumptionSummary.tsx b/src/presentation/components/features/event/ConsumptionSummary.tsx
index f5ab461..6ce4b45 100644
--- a/src/presentation/components/features/event/ConsumptionSummary.tsx
+++ b/src/presentation/components/features/event/ConsumptionSummary.tsx
@@ -43,7 +43,7 @@ export function ConsumptionSummary({ userId }: { userId: string }) {
{blocks.detail.map((d, idx) => (
-
- • {d.item} · {d.quantity} {displayUnit(d.unit, t)}
+ • {d.item} · {d.quantity} {displayUnit(d.unit, t, d.quantity)}
))}
diff --git a/src/presentation/components/features/event/ExpenseForm.tsx b/src/presentation/components/features/event/ExpenseForm.tsx
index ef54391..33c6f92 100644
--- a/src/presentation/components/features/event/ExpenseForm.tsx
+++ b/src/presentation/components/features/event/ExpenseForm.tsx
@@ -275,7 +275,7 @@ export function ExpenseForm({
{listItems.map((p) => {
const checked = p.id in links
const remaining = Math.max(1, p.totalQuantity - boughtByOthers(p.id))
- const unit = displayUnit(p.unit, t)
+ const unit = displayUnit(p.unit, t, p.totalQuantity)
const assignee = p.assignedTo
? (event.users.find((u) => u.id === p.assignedTo) ?? null)
: null
diff --git a/src/presentation/components/features/event/PurchasesTab.tsx b/src/presentation/components/features/event/PurchasesTab.tsx
index f883c85..895aba7 100644
--- a/src/presentation/components/features/event/PurchasesTab.tsx
+++ b/src/presentation/components/features/event/PurchasesTab.tsx
@@ -342,7 +342,7 @@ export function PurchasesTab() {
{t(p.kind === 'bring' ? 'purchases.totalToBring' : 'purchases.totalQuantity', {
n: Math.round(p.totalQuantity * 100) / 100,
- unit: displayUnit(p.unit, t),
+ unit: displayUnit(p.unit, t, p.totalQuantity),
})}
@@ -384,7 +384,7 @@ export function PurchasesTab() {
{t('purchases.boughtProgress', {
n: round2(bought),
total: round2(total),
- unit: displayUnit(p.unit, t),
+ unit: displayUnit(p.unit, t, total),
})}
diff --git a/src/presentation/i18n/locales/ca/translation.json b/src/presentation/i18n/locales/ca/translation.json
index 15b3bf9..fb3a4ae 100644
--- a/src/presentation/i18n/locales/ca/translation.json
+++ b/src/presentation/i18n/locales/ca/translation.json
@@ -224,13 +224,27 @@
"update": "Desa els canvis",
"units": {
"units": "unitats",
+ "units_one": "unitat",
+ "units_other": "unitats",
"bottles": "ampolles",
+ "bottles_one": "ampolla",
+ "bottles_other": "ampolles",
"cans": "llaunes",
+ "cans_one": "llauna",
+ "cans_other": "llaunes",
"kg": "kg",
"liters": "litres",
+ "liters_one": "litre",
+ "liters_other": "litres",
"grams": "grams",
+ "grams_one": "gram",
+ "grams_other": "grams",
"bag": "bosses",
+ "bag_one": "bossa",
+ "bag_other": "bosses",
"tray": "safates",
+ "tray_one": "safata",
+ "tray_other": "safates",
"single": "Unitat única"
},
"unitPlaceholder": "ex. ampolles, garrafa 8L",
diff --git a/src/presentation/i18n/locales/en/translation.json b/src/presentation/i18n/locales/en/translation.json
index bce56bb..28fd2b8 100644
--- a/src/presentation/i18n/locales/en/translation.json
+++ b/src/presentation/i18n/locales/en/translation.json
@@ -224,13 +224,27 @@
"update": "Save changes",
"units": {
"units": "units",
+ "units_one": "unit",
+ "units_other": "units",
"bottles": "bottles",
+ "bottles_one": "bottle",
+ "bottles_other": "bottles",
"cans": "cans",
+ "cans_one": "can",
+ "cans_other": "cans",
"kg": "kg",
"liters": "liters",
+ "liters_one": "liter",
+ "liters_other": "liters",
"grams": "grams",
+ "grams_one": "gram",
+ "grams_other": "grams",
"bag": "bags",
+ "bag_one": "bag",
+ "bag_other": "bags",
"tray": "trays",
+ "tray_one": "tray",
+ "tray_other": "trays",
"single": "Single unit"
},
"unitPlaceholder": "e.g. bottles, garrafa 8L",
diff --git a/src/presentation/i18n/locales/es/translation.json b/src/presentation/i18n/locales/es/translation.json
index 0f51848..ff2118f 100644
--- a/src/presentation/i18n/locales/es/translation.json
+++ b/src/presentation/i18n/locales/es/translation.json
@@ -224,13 +224,27 @@
"update": "Guardar cambios",
"units": {
"units": "unidades",
+ "units_one": "unidad",
+ "units_other": "unidades",
"bottles": "botellas",
+ "bottles_one": "botella",
+ "bottles_other": "botellas",
"cans": "latas",
+ "cans_one": "lata",
+ "cans_other": "latas",
"kg": "kg",
"liters": "litros",
+ "liters_one": "litro",
+ "liters_other": "litros",
"grams": "gramos",
+ "grams_one": "gramo",
+ "grams_other": "gramos",
"bag": "bolsas",
+ "bag_one": "bolsa",
+ "bag_other": "bolsas",
"tray": "bandejas",
+ "tray_one": "bandeja",
+ "tray_other": "bandejas",
"single": "Unidad única"
},
"unitPlaceholder": "ej. botellas, garrafa 8L",
diff --git a/src/presentation/i18n/locales/eu/translation.json b/src/presentation/i18n/locales/eu/translation.json
index 1b6818c..a03f0dc 100644
--- a/src/presentation/i18n/locales/eu/translation.json
+++ b/src/presentation/i18n/locales/eu/translation.json
@@ -224,13 +224,27 @@
"update": "Aldaketak gorde",
"units": {
"units": "unitateak",
+ "units_one": "unitate",
+ "units_other": "unitateak",
"bottles": "botilak",
+ "bottles_one": "botila",
+ "bottles_other": "botilak",
"cans": "latak",
+ "cans_one": "lata",
+ "cans_other": "latak",
"kg": "kg",
"liters": "litroak",
+ "liters_one": "litro",
+ "liters_other": "litroak",
"grams": "gramoak",
+ "grams_one": "gramo",
+ "grams_other": "gramoak",
"bag": "poltsak",
+ "bag_one": "poltsa",
+ "bag_other": "poltsak",
"tray": "erretiluak",
+ "tray_one": "erretilu",
+ "tray_other": "erretiluak",
"single": "Unitate bakarra"
},
"unitPlaceholder": "adib. botilak, garrafa 8L",
diff --git a/src/presentation/i18n/locales/gl/translation.json b/src/presentation/i18n/locales/gl/translation.json
index 5bdf8e8..b4e9e37 100644
--- a/src/presentation/i18n/locales/gl/translation.json
+++ b/src/presentation/i18n/locales/gl/translation.json
@@ -224,13 +224,27 @@
"update": "Gardar cambios",
"units": {
"units": "unidades",
+ "units_one": "unidade",
+ "units_other": "unidades",
"bottles": "botellas",
+ "bottles_one": "botella",
+ "bottles_other": "botellas",
"cans": "latas",
+ "cans_one": "lata",
+ "cans_other": "latas",
"kg": "kg",
"liters": "litros",
+ "liters_one": "litro",
+ "liters_other": "litros",
"grams": "gramos",
+ "grams_one": "gramo",
+ "grams_other": "gramos",
"bag": "bolsas",
+ "bag_one": "bolsa",
+ "bag_other": "bolsas",
"tray": "bandexas",
+ "tray_one": "bandexa",
+ "tray_other": "bandexas",
"single": "Unidade única"
},
"unitPlaceholder": "ex. botellas, garrafa 8L",
diff --git a/src/presentation/i18n/locales/va/translation.json b/src/presentation/i18n/locales/va/translation.json
index fdc768c..dbe8e61 100644
--- a/src/presentation/i18n/locales/va/translation.json
+++ b/src/presentation/i18n/locales/va/translation.json
@@ -224,13 +224,27 @@
"update": "Guarda els canvis",
"units": {
"units": "unitats",
+ "units_one": "unitat",
+ "units_other": "unitats",
"bottles": "botelles",
+ "bottles_one": "botella",
+ "bottles_other": "botelles",
"cans": "llandes",
+ "cans_one": "llanda",
+ "cans_other": "llandes",
"kg": "kg",
"liters": "litres",
+ "liters_one": "litre",
+ "liters_other": "litres",
"grams": "grams",
+ "grams_one": "gram",
+ "grams_other": "grams",
"bag": "bosses",
+ "bag_one": "bossa",
+ "bag_other": "bosses",
"tray": "safates",
+ "tray_one": "safata",
+ "tray_other": "safates",
"single": "Unitat única"
},
"unitPlaceholder": "ex. botelles, garrafa 8L",
diff --git a/src/presentation/utils/formatShoppingListText.ts b/src/presentation/utils/formatShoppingListText.ts
index 56888fd..f7bdc75 100644
--- a/src/presentation/utils/formatShoppingListText.ts
+++ b/src/presentation/utils/formatShoppingListText.ts
@@ -1,7 +1,7 @@
import type { EventSnapshot } from '@/domain/entities/Event'
import type { PurchaseSnapshot } from '@/domain/entities/Purchase'
import { displayUnit } from '@/presentation/utils/units'
-type T = (key: string, vars?: Record) => string
+type T = (key: string, vars?: Record) => string
function formatQty(n: number): string {
return Number.isInteger(n) ? String(n) : String(Math.round(n * 10) / 10)
@@ -33,7 +33,7 @@ function renderBuyLine(event: EventSnapshot, p: PurchaseSnapshot, t: T): string
const checkmark = p.purchased ? ' ✅' : ''
const bought = boughtQtyFor(event, p.id)
const total = p.totalQuantity
- const unit = displayUnit(p.unit, t)
+ const unit = displayUnit(p.unit, t, total)
return ` • ${name}${assigneePart} · ${formatQty(bought)}/${formatQty(total)} ${unit}${checkmark}`
}
diff --git a/src/presentation/utils/units.ts b/src/presentation/utils/units.ts
index c3e9d45..f7e8712 100644
--- a/src/presentation/utils/units.ts
+++ b/src/presentation/utils/units.ts
@@ -5,7 +5,16 @@ export const SELECTABLE_UNITS = [...VALID_UNITS, SHARED_UNIT] as const
const KNOWN = new Set(SELECTABLE_UNITS)
-/** Translate a known unit key; fall back to the raw (free-text) unit otherwise. */
-export function displayUnit(unit: string, t: (key: string) => string): string {
- return KNOWN.has(unit) ? t(`purchases.form.units.${unit}`) : unit
+type T = (key: string, opts?: Record) => string
+
+/**
+ * Translate a known unit key, picking singular/plural based on `count` via i18next's
+ * built-in plural support (_one / _other suffixes). Falls back to the raw (free-text)
+ * unit otherwise. When `count` is omitted, returns the base plural form (used for
+ * dropdown labels).
+ */
+export function displayUnit(unit: string, t: T, count?: number): string {
+ if (!KNOWN.has(unit)) return unit
+ const key = `purchases.form.units.${unit}`
+ return count === undefined ? t(key) : t(key, { count })
}
diff --git a/tests/presentation/utils/formatShoppingListText.test.ts b/tests/presentation/utils/formatShoppingListText.test.ts
index c72341b..3d3ed07 100644
--- a/tests/presentation/utils/formatShoppingListText.test.ts
+++ b/tests/presentation/utils/formatShoppingListText.test.ts
@@ -12,11 +12,11 @@ const STUB_TRANSLATIONS: Record = {
'purchases.form.units.single': 'single',
}
-const t = (key: string, vars?: Record): string => {
+const t = (key: string, vars?: Record): string => {
let value = STUB_TRANSLATIONS[key] ?? key
if (vars) {
for (const [k, v] of Object.entries(vars)) {
- value = value.replace(`{{${k}}}`, v)
+ value = value.replace(`{{${k}}}`, String(v))
}
}
return value