Fix: use pluralization correctly (#445)

* feat(billing): add i18n pluralization support for billing unit names

Use i18next plural feature to properly translate and pluralize unit names
in billing plans (e.g., "member" vs "members"). This ensures correct
grammar for phrases like "Up to 4 members included in the plan" and
enables proper translation of unit names in non-English locales.

* fix(billing): handle 'unlimited' tier values in pluralization

Add getSafeCount helper to safely convert tier values to numbers,
preventing NaN when 'unlimited' values are passed to pluralization.
Falls back to plural form for 'unlimited' or invalid values.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Giancarlo Buomprisco
2026-01-18 10:39:40 +01:00
committed by GitHub
parent f3ce70a5b6
commit bebd56238b
2 changed files with 36 additions and 10 deletions

View File

@@ -19,7 +19,8 @@ export function LineItemDetails(
selectedInterval?: string | undefined;
}>,
) {
const locale = useTranslation().i18n.language;
const { t, i18n } = useTranslation();
const locale = i18n.language;
const currencyCode = props?.currency.toLowerCase();
return (
@@ -115,7 +116,7 @@ export function LineItemDetails(
<Trans
i18nKey={'billing:perUnit'}
values={{
unit: item.unit,
unit: t(`billing:units.${item.unit}`, { count: 1 }),
}}
/>
</span>
@@ -171,7 +172,7 @@ export function LineItemDetails(
<Trans
i18nKey={'billing:perUnit'}
values={{
unit: item.unit,
unit: t(`billing:units.${item.unit}`, { count: 1 }),
}}
/>
</span>
@@ -223,8 +224,17 @@ function Tiers({
currency: string;
item: z.infer<typeof LineItemSchema>;
}) {
const unit = item.unit;
const locale = useTranslation().i18n.language;
const unitKey = `billing:units.${item.unit}`;
const { t, i18n } = useTranslation();
const locale = i18n.language;
// Helper to safely convert tier values to numbers for pluralization
// Falls back to plural form (2) for 'unlimited' values
const getSafeCount = (value: number | 'unlimited' | string): number => {
if (value === 'unlimited') return 2;
const num = typeof value === 'number' ? value : Number(value);
return Number.isNaN(num) ? 2 : num;
};
const tiers = item.tiers?.map((tier, index) => {
const tiersLength = item.tiers?.length ?? 0;
@@ -257,8 +267,10 @@ function Tiers({
<Trans
i18nKey={'billing:andAbove'}
values={{
unit,
previousTier: (Number(previousTierFrom) as number) - 1,
unit: t(unitKey, {
count: getSafeCount(previousTierFrom) - 1,
}),
previousTier: getSafeCount(previousTierFrom) - 1,
}}
/>
</span>
@@ -268,7 +280,7 @@ function Tiers({
<Trans
i18nKey={'billing:forEveryUnit'}
values={{
unit,
unit: t(unitKey, { count: 1 }),
}}
/>
</span>
@@ -277,7 +289,13 @@ function Tiers({
<If condition={!isLastTier}>
<If condition={isIncluded}>
<span>
<Trans i18nKey={'billing:includedUpTo'} values={{ unit, upTo }} />
<Trans
i18nKey={'billing:includedUpTo'}
values={{
unit: t(unitKey, { count: getSafeCount(upTo) }),
upTo,
}}
/>
</span>
</If>{' '}
<If condition={!isIncluded}>
@@ -291,7 +309,11 @@ function Tiers({
<span>
<Trans
i18nKey={'billing:fromPreviousTierUpTo'}
values={{ previousTierFrom, unit, upTo }}
values={{
previousTierFrom,
unit: t(unitKey, { count: getSafeCount(previousTierFrom) }),
upTo,
}}
/>
</span>
</If>