Multilanguage setup
This commit is contained in:
parent
aa8d674064
commit
6337858ff4
|
@ -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("./dictionaries/en.json").then((module) => module.default),
|
||||
bg: () => import("./dictionaries/bg.json").then((module) => module.default),
|
||||
};
|
||||
|
||||
export const getDictionary = async (locale: Locale) =>
|
||||
dictionaries[locale]?.() ?? dictionaries.en();
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"main": {
|
||||
"hello": "Добре дошли"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"main": {
|
||||
"hello": "Welcome"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export const i18n = {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "bg"],
|
||||
} as const;
|
||||
|
||||
export type Locale = (typeof i18n)["locales"][number];
|
|
@ -8,11 +8,14 @@
|
|||
"name": "openfest",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^0.6.3",
|
||||
"next": "14.2.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
@ -103,6 +106,14 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
|
||||
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
|
@ -448,6 +459,12 @@
|
|||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz",
|
||||
|
@ -3112,6 +3129,14 @@
|
|||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
|
||||
|
|
13
package.json
13
package.json
|
@ -9,18 +9,21 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^0.6.3",
|
||||
"next": "14.2.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.3"
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
@ -1,6 +1,11 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { i18n, type Locale } from "../../../i18n-config";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return i18n.locales.map((locale) => ({ lang: locale }));
|
||||
}
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
@ -11,11 +16,13 @@ export const metadata: Metadata = {
|
|||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
params
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: { lang: Locale };
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang={params.lang}>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
|
@ -1,10 +1,16 @@
|
|||
import Image from "next/image";
|
||||
import { getDictionary } from '../../../dictionaries'
|
||||
import { Locale } from "../../../i18n-config";
|
||||
|
||||
|
||||
export default async function Home({ params: { lang }, }: { params: { lang: Locale }; }) {
|
||||
const dictionary = await getDictionary(lang);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
{dictionary.main.hello},
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
||||
</p>
|
|
@ -0,0 +1,65 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { i18n } from "../i18n-config";
|
||||
|
||||
import { match as matchLocale } from "@formatjs/intl-localematcher";
|
||||
import Negotiator from "negotiator";
|
||||
|
||||
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 (
|
||||
[
|
||||
'/manifest.json',
|
||||
'/favicon.ico',
|
||||
'/next.svg',
|
||||
'/vercel.svg',
|
||||
// Your other files in `public`
|
||||
].includes(pathname)
|
||||
)
|
||||
return
|
||||
|
||||
// Check if there is any supported locale in the pathname
|
||||
const pathnameIsMissingLocale = i18n.locales.every(
|
||||
(locale) => !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).*)"],
|
||||
};
|
Loading…
Reference in New Issue