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:
committed by
GitHub
parent
9355c0a614
commit
58f08c5f39
@@ -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}}",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user