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", "proceedToPayment": "Proceed to Payment",
"startTrial": "Start Trial", "startTrial": "Start Trial",
"perTeamMember": "Per team member", "perTeamMember": "Per team member",
"perUnitShort": "Per {{unit}}",
"perUnit": "Per {{unit}} usage", "perUnit": "Per {{unit}} usage",
"teamMembers": "Team Members", "teamMembers": "Team Members",
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan", "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 }}", "andAbove": "above {{ previousTier }} {{ unit }}",
"startingAtPriceUnit": "Starting at {{price}}/{{unit}}", "startingAtPriceUnit": "Starting at {{price}}/{{unit}}",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",

View File

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

View File

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

View File

@@ -154,10 +154,22 @@ function PricingItem(
}; };
}>, }>,
) { ) {
const { t, i18n } = useTranslation();
const highlighted = props.product.highlighted ?? false; const highlighted = props.product.highlighted ?? false;
const lineItem = props.primaryLineItem!; const lineItem = props.primaryLineItem!;
const isCustom = props.plan.custom ?? false; 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 // we exclude flat line items from the details since
// it doesn't need further explanation // it doesn't need further explanation
const lineItemsToDisplay = props.plan.lineItems.filter((item) => { const lineItemsToDisplay = props.plan.lineItems.filter((item) => {
@@ -259,14 +271,26 @@ function PricingItem(
<span <span
className={cn( 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'}> <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>
<If condition={lineItem?.unit}> <If
condition={lineItem?.type !== 'per_seat' && lineItem?.unit}
>
<Trans <Trans
i18nKey={'billing:perUnit'} i18nKey={'billing:perUnit'}
values={{ values={{
@@ -324,6 +348,7 @@ function PricingItem(
selectedInterval={props.plan.interval} selectedInterval={props.plan.interval}
currency={props.product.currency} currency={props.product.currency}
lineItems={lineItemsToDisplay} lineItems={lineItemsToDisplay}
alwaysDisplayMonthlyPrice={props.alwaysDisplayMonthlyPrice}
/> />
</div> </div>
</If> </If>