feat: enhance internationalization with comprehensive locale support
- Add German and Chinese language translations - Update routing configuration to support multiple locales (en, de, zh) - Create new components for locale switching and navigation - Implement dynamic error and not-found pages with internationalized content - Refactor global styles and MDX styling - Update middleware and navigation configuration for improved i18n routing
This commit is contained in:
parent
9ff8fca3f4
commit
a4e7a59e17
8
global.d.ts
vendored
Normal file
8
global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import en from './messages/en.json';
|
||||
|
||||
type Messages = typeof en;
|
||||
|
||||
declare global {
|
||||
// Use type safe message keys with `next-intl`
|
||||
interface IntlMessages extends Messages {}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "你好,世界!",
|
||||
"about": "去关于页面"
|
||||
},
|
||||
"AboutPage": {
|
||||
"title": "关于"
|
||||
},
|
||||
"BlogPage": {
|
||||
"title": "博客"
|
||||
}
|
||||
}
|
46
messages/de.json
Normal file
46
messages/de.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"Error": {
|
||||
"description": "<p>Es ist leider ein Problem aufgetreten.</p><p>Du kannst versuchen <retry>diese Seite neu zu laden</retry>.</p>",
|
||||
"title": "Etwas ist schief gelaufen!"
|
||||
},
|
||||
"IndexPage": {
|
||||
"description": "Dies ist ein Beispiel, das die Verwendung von <code>next-intl</code> mit dem Next.js App Router demonstriert. Bei Ändern der Sprache rechts oben ändert sich der Inhalt dieser Seite.",
|
||||
"title": "next-intl Beispiel"
|
||||
},
|
||||
"LocaleLayout": {
|
||||
"title": "next-intl Beispiel"
|
||||
},
|
||||
"LocaleSwitcher": {
|
||||
"label": "Sprache ändern",
|
||||
"locale": "{locale, select, de {🇩🇪 Deutsch} en {🇺🇸 English} zh {🇨🇳 中文} other {Unbekannt}}"
|
||||
},
|
||||
"Manifest": {
|
||||
"name": ""
|
||||
},
|
||||
"Navigation": {
|
||||
"home": "Start",
|
||||
"pathnames": "Pfadnamen"
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"description": "Bitte überprüfe die Addressleiste deines Browsers oder verwende die Navigation um zu einer bekannten Seite zu wechseln.",
|
||||
"title": "Seite nicht gefunden"
|
||||
},
|
||||
"PageLayout": {
|
||||
"links": {
|
||||
"docs": {
|
||||
"description": "Erfahre mehr über next-intl in der offiziellen Dokumentation.",
|
||||
"href": "https://next-intl.dev",
|
||||
"title": "Dokumentation"
|
||||
},
|
||||
"source": {
|
||||
"description": "Sieh dir den Quellcode dieses Beispiels auf GitHub an.",
|
||||
"href": "https://github.com/amannn/next-intl/tree/main/examples/example-app-router",
|
||||
"title": "Quellcode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PathnamesPage": {
|
||||
"description": "<p>Auch die Pfadnamen sind internationalisiert.</p><p>Wenn du die Standardsprache Englisch verwendest, siehst du <code>/en/pathnames</code> in der Adressleiste des Browsers auf dieser Seite.</p><p>Wenn du die Sprache auf Deutsch änderst, wird die URL entsprechend lokalisiert (<code>/de/pfadnamen</code>).</p>",
|
||||
"title": "Pfadnamen"
|
||||
}
|
||||
}
|
@ -1,12 +1,46 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
"Error": {
|
||||
"description": "<p>We've unfortunately encountered an error.</p><p>You can try to <retry>reload the page</retry> you were visiting.</p>",
|
||||
"title": "Something went wrong!"
|
||||
},
|
||||
"AboutPage": {
|
||||
"title": "About"
|
||||
"IndexPage": {
|
||||
"description": "This is a basic example that demonstrates the usage of <code>next-intl</code> with the Next.js App Router. Try changing the locale in the top right corner and see how the content changes.",
|
||||
"title": "next-intl example"
|
||||
},
|
||||
"BlogPage": {
|
||||
"title": "Blog"
|
||||
"LocaleLayout": {
|
||||
"title": "next-intl example"
|
||||
},
|
||||
"LocaleSwitcher": {
|
||||
"label": "Change language",
|
||||
"locale": "{locale, select, de {🇩🇪 Deutsch} en {🇺🇸 English} zh {🇨🇳 中文} other {Unknown}}"
|
||||
},
|
||||
"Manifest": {
|
||||
"name": "next-intl example"
|
||||
},
|
||||
"Navigation": {
|
||||
"home": "Home",
|
||||
"pathnames": "Pathnames"
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"description": "Please double-check the browser address bar or use the navigation to go to a known page.",
|
||||
"title": "Page not found"
|
||||
},
|
||||
"PageLayout": {
|
||||
"links": {
|
||||
"docs": {
|
||||
"description": "Learn more about next-intl in the official docs.",
|
||||
"href": "https://next-intl.dev",
|
||||
"title": "Docs"
|
||||
},
|
||||
"source": {
|
||||
"description": "Browse the source code of this example on GitHub.",
|
||||
"href": "https://github.com/amannn/next-intl/tree/main/examples/example-app-router",
|
||||
"title": "Source code"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PathnamesPage": {
|
||||
"description": "<p>The pathnames are internationalized too.</p><p>If you're using the default language English, you'll see <code>/en/pathnames</code> in the browser address bar on this page.</p><p>If you change the locale to German, the URL is localized accordingly (<code>/de/pfadnamen</code>).</p>",
|
||||
"title": "Pathnames"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
messages/zh.json
Normal file
46
messages/zh.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"Error": {
|
||||
"description": "<p>很抱歉,我们遇到了一个错误。</p><p>您可以尝试<retry>重新加载</retry>您正在访问的页面。</p>",
|
||||
"title": "出错了!"
|
||||
},
|
||||
"IndexPage": {
|
||||
"description": "这是一个基础示例,展示了如何在 Next.js App Router 中使用 <code>next-intl</code>。尝试在右上角更改语言,看看内容如何变化。",
|
||||
"title": "next-intl 示例"
|
||||
},
|
||||
"LocaleLayout": {
|
||||
"title": "next-intl 示例"
|
||||
},
|
||||
"LocaleSwitcher": {
|
||||
"label": "更改语言",
|
||||
"locale": "{locale, select, de {🇩🇪 德语} en {🇺🇸 英语} zh {🇨🇳 中文} other {未知}}"
|
||||
},
|
||||
"Manifest": {
|
||||
"name": "next-intl 示例"
|
||||
},
|
||||
"Navigation": {
|
||||
"home": "首页",
|
||||
"pathnames": "路径名称"
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"description": "请检查浏览器地址栏或使用导航前往已知页面。",
|
||||
"title": "页面未找到"
|
||||
},
|
||||
"PageLayout": {
|
||||
"links": {
|
||||
"docs": {
|
||||
"description": "在官方文档中了解更多关于 next-intl 的信息。",
|
||||
"href": "https://next-intl.dev",
|
||||
"title": "文档"
|
||||
},
|
||||
"source": {
|
||||
"description": "在 GitHub 上浏览此示例的源代码。",
|
||||
"href": "https://github.com/amannn/next-intl/tree/main/examples/example-app-router",
|
||||
"title": "源代码"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PathnamesPage": {
|
||||
"description": "<p>路径名称也是国际化的。</p><p>如果您使用默认语言英语,您将在此页面的浏览器地址栏中看到 <code>/en/pathnames</code>。</p><p>如果您将语言更改为中文,URL 将相应地本地化(例如 <code>/cn/路径名称</code>)。</p>",
|
||||
"title": "路径名称"
|
||||
}
|
||||
}
|
@ -7,6 +7,13 @@ import { withContentCollections } from "@content-collections/next";
|
||||
*/
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
module.exports = {
|
||||
experimental: {
|
||||
// https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
|
||||
missingSuspenseWithCSRBailout: false,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* https://nextjs.org/docs/app/api-reference/config/next-config-js
|
||||
*/
|
||||
|
@ -28,7 +28,7 @@ export default async function HomePage(props: HomePageProps) {
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Use getTranslations instead of useTranslations for async server components
|
||||
const t = await getTranslations('HomePage');
|
||||
const t = await getTranslations('IndexPage');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -36,7 +36,7 @@ export default async function HomePage(props: HomePageProps) {
|
||||
|
||||
<div className="mt-12 flex flex-col gap-16">
|
||||
|
||||
<div>
|
||||
<div className="text-center">
|
||||
<h1>{t('title')}</h1>
|
||||
{/* <Link href="/about">{t('about')}</Link> */}
|
||||
</div>
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
interface AboutPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export default async function AboutPage(props: AboutPageProps) {
|
||||
const params = await props.params;
|
||||
const { locale } = params;
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations('AboutPage');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t('title')}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -11,9 +11,8 @@ import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { BlogToc } from '@/components/blog/blog-toc';
|
||||
import { Mdx } from '@/components/marketing/blog/mdx-component';
|
||||
|
||||
import '@/app/mdx.css';
|
||||
import AllPostsButton from '@/components/blog/all-posts-button';
|
||||
import '@/app/styles/mdx.css';
|
||||
|
||||
/**
|
||||
* Gets the blog post from the params
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default function CatchAllPage() {
|
||||
notFound();
|
||||
notFound();
|
||||
}
|
@ -1,48 +1,41 @@
|
||||
import "@/app/globals.css";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Navbar } from "@/components/marketing/navbar";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { LangAttributeSetter } from "@/components/layout/lang-attribute-setter";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
||||
import { Footer } from '@/components/layout/footer';
|
||||
import { Navbar } from '@/components/marketing/navbar';
|
||||
import { TailwindIndicator } from '@/components/tailwind-indicator';
|
||||
import { marketingConfig } from '@/config/marketing';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { Providers } from "./providers";
|
||||
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
import { Providers } from './providers';
|
||||
|
||||
export const metadata: Metadata = constructMetadata();
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
minimumScale: 1,
|
||||
maximumScale: 1,
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: 'black' }
|
||||
]
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
import '@/app/styles/globals.css';
|
||||
|
||||
interface LocaleLayoutProps {
|
||||
children: ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#layout
|
||||
*/
|
||||
export default async function LocaleLayout(props: LocaleLayoutProps) {
|
||||
const { children } = props;
|
||||
const params = await props.params;
|
||||
const { locale } = params;
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: Omit<LocaleLayoutProps, 'children'>) {
|
||||
const { locale } = await props.params;
|
||||
const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
|
||||
|
||||
return {
|
||||
title: t('title')
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
@ -56,26 +49,30 @@ export default async function LocaleLayout(props: LocaleLayoutProps) {
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
// Apply all the classes and providers without the html/body tags
|
||||
// as those are now handled by the root layout
|
||||
return (
|
||||
<>
|
||||
{/* Client component that sets the lang attribute on the html element */}
|
||||
<LangAttributeSetter locale={locale} />
|
||||
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body className={cn(
|
||||
"size-full antialiased",
|
||||
GeistSans.className,
|
||||
fontSourceSerif4.variable,
|
||||
fontSourceSans.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
|
||||
<TailwindIndicator />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</>
|
||||
<TailwindIndicator />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
25
src/app/[locale]/not-found.tsx
Normal file
25
src/app/[locale]/not-found.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Note that `app/[locale]/[...rest]/page.tsx`
|
||||
* is necessary for this page to render.
|
||||
*/
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
|
||||
<p className="text-balance text-center text-xl font-medium px-4">
|
||||
Sorry, the page you are looking for does not exist.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" variant="default">
|
||||
<Link href="/">Back to home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,28 +1,13 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import {ReactNode} from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Root layout must include html and body tags.
|
||||
* 2. We can't directly access the locale here.
|
||||
* 3. The locale layout will set the correct lang attribute on the html element
|
||||
* via data attributes (@/components/layout/lang-attribute-setter.tsx).
|
||||
* Since we have a `not-found.tsx` page on the root, a layout file
|
||||
* is required, even if it's just passing children through.
|
||||
*/
|
||||
export default function RootLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<body className={cn(
|
||||
"size-full antialiased",
|
||||
GeistSans.className,
|
||||
fontSourceSerif4.variable,
|
||||
fontSourceSans.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
export default function RootLayout({children}: Props) {
|
||||
return children;
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader2Icon className="my-32 mx-auto size-6 animate-spin" />;
|
||||
}
|
@ -1,21 +1,18 @@
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
export default function NotFound() {
|
||||
import Error from "next/error";
|
||||
|
||||
/**
|
||||
* This page renders when a route like `/unknown.txt` is requested.
|
||||
* In this case, the layout at `app/[locale]/layout.tsx` receives
|
||||
* an invalid value as the `[locale]` param and calls `notFound()`.
|
||||
*/
|
||||
export default function GlobalNotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
|
||||
<p className="text-balance text-center text-xl font-medium px-4">
|
||||
Sorry, the page you are looking for does not exist.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" variant="default">
|
||||
<Link href="/">Back to home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Error statusCode={404} />;
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// This page only renders when the app is built statically (output: 'export')
|
||||
/**
|
||||
* This page only renders when the app is built statically (output: 'export')
|
||||
* and the `redirect` function is used to redirect to the default locale.
|
||||
*/
|
||||
export default function RootPage() {
|
||||
redirect('/en');
|
||||
}
|
||||
redirect("/en");
|
||||
}
|
||||
|
21
src/components/LocaleSwitcher.tsx
Normal file
21
src/components/LocaleSwitcher.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import LocaleSwitcherSelect from "./LocaleSwitcherSelect";
|
||||
|
||||
/**
|
||||
* This component is used to render a locale switcher.
|
||||
*/
|
||||
export default function LocaleSwitcher() {
|
||||
const t = useTranslations("LocaleSwitcher");
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<LocaleSwitcherSelect defaultValue={locale} label={t("label")}>
|
||||
{routing.locales.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{t("locale", { locale: item })}
|
||||
</option>
|
||||
))}
|
||||
</LocaleSwitcherSelect>
|
||||
);
|
||||
}
|
60
src/components/LocaleSwitcherSelect.tsx
Normal file
60
src/components/LocaleSwitcherSelect.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChangeEvent, ReactNode, useTransition } from "react";
|
||||
import { Locale } from "@/i18n/routing";
|
||||
import { usePathname, useRouter } from "@/i18n/navigation";
|
||||
|
||||
interface LocaleSwitcherSelectProps {
|
||||
children: ReactNode;
|
||||
defaultValue: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is used to render a select element for the locale switcher.
|
||||
*/
|
||||
export default function LocaleSwitcherSelect({
|
||||
children,
|
||||
defaultValue,
|
||||
label
|
||||
}: LocaleSwitcherSelectProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
|
||||
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||
const nextLocale = event.target.value as Locale;
|
||||
startTransition(() => {
|
||||
router.replace(
|
||||
// @ts-expect-error -- TypeScript will validate that only known `params`
|
||||
// are used in combination with a given `pathname`. Since the two will
|
||||
// always match for the current route, we can skip runtime checks.
|
||||
{ pathname, params },
|
||||
{ locale: nextLocale }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
"relative text-gray-400",
|
||||
isPending && "transition-opacity [&:disabled]:opacity-30"
|
||||
)}
|
||||
>
|
||||
<p className="sr-only">{label}</p>
|
||||
<select
|
||||
className="inline-flex appearance-none bg-transparent py-3 pl-2 pr-6"
|
||||
defaultValue={defaultValue}
|
||||
disabled={isPending}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
<span className="pointer-events-none absolute right-2 top-[8px]">⌄</span>
|
||||
</label>
|
||||
);
|
||||
}
|
18
src/components/Navigation.tsx
Normal file
18
src/components/Navigation.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import {useTranslations} from 'next-intl';
|
||||
import LocaleSwitcher from './LocaleSwitcher';
|
||||
import NavigationLink from './NavigationLink';
|
||||
|
||||
export default function Navigation() {
|
||||
const t = useTranslations('Navigation');
|
||||
|
||||
return (
|
||||
<div className="bg-slate-850">
|
||||
<nav className="container flex justify-between p-2 text-white">
|
||||
<div>
|
||||
<NavigationLink href="/">{t('home')}</NavigationLink>
|
||||
</div>
|
||||
<LocaleSwitcher />
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
30
src/components/NavigationLink.tsx
Normal file
30
src/components/NavigationLink.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import clsx from "clsx";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
/**
|
||||
* This component is used to render a link in the navigation bar.
|
||||
*/
|
||||
export default function NavigationLink({
|
||||
href,
|
||||
...rest
|
||||
}: ComponentProps<typeof Link>) {
|
||||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||
const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : "/";
|
||||
const isActive = pathname === href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={clsx(
|
||||
"inline-block px-2 py-3 transition-colors",
|
||||
isActive ? "text-white" : "text-gray-400 hover:text-gray-200"
|
||||
)}
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
@ -26,6 +26,7 @@ import { MarketingConfig } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ArrowUpRightIcon } from 'lucide-react';
|
||||
import LocaleSwitcher from '../LocaleSwitcher';
|
||||
|
||||
interface NavBarProps {
|
||||
scroll?: boolean;
|
||||
@ -171,6 +172,7 @@ export function Navbar({ scroll, config }: NavBarProps) {
|
||||
)}
|
||||
|
||||
<ThemeSwitcher />
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { createNavigation } from 'next-intl/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
import { routing } from "./routing";
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-navigation
|
||||
*/
|
||||
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||
export const { Link, getPathname, redirect, usePathname, useRouter } =
|
||||
createNavigation(routing);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from './routing';
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { routing } from "./routing";
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// This typically corresponds to the `[locale]` segment
|
||||
@ -13,10 +13,10 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
||||
return {
|
||||
locale,
|
||||
messages: (
|
||||
await (locale === 'en'
|
||||
await (locale === "en"
|
||||
? // When using Turbopack, this will enable HMR for `en`
|
||||
import('../../messages/en.json')
|
||||
import("../../messages/en.json")
|
||||
: import(`../../messages/${locale}.json`))
|
||||
).default
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -1,17 +1,10 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-routing
|
||||
*/
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: ['en', 'cn'],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: 'en',
|
||||
|
||||
locales: ["en", "de", "zh"],
|
||||
defaultLocale: "en",
|
||||
pathnames: {
|
||||
'/': '/'
|
||||
"/": "/",
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
@ -10,10 +10,10 @@ export const config = {
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
'/(cn|en)/:path*',
|
||||
'/(de|en)/:path*',
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user