chore: bump version to 2.23.12 and update billing localization (#449)

* chore: bump version to 2.23.12 and update billing localization

- Updated application version from 2.23.11 to 2.23.12 in package.json.
- Added new localization key for billing: "perUnitShort" to enhance user clarity in billing plans.
- Improved unit label handling in LineItemDetails and Tiers components for better internationalization support.
- Adjusted pricing table to conditionally display unit labels based on item type.

* fix(billing): update pluralization in billing localization and component logic

- Adjusted the localization key for "fromPreviousTierUpTo" to include pluralization support for unit labels.
- Enhanced the LineItemDetails component to calculate and display the range count between previous and current tiers, improving clarity in billing information.
This commit is contained in:
Giancarlo Buomprisco
2026-02-03 13:26:52 +01:00
committed by GitHub
parent 9355c0a614
commit 58f08c5f39
4 changed files with 111 additions and 23 deletions

View File

@@ -40,10 +40,11 @@
"proceedToPayment": "Proceed to Payment",
"startTrial": "Start Trial",
"perTeamMember": "Per team member",
"perUnitShort": "Per {{unit}}",
"perUnit": "Per {{unit}} usage",
"teamMembers": "Team Members",
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unitPlural }}",
"andAbove": "above {{ previousTier }} {{ unit }}",
"startingAtPriceUnit": "Starting at {{price}}/{{unit}}",
"priceUnit": "{{price}}/{{unit}}",

View File

@@ -1,6 +1,6 @@
{
"name": "next-supabase-saas-kit-turbo",
"version": "2.23.11",
"version": "2.23.12",
"private": true,
"sideEffects": false,
"engines": {

View File

@@ -17,12 +17,41 @@ export function LineItemDetails(
lineItems: z.infer<typeof LineItemSchema>[];
currency: string;
selectedInterval?: string | undefined;
alwaysDisplayMonthlyPrice?: boolean;
}>,
) {
const { t, i18n } = useTranslation();
const locale = i18n.language;
const currencyCode = props?.currency.toLowerCase();
const shouldDisplayMonthlyPrice =
props.alwaysDisplayMonthlyPrice && props.selectedInterval === 'year';
const getUnitLabel = (unit: string | undefined, count: number) => {
if (!unit) {
return '';
}
const i18nKey = `billing:units.${unit}`;
if (!i18n.exists(i18nKey)) {
return unit;
}
return t(i18nKey, {
count,
defaultValue: unit,
});
};
const getDisplayCost = (cost: number, hasTiers: boolean) => {
if (shouldDisplayMonthlyPrice && !hasTiers) {
return cost / 12;
}
return cost;
};
return (
<div className={'flex flex-col space-y-1'}>
{props.lineItems.map((item, index) => {
@@ -68,6 +97,12 @@ export function LineItemDetails(
</If>
);
const unit =
item.unit ?? (item.type === 'per_seat' ? 'member' : undefined);
const hasTiers = Boolean(item.tiers?.length);
const isDefaultSeatUnit = unit === 'member';
const FlatFee = () => (
<div className={'flex flex-col'}>
<div className={cn(className, 'space-x-1')}>
@@ -99,7 +134,7 @@ export function LineItemDetails(
<span className={'text-xs font-semibold'}>
{formatCurrency({
currencyCode,
value: item.cost,
value: getDisplayCost(item.cost, hasTiers),
locale,
})}
</span>
@@ -116,14 +151,14 @@ export function LineItemDetails(
<Trans
i18nKey={'billing:perUnit'}
values={{
unit: t(`billing:units.${item.unit}`, { count: 1 }),
unit: getUnitLabel(unit, 1),
}}
/>
</span>
</span>
</span>
<Tiers item={item} currency={props.currency} />
<Tiers item={item} currency={props.currency} unit={unit} />
</If>
</div>
);
@@ -135,16 +170,27 @@ export function LineItemDetails(
<PlusSquare className={'w-3'} />
<span>
<Trans i18nKey={'billing:perTeamMember'} />
<If
condition={Boolean(unit) && !isDefaultSeatUnit}
fallback={<Trans i18nKey={'billing:perTeamMember'} />}
>
<Trans
i18nKey={'billing:perUnitShort'}
values={{
unit: getUnitLabel(unit, 1),
}}
/>
</If>
</span>
<span>-</span>
<If condition={!item.tiers?.length}>
<span>-</span>
<span className={'font-semibold'}>
{formatCurrency({
currencyCode,
value: item.cost,
value: getDisplayCost(item.cost, hasTiers),
locale,
})}
</span>
@@ -155,7 +201,7 @@ export function LineItemDetails(
<SetupFee />
<If condition={item.tiers?.length}>
<Tiers item={item} currency={props.currency} />
<Tiers item={item} currency={props.currency} unit={unit} />
</If>
</div>
);
@@ -172,7 +218,7 @@ export function LineItemDetails(
<Trans
i18nKey={'billing:perUnit'}
values={{
unit: t(`billing:units.${item.unit}`, { count: 1 }),
unit: getUnitLabel(unit, 1),
}}
/>
</span>
@@ -185,7 +231,7 @@ export function LineItemDetails(
<span className={'font-semibold'}>
{formatCurrency({
currencyCode,
value: item.cost,
value: getDisplayCost(item.cost, hasTiers),
locale,
})}
</span>
@@ -196,7 +242,7 @@ export function LineItemDetails(
{/* If there are tiers, we render them as a list */}
<If condition={item.tiers?.length}>
<Tiers item={item} currency={props.currency} />
<Tiers item={item} currency={props.currency} unit={unit} />
</If>
</div>
);
@@ -220,11 +266,12 @@ export function LineItemDetails(
function Tiers({
currency,
item,
unit,
}: {
currency: string;
item: z.infer<typeof LineItemSchema>;
unit?: string;
}) {
const unitKey = `billing:units.${item.unit}`;
const { t, i18n } = useTranslation();
const locale = i18n.language;
@@ -236,6 +283,15 @@ function Tiers({
return Number.isNaN(num) ? 2 : num;
};
const getUnitLabel = (count: number) => {
if (!unit) return '';
return t(`billing:units.${unit}`, {
count,
defaultValue: unit,
});
};
const tiers = item.tiers?.map((tier, index) => {
const tiersLength = item.tiers?.length ?? 0;
const previousTier = item.tiers?.[index - 1];
@@ -249,6 +305,13 @@ function Tiers({
: previousTier.upTo + 1 || 0;
const upTo = tier.upTo;
const previousTierUpTo =
typeof previousTier?.upTo === 'number' ? previousTier.upTo : undefined;
const currentTierUpTo = typeof upTo === 'number' ? upTo : undefined;
const rangeCount =
previousTierUpTo !== undefined && currentTierUpTo !== undefined
? currentTierUpTo - previousTierUpTo
: undefined;
const isIncluded = tier.cost === 0;
return (
@@ -267,9 +330,7 @@ function Tiers({
<Trans
i18nKey={'billing:andAbove'}
values={{
unit: t(unitKey, {
count: getSafeCount(previousTierFrom) - 1,
}),
unit: getUnitLabel(getSafeCount(previousTierFrom) - 1),
previousTier: getSafeCount(previousTierFrom) - 1,
}}
/>
@@ -280,7 +341,7 @@ function Tiers({
<Trans
i18nKey={'billing:forEveryUnit'}
values={{
unit: t(unitKey, { count: 1 }),
unit: getUnitLabel(1),
}}
/>
</span>
@@ -292,7 +353,7 @@ function Tiers({
<Trans
i18nKey={'billing:includedUpTo'}
values={{
unit: t(unitKey, { count: getSafeCount(upTo) }),
unit: getUnitLabel(getSafeCount(upTo)),
upTo,
}}
/>
@@ -311,8 +372,9 @@ function Tiers({
i18nKey={'billing:fromPreviousTierUpTo'}
values={{
previousTierFrom,
unit: t(unitKey, { count: getSafeCount(previousTierFrom) }),
upTo,
unit: getUnitLabel(1),
unitPlural: getUnitLabel(getSafeCount(rangeCount ?? upTo)),
upTo: rangeCount ?? upTo,
}}
/>
</span>

View File

@@ -154,10 +154,22 @@ function PricingItem(
};
}>,
) {
const { t, i18n } = useTranslation();
const highlighted = props.product.highlighted ?? false;
const lineItem = props.primaryLineItem!;
const isCustom = props.plan.custom ?? false;
const i18nKey = `billing:units.${lineItem.unit}`;
const unitLabel = lineItem?.unit
? i18n.exists(i18nKey) ? t(i18nKey, {
count: 1,
defaultValue: lineItem.unit,
}) : lineItem.unit
: '';
const isDefaultSeatUnit = lineItem?.unit === 'member';
// we exclude flat line items from the details since
// it doesn't need further explanation
const lineItemsToDisplay = props.plan.lineItems.filter((item) => {
@@ -259,14 +271,26 @@ function PricingItem(
<span
className={cn(
`animate-in slide-in-from-left-4 fade-in text-sm capitalize`,
`animate-in slide-in-from-left-4 fade-in text-xs capitalize`,
)}
>
<If condition={lineItem?.type === 'per_seat'}>
<Trans i18nKey={'billing:perTeamMember'} />
<If
condition={Boolean(lineItem?.unit) && !isDefaultSeatUnit}
fallback={<Trans i18nKey={'billing:perTeamMember'} />}
>
<Trans
i18nKey={'billing:perUnitShort'}
values={{
unit: unitLabel,
}}
/>
</If>
</If>
<If condition={lineItem?.unit}>
<If
condition={lineItem?.type !== 'per_seat' && lineItem?.unit}
>
<Trans
i18nKey={'billing:perUnit'}
values={{
@@ -324,6 +348,7 @@ function PricingItem(
selectedInterval={props.plan.interval}
currency={props.product.currency}
lineItems={lineItemsToDisplay}
alwaysDisplayMonthlyPrice={props.alwaysDisplayMonthlyPrice}
/>
</div>
</If>