diff --git a/.yarn/cache/@formatjs-intl-localematcher-npm-0.4.0-9a73a446bf-c65108e9a8.zip b/.yarn/cache/@formatjs-intl-localematcher-npm-0.4.0-9a73a446bf-c65108e9a8.zip
new file mode 100644
index 000000000..204f609c5
Binary files /dev/null and b/.yarn/cache/@formatjs-intl-localematcher-npm-0.4.0-9a73a446bf-c65108e9a8.zip differ
diff --git a/.yarn/cache/@types-negotiator-npm-0.6.1-bd102330ab-e39f985874.zip b/.yarn/cache/@types-negotiator-npm-0.6.1-bd102330ab-e39f985874.zip
new file mode 100644
index 000000000..619e6944c
Binary files /dev/null and b/.yarn/cache/@types-negotiator-npm-0.6.1-bd102330ab-e39f985874.zip differ
diff --git a/app/[page]/layout.tsx b/app/[lang]/[page]/layout.tsx
similarity index 100%
rename from app/[page]/layout.tsx
rename to app/[lang]/[page]/layout.tsx
diff --git a/app/[page]/opengraph-image.tsx b/app/[lang]/[page]/opengraph-image.tsx
similarity index 100%
rename from app/[page]/opengraph-image.tsx
rename to app/[lang]/[page]/opengraph-image.tsx
diff --git a/app/[page]/page.tsx b/app/[lang]/[page]/page.tsx
similarity index 100%
rename from app/[page]/page.tsx
rename to app/[lang]/[page]/page.tsx
diff --git a/app/error.tsx b/app/[lang]/error.tsx
similarity index 100%
rename from app/error.tsx
rename to app/[lang]/error.tsx
diff --git a/app/favicon.ico b/app/[lang]/favicon.ico
similarity index 100%
rename from app/favicon.ico
rename to app/[lang]/favicon.ico
diff --git a/app/globals.css b/app/[lang]/globals.css
similarity index 100%
rename from app/globals.css
rename to app/[lang]/globals.css
diff --git a/app/layout.tsx b/app/[lang]/layout.tsx
similarity index 75%
rename from app/layout.tsx
rename to app/[lang]/layout.tsx
index b5aebc472..7da8ca05d 100644
--- a/app/layout.tsx
+++ b/app/[lang]/layout.tsx
@@ -1,4 +1,5 @@
import Navbar from 'components/layout/navbar';
+import { i18n } from 'i18n-config';
import { Inter } from 'next/font/google';
import { ReactNode, Suspense } from 'react';
import './globals.css';
@@ -34,12 +35,22 @@ const inter = Inter({
variable: '--font-inter'
});
-export default async function RootLayout({ children }: { children: ReactNode }) {
+export async function generateStaticParams() {
+ return i18n.locales.map((locale) => ({ lang: locale }));
+}
+
+export default async function RootLayout({
+ children,
+ params
+}: {
+ children: ReactNode;
+ params: { lang: string };
+}) {
return (
-
+
-
+
{children}
diff --git a/app/opengraph-image.tsx b/app/[lang]/opengraph-image.tsx
similarity index 100%
rename from app/opengraph-image.tsx
rename to app/[lang]/opengraph-image.tsx
diff --git a/app/page.tsx b/app/[lang]/page.tsx
similarity index 80%
rename from app/page.tsx
rename to app/[lang]/page.tsx
index 213f04410..361f5f8d3 100644
--- a/app/page.tsx
+++ b/app/[lang]/page.tsx
@@ -2,6 +2,8 @@ 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 Image from 'next/image';
import Namemark from 'public/assets/images/namemark.png';
import { Suspense } from 'react';
@@ -15,11 +17,13 @@ export const metadata = {
}
};
-export default async function HomePage() {
+export default async function HomePage({ params: { lang } }: { params: { lang: Locale } }) {
+ // const dictionary = await getDictionary(lang);
+
return (
<>
-
+
+
-
-
-
+
);
}
diff --git a/components/grid/tile.tsx b/components/grid/tile.tsx
index cf9aa49a9..9ec17e143 100644
--- a/components/grid/tile.tsx
+++ b/components/grid/tile.tsx
@@ -14,36 +14,21 @@ export function GridTileImage({
title: string;
amount: string;
currencyCode: string;
- position?: 'bottom' | 'center';
};
} & React.ComponentProps) {
return (
-
+
{props.src ? (
// eslint-disable-next-line jsx-a11y/alt-text -- `alt` is inherited from `props`, which is being enforced with TypeScript
) : null}
{label ? (
-
+
) : null}
);
diff --git a/components/label.tsx b/components/label.tsx
index 113afacb0..d94a89c37 100644
--- a/components/label.tsx
+++ b/components/label.tsx
@@ -4,24 +4,18 @@ import Price from './price';
const Label = ({
title,
amount,
- currencyCode,
- position = 'bottom'
+ currencyCode
}: {
title: string;
amount: string;
currencyCode: string;
- position?: 'bottom' | 'center';
}) => {
return (
-
-
-
{title}
+
+
+
{title}
-
+
diff --git a/components/layout/navbar/index.tsx b/components/layout/navbar/index.tsx
index 8ca3c5903..bf8187451 100644
--- a/components/layout/navbar/index.tsx
+++ b/components/layout/navbar/index.tsx
@@ -1,16 +1,17 @@
import Cart from 'components/cart';
import OpenCart from 'components/cart/open-cart';
+import type { Locale } from 'i18n-config';
import { Suspense } from 'react';
import { MenuModal } from '../menu/modal';
-export default async function Navbar() {
+export default async function Navbar({ lang }: { lang: Locale }) {
return (
diff --git a/components/layout/navbar/language-control.tsx b/components/layout/navbar/language-control.tsx
index 10e54c0d0..30c238feb 100644
--- a/components/layout/navbar/language-control.tsx
+++ b/components/layout/navbar/language-control.tsx
@@ -1,9 +1,45 @@
-export const LanguageControl = () => {
+'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 }) => {
+ const pathName = usePathname();
+ console.debug({ lang });
+ const redirectedPathName = (locale: string) => {
+ if (!pathName) return '/';
+ const segments = pathName.split('/');
+ segments[1] = locale;
+ return segments.join('/');
+ };
+
return (
- JP
+
+
+ JP
+
+
/
- EN
+
+
+ EN
+
+
);
};
diff --git a/dictionaries/en.json b/dictionaries/en.json
new file mode 100644
index 000000000..250e9f084
--- /dev/null
+++ b/dictionaries/en.json
@@ -0,0 +1,6 @@
+{
+ "hello": {
+ "title": "Hello World",
+ "description": "This is a description"
+ }
+}
diff --git a/dictionaries/index.ts b/dictionaries/index.ts
new file mode 100644
index 000000000..9644bd93f
--- /dev/null
+++ b/dictionaries/index.ts
@@ -0,0 +1,12 @@
+import 'server-only';
+import type { Locale } from '../i18n-config';
+
+// We enumerate all dictionaries here for better linting and typescript support
+// We also get the default import for cleaner types
+const dictionaries = {
+ en: () => import('./en.json').then((module) => module.default),
+ ja: () => import('./ja.json').then((module) => module.default)
+};
+
+export const getDictionary = async (locale: Locale) =>
+ dictionaries[locale]?.() ?? dictionaries.en();
diff --git a/dictionaries/ja.json b/dictionaries/ja.json
new file mode 100644
index 000000000..63ac1ad5f
--- /dev/null
+++ b/dictionaries/ja.json
@@ -0,0 +1,6 @@
+{
+ "hello": {
+ "title": "こんにちは",
+ "description": "これはせつめいですよ"
+ }
+}
diff --git a/i18n-config.ts b/i18n-config.ts
new file mode 100644
index 000000000..d32266228
--- /dev/null
+++ b/i18n-config.ts
@@ -0,0 +1,6 @@
+export const i18n = {
+ defaultLocale: 'en',
+ locales: ['en', 'ja']
+} as const;
+
+export type Locale = (typeof i18n)['locales'][number];
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 000000000..0ae013132
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,59 @@
+import type { NextRequest } from 'next/server';
+import { NextResponse } from 'next/server';
+
+import { match as matchLocale } from '@formatjs/intl-localematcher';
+import { i18n } from 'i18n-config';
+import Negotiator from 'negotiator';
+
+function getLocale(request: NextRequest): string | undefined {
+ // Negotiator expects plain object so we need to transform headers
+ const negotiatorHeaders: Record = {};
+ 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/addets/images/logo.png',
+ '/public/addets/images/logo+namemark.png',
+ '/public/addets/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)
+ );
+ }
+}
+
+export const config = {
+ // Matcher ignoring `/_next/` and `/api/`
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
+};
diff --git a/package.json b/package.json
index af4414423..9e365652f 100644
--- a/package.json
+++ b/package.json
@@ -23,11 +23,13 @@
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.4.0",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"clsx": "^2.0.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"eslint-plugin-unused-imports": "^3.0.0",
+ "negotiator": "^0.6.3",
"next": "latest",
"prettier-plugin-organize-imports": "^3.2.3",
"react": "latest",
@@ -36,6 +38,7 @@
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.9",
+ "@types/negotiator": "^0.6.1",
"@types/node": "20.4.4",
"@types/react": "18.2.16",
"@types/react-dom": "18.2.7",
diff --git a/yarn.lock b/yarn.lock
index d42aeedc8..ac22806d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -97,6 +97,15 @@ __metadata:
languageName: node
linkType: hard
+"@formatjs/intl-localematcher@npm:^0.4.0":
+ version: 0.4.0
+ resolution: "@formatjs/intl-localematcher@npm:0.4.0"
+ dependencies:
+ tslib: ^2.4.0
+ checksum: c65108e9a81c3733d2b6240ceedc846d0ae59c3606041cb5cc71c13453cdabe295b0dc8559dc4a8acaafdc45876807bd5e9ef37a3ec1cb864e78db655d434b66
+ languageName: node
+ linkType: hard
+
"@headlessui/react@npm:^1.7.15":
version: 1.7.16
resolution: "@headlessui/react@npm:1.7.16"
@@ -395,6 +404,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/negotiator@npm:^0.6.1":
+ version: 0.6.1
+ resolution: "@types/negotiator@npm:0.6.1"
+ checksum: e39f985874a30bd13186249eaaede0c5eec107178f55f2bd719c2c9d1397de329a5e0869a25dd5e7c702cdd311bd5e4153e25963900b4353545842354a2d1bc4
+ languageName: node
+ linkType: hard
+
"@types/node@npm:20.4.4":
version: 20.4.4
resolution: "@types/node@npm:20.4.4"
@@ -1172,10 +1188,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "commerce@workspace:."
dependencies:
+ "@formatjs/intl-localematcher": ^0.4.0
"@headlessui/react": ^1.7.15
"@heroicons/react": ^2.0.18
"@tailwindcss/container-queries": ^0.1.1
"@tailwindcss/typography": ^0.5.9
+ "@types/negotiator": ^0.6.1
"@types/node": 20.4.4
"@types/react": 18.2.16
"@types/react-dom": 18.2.7
@@ -1188,6 +1206,7 @@ __metadata:
eslint-plugin-unicorn: ^48.0.0
eslint-plugin-unused-imports: ^3.0.0
lint-staged: ^13.2.3
+ negotiator: ^0.6.3
next: latest
postcss: ^8.4.27
prettier: 3.0.1