1
0
mirror of https://github.com/vercel/commerce.git synced 2025-07-15 00:41:21 +00:00

fix: Use next-intl for improved locale support

This commit is contained in:
Sol Irvine 2023-08-17 11:56:36 +09:00
parent 32bb4ffd0c
commit c1d06e90bb
42 changed files with 237 additions and 212 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

(image error) Size: 6.2 MiB

After

(image error) Size: 6.2 MiB

@ -1,11 +1,11 @@
import Navbar from 'components/layout/navbar';
import { Locale, i18n } from 'i18n-config';
import { Locale } from 'i18n-config';
import { Noto_Sans_JP } from 'next/font/google';
import localFont from 'next/font/local';
import { ReactNode, Suspense } from 'react';
import { LanguageProvider } from 'app/context/language-context';
import { getDictionary } from 'dictionaries';
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import './globals.css';
const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env;
@ -69,8 +69,8 @@ const mincho = localFont({
variable: '--font-mincho'
});
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }));
export function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'ja' }];
}
export default async function RootLayout({
@ -78,20 +78,25 @@ export default async function RootLayout({
params
}: {
children: ReactNode;
params: { lang: Locale };
params: { locale: Locale };
}) {
const dictionary = await getDictionary(params?.lang);
let messages;
try {
messages = (await import(`../../messages/${params?.locale}.json`)).default;
} catch (error) {
notFound();
}
return (
<html lang={params.lang} className={`${cinzel.variable} ${alpina.variable} ${noto.variable}`}>
<html lang={params.locale} className={`${cinzel.variable} ${alpina.variable} ${noto.variable}`}>
<body className="bg-dark text-white selection:bg-green-800 selection:text-green-400">
<div className="mx-auto max-w-screen-2xl">
<LanguageProvider locale={params.lang as Locale} dictionary={dictionary}>
<NextIntlClientProvider locale={params?.locale} messages={messages}>
<Navbar />
<Suspense>
<main>{children}</main>
</Suspense>
</LanguageProvider>
</NextIntlClientProvider>
</div>
</body>
</html>

@ -1,8 +1,7 @@
import { Carousel } from 'components/carousel';
import { ThreeItemGrid } from 'components/grid/three-items';
import Footer from 'components/layout/footer';
import { LanguageControl } from 'components/layout/navbar/language-control';
import type { Locale } from '../../i18n-config';
import { LanguageControl, SupportedLocales } from 'components/layout/navbar/language-control';
import clsx from 'clsx';
import LogoNamemark from 'components/icons/namemark';
@ -23,16 +22,20 @@ export const metadata = {
}
};
export default async function HomePage({ params: { lang } }: { params: { lang: Locale } }) {
export default async function HomePage({
params: { locale }
}: {
params: { locale: SupportedLocales };
}) {
return (
<>
<div className="invisible absolute right-40 top-12 md:visible">
<LanguageControl lang={lang} />
<LanguageControl lang={locale} />
</div>
<div className="px-6 pb-12 pt-6 md:pb-48 md:pl-6 md:pt-12">
<LogoNamemark className="w-[260px] fill-current md:w-[600px]" />
</div>
<ThreeItemGrid lang={lang} />
<ThreeItemGrid lang={locale} />
<div className="py-48">
<NewsletterSignup />
</div>

@ -1,44 +0,0 @@
'use client';
import { Locale } from 'i18n-config';
import { ReactNode, createContext, useContext, useState } from 'react';
interface IContextProps {
currentLocale?: Locale;
currentLanguage?: Locale;
setCurrentLanguage: (language: Locale) => void;
currentDictionary?: any;
}
export const LanguageContext = createContext<IContextProps>({} as IContextProps);
export function LanguageProvider({
locale,
dictionary,
children
}: {
locale: Locale;
dictionary?: any;
children: ReactNode | ReactNode[] | string;
}) {
const [currentLocale, setCurrentLocale] = useState<Locale>(locale || 'en');
const [currentLanguage, setCurrentLanguage] = useState<Locale>(locale || 'en');
const [currentDictionary] = useState<any | undefined>(dictionary);
return (
<LanguageContext.Provider
value={{
currentLocale,
currentLanguage,
setCurrentLanguage,
currentDictionary
}}
>
{children}
</LanguageContext.Provider>
);
}
export const useLanguage = () => {
return useContext(LanguageContext);
};

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { Locale } from 'i18n-config';
import { SupportedLocales } from 'components/layout/navbar/language-control';
import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Link from 'next/link';
@ -35,14 +35,14 @@ function ThreeItemGridItem({ item, priority }: { item: Product; priority?: boole
);
}
export async function ThreeItemGrid({ lang }: { lang: Locale }) {
export async function ThreeItemGrid({ lang }: { lang: SupportedLocales }) {
// Collections that start with `hidden-*` are hidden from the search page.
const homepageItems = await getCollectionProducts({
collection: 'hidden-homepage-featured-items',
language: lang.toUpperCase()
language: lang?.toUpperCase()
});
console.debug({ homepageItems });
console.debug({ lang });
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;

@ -1,15 +1,16 @@
'use client';
import { Dialog, Transition } from '@headlessui/react';
import { useLanguage } from 'app/context/language-context';
import CloseIcon from 'components/icons/close';
import MenuIcon from 'components/icons/menu';
import { useLocale, useTranslations } from 'next-intl';
import Link from 'next/link';
import { Fragment, useRef, useState } from 'react';
import { LanguageControl } from '../navbar/language-control';
import { LanguageControl, SupportedLocales } from '../navbar/language-control';
export function MenuModal() {
const { currentLanguage, currentDictionary } = useLanguage();
const t = useTranslations('Index');
const locale = useLocale();
let [isOpen, setIsOpen] = useState(false);
let closeButtonRef = useRef(null);
@ -57,7 +58,7 @@ export function MenuModal() {
<Dialog.Panel>
<div className="fixed right-5 top-6 z-40 px-2 py-1 md:top-11">
<div className="flex flex-row space-x-6">
<LanguageControl lang={currentLanguage} />
<LanguageControl lang={locale as SupportedLocales} />
<button ref={closeButtonRef} onClick={close} className="">
<CloseIcon className="h-10 w-10 stroke-current transition-opacity duration-150 hover:opacity-50" />
@ -73,7 +74,7 @@ export function MenuModal() {
href="/products"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.products}
{t('menu.products')}
</Link>
</div>
@ -82,7 +83,7 @@ export function MenuModal() {
href="/shops"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.shops}
{t('menu.shops')}
</Link>
</div>
@ -91,7 +92,7 @@ export function MenuModal() {
href="/about"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.about}
{t('menu.about')}
</Link>
</div>
@ -100,7 +101,7 @@ export function MenuModal() {
href="/bar"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.bar}
{t('menu.bar')}
</Link>
</div>
@ -109,7 +110,7 @@ export function MenuModal() {
href="/concept"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.concept}
{t('menu.concept')}
</Link>
</div>
@ -118,7 +119,7 @@ export function MenuModal() {
href="/stories"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.stories}
{t('menu.stories')}
</Link>
</div>
@ -127,7 +128,7 @@ export function MenuModal() {
href="/company"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.company}
{t('menu.company')}
</Link>
</div>
@ -136,7 +137,7 @@ export function MenuModal() {
href="/contact"
className="font-serif text-4xl font-normal transition-opacity duration-150 hover:opacity-50"
>
{currentDictionary?.menu?.contact}
{t('menu.contact')}
</Link>
</div>
</div>

@ -1,11 +1,12 @@
'use client';
import clsx from 'clsx';
import type { Locale } from 'i18n-config';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export const LanguageControl = ({ lang }: { lang?: Locale }) => {
export type SupportedLocales = 'en' | 'ja' | undefined;
export const LanguageControl = ({ lang }: { lang?: SupportedLocales }) => {
const pathName = usePathname();
const redirectedPathName = (locale: string) => {
if (!pathName) return '/';

@ -1,13 +1,14 @@
'use client';
import { useLanguage } from 'app/context/language-context';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
export default function NewsletterSignup() {
const { currentDictionary } = useLanguage();
const t = useTranslations('Index');
return (
<div className="mx-auto max-w-xl space-y-4">
<h3 className="font-serif text-2xl tracking-wider">{currentDictionary?.newsletter?.title}</h3>
<div className="font-multilingual">{currentDictionary?.newsletter?.description}</div>
<h3 className="font-serif text-2xl tracking-wider">{t('newsletter.title')}</h3>
<div className="font-multilingual">{t('newsletter.description')}</div>
<form
className="max-w-xl space-x-px md:flex"
action={`${process?.env?.NEXT_PUBLIC_MAILCHIMP_HOST}/subscribe/post?u=${process?.env?.NEXT_PUBLIC_MAILCHIMP_USER_ID}&amp;id=${process?.env?.NEXT_PUBLIC_MAILCHIMP_LIST_ID}`}
@ -16,7 +17,7 @@ export default function NewsletterSignup() {
target="_blank"
>
<label htmlFor="email-address" className="sr-only">
{currentDictionary.newsletter.placeholder}
{t('newsletter.placeholder')}
</label>
<input
type="email"
@ -31,7 +32,7 @@ export default function NewsletterSignup() {
'focus:ring-2 focus:ring-inset focus:ring-emerald-300 focus:ring-offset-0',
'text-gray-900 placeholder-gray-400'
)}
placeholder={currentDictionary.newsletter.placeholder}
placeholder={t('newsletter.placeholder')}
/>
<div className="mt-3 rounded-md sm:ml-3 sm:mt-0 sm:flex-shrink-0">
<button
@ -44,7 +45,7 @@ export default function NewsletterSignup() {
'focus:border-emerald-300 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-emerald-300 focus:ring-offset-0'
)}
>
{currentDictionary.newsletter.button}
{t('newsletter.button')}
</button>
</div>
<div style={{ position: 'absolute', left: '-5000px' }} aria-hidden="true">

@ -1,25 +1,22 @@
'use client';
import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { useLanguage } from 'app/context/language-context';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function Shoplist() {
const { currentLocale, currentDictionary } = useLanguage();
console.debug({ currentLocale });
const t = useTranslations('Index');
return (
<div className="mx-auto max-w-screen-2xl space-y-4 px-2">
<div className="flex w-full flex-row items-baseline space-x-12 pb-6">
<h2 className="font-serif text-6xl tracking-wider">shop list</h2>
<h3 className="font-multilingual font-serif text-2xl tracking-wider">
{currentDictionary?.shops?.title}
</h3>
<h3 className="font-multilingual font-serif text-2xl tracking-wider">{t('shops.title')}</h3>
</div>
<div className="grid w-full grid-cols-2 gap-px">
<Link
href="shops/hokkaido"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.hokkaido}</div>
<div>{t('shops.hokkaido')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"
@ -31,7 +28,7 @@ export default function Shoplist() {
href="shops/kanto"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.kanto}</div>
<div>{t('shops.kanto')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"
@ -43,7 +40,7 @@ export default function Shoplist() {
href="shops/chubu"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.chubu}</div>
<div>{t('shops.chubu')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"
@ -55,7 +52,7 @@ export default function Shoplist() {
href="shops/kinki"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.kinki}</div>
<div>{t('shops.kinki')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"
@ -67,7 +64,7 @@ export default function Shoplist() {
href="shops/chugoku"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.chugoku}</div>
<div>{t('shops.chugoku')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"
@ -79,7 +76,7 @@ export default function Shoplist() {
href="shops/kyushu"
className="group col-span-1 flex flex-row items-center justify-between p-6 outline outline-1 outline-subtle"
>
<div>{currentDictionary.shops.kyushu}</div>
<div>{t('shops.kyushu')}</div>
<div>
<ChevronRightIcon
className="h-6 w-6 stroke-subtle transition-colors duration-150 group-hover:stroke-white"

@ -1,27 +0,0 @@
{
"menu": {
"products": "products",
"shops": "shop list",
"about": "about narai",
"bar": "sagyobar",
"concept": "concept",
"stories": "stories",
"company": "company",
"contact": "contact"
},
"newsletter": {
"title": "newsletter",
"description": "Subscribe to our newsletter to receive free shipping on your first order, and access to exclusive information regarding events and pairing dinners.",
"placeholder": "Email",
"button": "Notify me"
},
"shops": {
"title": "",
"hokkaido": "Hokkaido / North",
"kanto": "Kanto",
"chubu": "Chubu",
"kinki": "Kinki",
"chugoku": "Chugoku",
"kyushu": "Kyushu"
}
}

@ -1,27 +0,0 @@
{
"menu": {
"products": "商品",
"shops": "取り扱い店",
"about": "naraiについて",
"bar": "sagyobar",
"concept": "コンセプト",
"stories": "ストーリー",
"company": "会社概要",
"contact": "contact"
},
"newsletter": {
"title": "newsletter",
"description": "ニュースレターにご登録いただくと、初回送料無料クーポン、購読者限定の情報やペアリングディナーなどのご案内をお送りさせていただきます。",
"placeholder": "メールアドレス",
"button": "登録する"
},
"shops": {
"title": "取り扱い店",
"hokkaido": "北海道・東北",
"kanto": "関東",
"chubu": "中部",
"kinki": "近畿",
"chugoku": "中国・四国",
"kyushu": "九州"
}
}

@ -1,6 +0,0 @@
export const i18n = {
defaultLocale: 'en',
locales: ['en', 'ja']
} as const;
export type Locale = (typeof i18n)['locales'][number];

29
messages/en.json Normal file

@ -0,0 +1,29 @@
{
"Index": {
"menu": {
"products": "products",
"shops": "shop list",
"about": "about narai",
"bar": "sagyobar",
"concept": "concept",
"stories": "stories",
"company": "company",
"contact": "contact"
},
"newsletter": {
"title": "newsletter",
"description": "Subscribe to our newsletter to receive free shipping on your first order, and access to exclusive information regarding events and pairing dinners.",
"placeholder": "Email",
"button": "Notify me"
},
"shops": {
"title": "",
"hokkaido": "Hokkaido / North",
"kanto": "Kanto",
"chubu": "Chubu",
"kinki": "Kinki",
"chugoku": "Chugoku",
"kyushu": "Kyushu"
}
}
}

29
messages/ja.json Normal file

@ -0,0 +1,29 @@
{
"Index": {
"menu": {
"products": "商品",
"shops": "取り扱い店",
"about": "naraiについて",
"bar": "sagyobar",
"concept": "コンセプト",
"stories": "ストーリー",
"company": "会社概要",
"contact": "contact"
},
"newsletter": {
"title": "newsletter",
"description": "ニュースレターにご登録いただくと、初回送料無料クーポン、購読者限定の情報やペアリングディナーなどのご案内をお送りさせていただきます。",
"placeholder": "メールアドレス",
"button": "登録する"
},
"shops": {
"title": "取り扱い店",
"hokkaido": "北海道・東北",
"kanto": "関東",
"chubu": "中部",
"kinki": "近畿",
"chugoku": "中国・四国",
"kyushu": "九州"
}
}
}

@ -1,59 +1,15 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import createMiddleware from 'next-intl/middleware';
import { match as matchLocale } from '@formatjs/intl-localematcher';
import { i18n } from 'i18n-config';
import Negotiator from 'negotiator';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'ja'],
function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;
// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(locales);
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// If you have one
if (
[
'/public/assets/images/logo.png',
'/public/assets/images/logo+namemark.png',
'/public/assets/images/namemark.png'
// Your other files in `public`
].includes(pathname)
)
return;
// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale: any) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(
new URL(`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, request.url)
);
}
}
// If this locale is matched, pathnames work without a prefix (e.g. `/about`)
defaultLocale: 'en'
});
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
// Skip all paths that should not be internationalized. This example skips the
// folders "api", "_next" and all files with an extension (e.g. favicon.ico)
matcher: ['/((?!api|_next|.*\\..*).*)']
};

@ -23,7 +23,6 @@
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.4.0",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@thgh/next-gtm": "^0.1.4",
@ -32,6 +31,7 @@
"eslint-plugin-unused-imports": "^3.0.0",
"negotiator": "^0.6.3",
"next": "latest",
"next-intl": "latest",
"prettier-plugin-organize-imports": "^3.2.3",
"react": "latest",
"react-dom": "latest"

109
yarn.lock

@ -97,7 +97,66 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:^0.4.0":
"@formatjs/ecma402-abstract@npm:1.11.4":
version: 1.11.4
resolution: "@formatjs/ecma402-abstract@npm:1.11.4"
dependencies:
"@formatjs/intl-localematcher": 0.2.25
tslib: ^2.1.0
checksum: 05dbe1c6457acfe9cdc0fc770940260e19fa588be6c655b1ff1697506348dac1eee9b249b64e6544531174d07a5a74a9e75f68430947cfdc074ebe8e3c86f86f
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:^1.11.4":
version: 1.17.0
resolution: "@formatjs/ecma402-abstract@npm:1.17.0"
dependencies:
"@formatjs/intl-localematcher": 0.4.0
tslib: ^2.4.0
checksum: cc45d238e541076cb27b9cf02d8b97f789d1744b60218da6d31793204850c159e85f5b2557de3905a365eefd52a1c2e7f1febb9e1f009bad23d5eca17b3de6c8
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:1.2.1":
version: 1.2.1
resolution: "@formatjs/fast-memoize@npm:1.2.1"
dependencies:
tslib: ^2.1.0
checksum: 7df9e941142be16e5862afe7387926cec44ec136d2c2f9a7e1598cb6c8c23a65e420ed90251ec9b48df083f5473b10d6fbbee2e9fc7233d5bf1f27efffba59a7
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.1.0":
version: 2.1.0
resolution: "@formatjs/icu-messageformat-parser@npm:2.1.0"
dependencies:
"@formatjs/ecma402-abstract": 1.11.4
"@formatjs/icu-skeleton-parser": 1.3.6
tslib: ^2.1.0
checksum: 8dab4d102b334dd7ab25b85817b074f1b54845d02f9ef9fa5a1fa6a9723176ad28a59d845fdc0eeedb868b891e61a4c530384b123db29f5b14e1f3f8b207373f
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.3.6":
version: 1.3.6
resolution: "@formatjs/icu-skeleton-parser@npm:1.3.6"
dependencies:
"@formatjs/ecma402-abstract": 1.11.4
tslib: ^2.1.0
checksum: cce2d8bea54f0096c557dc03920bfe4785893e60962313fab9eeee41f0b411d38b9d45852882b19f261417d730362c8685bea6ba5ac1e2dee141f030cda624e9
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.2.25":
version: 0.2.25
resolution: "@formatjs/intl-localematcher@npm:0.2.25"
dependencies:
tslib: ^2.1.0
checksum: ee00ddc23317dc47a58831aaca5112e101d8bb1f38adc0ecfe1a9d7e008d0bb1091519f07e1d7d805b0c1e28f2c3e75f697ae479e22423445814412c7669284c
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.4.0, @formatjs/intl-localematcher@npm:^0.4.0":
version: 0.4.0
resolution: "@formatjs/intl-localematcher@npm:0.4.0"
dependencies:
@ -106,6 +165,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:^0.2.32":
version: 0.2.32
resolution: "@formatjs/intl-localematcher@npm:0.2.32"
dependencies:
tslib: ^2.4.0
checksum: 477e18aabaf2e6e90fc12952a3cb6c0ebb40ad99414d6b9d2501c6348fbad58cacb433ec6630955cfd1491ea7630f32a9dc280bb27d0fb8a784251404a54140a
languageName: node
linkType: hard
"@headlessui/react@npm:^1.7.15":
version: 1.7.16
resolution: "@headlessui/react@npm:1.7.16"
@ -1220,6 +1288,7 @@ __metadata:
lint-staged: ^13.2.3
negotiator: ^0.6.3
next: latest
next-intl: latest
postcss: ^8.4.27
prettier: 3.0.1
prettier-plugin-organize-imports: ^3.2.3
@ -2541,6 +2610,18 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:^9.3.18":
version: 9.13.0
resolution: "intl-messageformat@npm:9.13.0"
dependencies:
"@formatjs/ecma402-abstract": 1.11.4
"@formatjs/fast-memoize": 1.2.1
"@formatjs/icu-messageformat-parser": 2.1.0
tslib: ^2.1.0
checksum: effb840ae6e213adceab9951dbb2d18d1a354c82e35dbd213011daf23bcc79e2a18b6ef79157ae3b69c1e6d898fe66b54dd184ac180ee0646e26a70276e60038
languageName: node
linkType: hard
"ip@npm:^2.0.0":
version: 2.0.0
resolution: "ip@npm:2.0.0"
@ -3328,6 +3409,20 @@ __metadata:
languageName: node
linkType: hard
"next-intl@npm:latest":
version: 2.19.1
resolution: "next-intl@npm:2.19.1"
dependencies:
"@formatjs/intl-localematcher": ^0.2.32
negotiator: ^0.6.3
use-intl: ^2.19.1
peerDependencies:
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: b4c3cfbb22645b75fcb549259d18484f3df284a8240054f269f7403bc083fc57e53dc555abd4e29ba68fcd56c52fdb0a54d5021791597441519150d0054e9665
languageName: node
linkType: hard
"next@npm:latest":
version: 13.4.13
resolution: "next@npm:13.4.13"
@ -5040,6 +5135,18 @@ __metadata:
languageName: node
linkType: hard
"use-intl@npm:^2.19.1":
version: 2.19.1
resolution: "use-intl@npm:2.19.1"
dependencies:
"@formatjs/ecma402-abstract": ^1.11.4
intl-messageformat: ^9.3.18
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 9c249791a4cb4213de6426d5542cc44685eb4a714c44f3761dc7713b48fc505d02e5ee5047f9ef31aec3a9e869fff838b96a6d1363f5ead583e6b6da0a96d142
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"