Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): use next-intl to format dates and currencies #808

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-trees-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

use next-intl formatter to properly localize dates & prices
8 changes: 5 additions & 3 deletions apps/core/app/[locale]/(default)/blog/[blogId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { Tag, TagContent } from '@bigcommerce/components/tag';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getFormatter } from 'next-intl/server';

import { getBlogPost } from '~/client/queries/get-blog-post';
import { BcImage } from '~/components/bc-image';
Expand All @@ -32,7 +33,8 @@ export async function generateMetadata({ params: { blogId } }: Props): Promise<M
};
}

export default async function BlogPostPage({ params: { blogId } }: Props) {
export default async function BlogPostPage({ params: { blogId, locale } }: Props) {
const format = await getFormatter({ locale });
const blogPost = await getBlogPost(+blogId);

if (!blogPost || !blogPost.isVisibleInNavigation) {
Expand All @@ -45,7 +47,7 @@ export default async function BlogPostPage({ params: { blogId } }: Props) {

<div className="mb-8 flex">
<BlogPostDate className="mb-0">
{new Intl.DateTimeFormat('en-US').format(new Date(blogPost.publishedDate.utc))}
{format.dateTime(new Date(blogPost.publishedDate.utc))}
</BlogPostDate>
{blogPost.author ? <BlogPostAuthor>, by {blogPost.author}</BlogPostAuthor> : null}
</div>
Expand All @@ -67,7 +69,7 @@ export default async function BlogPostPage({ params: { blogId } }: Props) {
</BlogPostTitle>
<BlogPostDate variant="inBanner">
<span className="text-primary">
{new Intl.DateTimeFormat('en-US').format(new Date(blogPost.publishedDate.utc))}
{format.dateTime(new Date(blogPost.publishedDate.utc))}
</span>
</BlogPostDate>
</BlogPostBanner>
Expand Down
18 changes: 8 additions & 10 deletions apps/core/app/[locale]/(default)/cart/_components/cart-item.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { getFormatter, getLocale, getMessages } from 'next-intl/server';

import { getCart } from '~/client/queries/get-cart';
import { ExistingResultType } from '~/client/util';
Expand All @@ -15,18 +15,13 @@ export type Product =
export const CartItem = async ({
currencyCode,
product,
locale,
}: {
currencyCode: string;
product: Product;
locale: string;
}) => {
const locale = await getLocale();
const messages = await getMessages({ locale });

const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currencyCode,
});
const format = await getFormatter({ locale });

return (
<li>
Expand Down Expand Up @@ -88,7 +83,7 @@ export const CartItem = async ({
<div key={selectedOption.entityId}>
<span>{selectedOption.name}:</span>{' '}
<span className="font-semibold">
{Intl.DateTimeFormat().format(new Date(selectedOption.date.utc))}
{format.dateTime(new Date(selectedOption.date.utc))}
</span>
</div>
);
Expand All @@ -106,7 +101,10 @@ export const CartItem = async ({

<div>
<p className="inline-flex w-24 justify-center text-lg font-bold">
{currencyFormatter.format(product.extendedSalePrice.value)}
{format.number(product.extendedSalePrice.value, {
style: 'currency',
currency: currencyCode,
})}
</p>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AlertCircle } from 'lucide-react';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { getFormatter, getLocale, getMessages, getTranslations } from 'next-intl/server';
import { toast } from 'react-hot-toast';

import { getCheckout } from '~/client/queries/get-checkout';
Expand All @@ -10,8 +10,10 @@ import { getShippingCountries } from '../_actions/get-shipping-countries';
import { CouponCode } from './coupon-code';
import { ShippingEstimator } from './shipping-estimator';

export const CheckoutSummary = async ({ cartId, locale }: { cartId: string; locale: string }) => {
export const CheckoutSummary = async ({ cartId }: { cartId: string }) => {
const locale = await getLocale();
const t = await getTranslations({ locale, namespace: 'Cart.CheckoutSummary' });
const format = await getFormatter({ locale });
const messages = await getMessages({ locale });

const [checkout, shippingCountries] = await Promise.all([
Expand All @@ -27,16 +29,16 @@ export const CheckoutSummary = async ({ cartId, locale }: { cartId: string; loca
return null;
}

const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: checkout.cart?.currencyCode,
});

return (
<>
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="font-semibold">{t('subTotal')}</span>
<span>{currencyFormatter.format(checkout.subtotal?.value || 0)}</span>
<span>
{format.number(checkout.subtotal?.value || 0, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</div>

<NextIntlClientProvider locale={locale} messages={{ Cart: messages.Cart ?? {} }}>
Expand All @@ -46,7 +48,13 @@ export const CheckoutSummary = async ({ cartId, locale }: { cartId: string; loca
{checkout.cart?.discountedAmount && (
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="font-semibold">{t('discounts')}</span>
<span>-{currencyFormatter.format(checkout.cart.discountedAmount.value)}</span>
<span>
-
{format.number(checkout.cart.discountedAmount.value, {
style: 'currency',
currency: checkout.cart.currencyCode,
})}
</span>
</div>
)}

Expand All @@ -57,13 +65,23 @@ export const CheckoutSummary = async ({ cartId, locale }: { cartId: string; loca
{checkout.taxTotal && (
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="font-semibold">{t('tax')}</span>
<span>{currencyFormatter.format(checkout.taxTotal.value)}</span>
<span>
{format.number(checkout.taxTotal.value, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</div>
)}

<div className="flex justify-between border-t border-t-gray-200 py-4 text-xl font-bold lg:text-2xl">
{t('grandTotal')}
<span>{currencyFormatter.format(checkout.grandTotal?.value || 0)}</span>
<span>
{format.number(checkout.grandTotal?.value || 0, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from '@bigcommerce/components/button';
import { Field, FieldControl, FieldMessage, Form, FormSubmit } from '@bigcommerce/components/form';
import { Input } from '@bigcommerce/components/input';
import { AlertCircle, Loader2 as Spinner } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useFormatter, useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useFormStatus } from 'react-dom';
import { toast } from 'react-hot-toast';
Expand Down Expand Up @@ -37,16 +37,13 @@ const SubmitButton = () => {

export const CouponCode = ({ checkout }: { checkout: ExistingResultType<typeof getCheckout> }) => {
const t = useTranslations('Cart.CheckoutSummary');
const format = useFormatter();

const [showAddCoupon, setShowAddCoupon] = useState(false);
const [selectedCoupon, setSelectedCoupon] = useState<Checkout['coupons'][number] | null>(
checkout.coupons.at(0) || null,
);

const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: checkout.cart?.currencyCode,
});

useEffect(() => {
if (checkout.coupons[0]) {
setSelectedCoupon(checkout.coupons[0]);
Expand Down Expand Up @@ -84,7 +81,12 @@ export const CouponCode = ({ checkout }: { checkout: ExistingResultType<typeof g
<span className="font-semibold">
{t('coupon')} ({selectedCoupon.code})
</span>
<span>{currencyFormatter.format(selectedCoupon.discountedAmount.value * -1)}</span>
<span>
{format.number(selectedCoupon.discountedAmount.value * -1, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</div>
<form action={onSubmitRemoveCouponCode}>
<input name="checkoutEntityId" type="hidden" value={checkout.entityId} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Button } from '@bigcommerce/components/button';
import { useTranslations } from 'next-intl';
import { useFormatter, useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';

import { getCheckout } from '~/client/queries/get-checkout';
Expand All @@ -20,15 +20,11 @@ export const ShippingEstimator = ({
shippingCountries: ExistingResultType<typeof getShippingCountries>;
}) => {
const t = useTranslations('Cart.CheckoutSummary');
const format = useFormatter();

const [showShippingInfo, setShowShippingInfo] = useState(false);
const [showShippingOptions, setShowShippingOptions] = useState(false);

const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: checkout.cart?.currencyCode,
});

const selectedShippingConsignment = checkout.shippingConsignments?.find(
(shippingConsignment) => shippingConsignment.selectedShippingOption,
);
Expand Down Expand Up @@ -60,7 +56,12 @@ export const ShippingEstimator = ({
<div className="flex justify-between">
<span className="font-semibold">{t('shippingCost')}</span>
{selectedShippingConsignment ? (
<span>{currencyFormatter.format(checkout.shippingCostTotal?.value || 0)}</span>
<span>
{format.number(checkout.shippingCostTotal?.value || 0, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
) : (
<Button
aria-controls="shipping-options"
Expand Down Expand Up @@ -113,7 +114,12 @@ export const ShippingEstimator = ({
{Boolean(checkout.handlingCostTotal?.value) && (
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="font-semibold">{t('handlingCost')}</span>
<span>{currencyFormatter.format(checkout.handlingCostTotal?.value || 0)}</span>
<span>
{format.number(checkout.handlingCostTotal?.value || 0, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</div>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Label } from '@bigcommerce/components/label';
import { Message } from '@bigcommerce/components/message';
import { RadioGroup, RadioItem } from '@bigcommerce/components/radio-group';
import { AlertCircle, Loader2 as Spinner } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useFormatter, useTranslations } from 'next-intl';
import { useFormStatus } from 'react-dom';
import { toast } from 'react-hot-toast';

Expand Down Expand Up @@ -50,6 +50,7 @@ export const ShippingOptions = ({
availableShippingOptions: AvailableShippingOptions[] | null;
}) => {
const t = useTranslations('Cart.ShippingCost');
const format = useFormatter();

const shippingOptions = availableShippingOptions?.map(
({ cost, description, entityId: shippingOptionEntityId, isRecommended }) => ({
Expand All @@ -60,11 +61,6 @@ export const ShippingOptions = ({
}),
);

const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: checkout.cart?.currencyCode,
});

const onSubmit = async (formData: FormData) => {
const { status } = await submitShippingCosts(formData, checkout.entityId, consignmentEntityId);

Expand Down Expand Up @@ -98,7 +94,12 @@ export const ShippingOptions = ({
>
<p className="inline-flex w-full justify-between">
<span>{option.description}</span>
<span>{currencyFormatter.format(option.cost)}</span>
<span>
{format.number(option.cost, {
style: 'currency',
currency: checkout.cart?.currencyCode,
})}
</span>
</p>
</Label>
</div>
Expand Down
16 changes: 3 additions & 13 deletions apps/core/app/[locale]/(default)/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,16 @@ export default async function CartPage({ params: { locale } }: Props) {
<div className="pb-12 md:grid md:grid-cols-2 md:gap-8 lg:grid-cols-3">
<ul className="col-span-2">
{cart.lineItems.physicalItems.map((product) => (
<CartItem
currencyCode={cart.currencyCode}
key={product.entityId}
locale={locale}
product={product}
/>
<CartItem currencyCode={cart.currencyCode} key={product.entityId} product={product} />
))}

{cart.lineItems.digitalItems.map((product) => (
<CartItem
currencyCode={cart.currencyCode}
key={product.entityId}
locale={locale}
product={product}
/>
<CartItem currencyCode={cart.currencyCode} key={product.entityId} product={product} />
))}
</ul>

<div className="col-span-1 col-start-2 lg:col-start-3">
<CheckoutSummary cartId={cartId} locale={locale} />
<CheckoutSummary cartId={cartId} />

<Suspense fallback={t('loading')}>
<CheckoutButton cartId={cartId} label={t('proceedToCheckout')} />
Expand Down
Loading
Loading