Skip to content

Commit

Permalink
feat(core): use next-intl to format dates and currencies (#808)
Browse files Browse the repository at this point in the history
* feat(core): use next-intl to format dates and currencies

* fix: fetch locale using async function
  • Loading branch information
jorgemoya committed Apr 24, 2024
1 parent 32c3373 commit c0bca5d
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 139 deletions.
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

0 comments on commit c0bca5d

Please sign in to comment.