refactor: simplify internationalization messages and remove unused content

- Update English and Chinese message files to remove unnecessary translations
- Remove root page redirect component
- Update home page to use new 'HomePage' translation key
- Modify marketing links by commenting out docs link
- Adjust locale selector width for better UI consistency
This commit is contained in:
javayhu 2025-03-05 01:57:53 +08:00
parent 6646471e00
commit 508d35cc21
31 changed files with 371 additions and 179 deletions

View File

@ -1,5 +1,61 @@
{
"HomePage": {
"title": "next-intl example"
},
"NotFoundPage": {
"title": "404",
"message": "Sorry, the page you are looking for does not exist.",
"backToHome": "Back to home"
},
"ErrorPage": {
"title": "Oops! Something went wrong!",
"tryAgain": "Try again",
"backToHome": "Back to home"
},
"AuthPage": {
"login": {
"title": "Login",
"welcomeBack": "Welcome back",
"email": "Email",
"password": "Password",
"signIn": "Sign In",
"signUp": "Don't have an account? Sign up",
"forgotPassword": "Forgot Password?",
"signInWithGoogle": "Sign In with Google",
"signInWithGitHub": "Sign In with GitHub",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"byClickingContinue": "By clicking continue, you agree to our",
"and": "and"
},
"register": {
"title": "Register",
"createAccount": "Create an account",
"name": "Name",
"email": "Email",
"password": "Password",
"signIn": "Sign In",
"signUp": "Already have an account? Sign in",
"checkEmail": "Please check your email inbox"
},
"forgotPassword": {
"title": "Forgot Password",
"email": "Email",
"send": "Send",
"backToLogin": "Back to login",
"checkEmail": "Please check your email inbox"
},
"resetPassword": {
"title": "Reset Password",
"password": "Password",
"reset": "Reset",
"backToLogin": "Back to login"
},
"error": {
"title": "Oops! Something went wrong!",
"tryAgain": "Please try again.",
"backToLogin": "Back to login",
"checkEmail": "Please check your email inbox"
}
}
}

View File

@ -1,5 +1,61 @@
{
"HomePage": {
"title": "next-intl 示例"
}
},
"NotFoundPage": {
"title": "404",
"message": "抱歉,您正在寻找的页面不存在。",
"backToHome": "返回首页"
},
"ErrorPage": {
"title": "哎呀!出错了!",
"tryAgain": "重试",
"backToHome": "返回首页"
},
"AuthPage": {
"login": {
"title": "登录",
"welcomeBack": "欢迎回来",
"email": "邮箱",
"password": "密码",
"signIn": "登录",
"signUp": "没有账号?注册",
"forgotPassword": "忘记密码?",
"signInWithGoogle": "使用 Google 登录",
"signInWithGitHub": "使用 GitHub 登录",
"termsOfService": "服务条款",
"privacyPolicy": "隐私政策",
"byClickingContinue": "点击继续即表示您同意我们的",
"and": "和"
},
"register": {
"title": "注册",
"createAccount": "创建账号",
"name": "姓名",
"email": "邮箱",
"password": "密码",
"signUp": "注册",
"signIn": "已经有账号?登录",
"checkEmail": "请检查您的邮箱"
},
"forgotPassword": {
"title": "忘记密码",
"email": "邮箱",
"send": "发送",
"backToLogin": "返回登录",
"checkEmail": "请检查您的邮箱"
},
"resetPassword": {
"title": "重置密码",
"password": "密码",
"reset": "重置",
"backToLogin": "返回登录"
},
"error": {
"title": "哎呀!出错了!",
"tryAgain": "请重试。",
"backToLogin": "返回登录",
"checkEmail": "请检查您的邮箱"
}
}
}

View File

@ -2,20 +2,6 @@ import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
import { withContentCollections } from "@content-collections/next";
/**
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
*
* Explicitly specify the path to the request config file
*/
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
*/
@ -29,16 +15,11 @@ const nextConfig: NextConfig = {
},
images: {
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
// vercel has limits on image optimization, 1000 images per month
// unoptimized: true,
// https://medium.com/@niniroula/nextjs-upgrade-next-image-and-dangerouslyallowsvg-c934060d79f8
// The requested resource "https://cdn.sanity.io/images/58a2mkbj/preview/xxx.svg?fit=max&auto=format" has type "image/svg+xml"
// but dangerouslyAllowSVG is disabled
// dangerouslyAllowSVG: true,
remotePatterns: [
{
protocol: "http",
hostname: "localhost",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
@ -55,6 +36,13 @@ const nextConfig: NextConfig = {
},
};
/**
* You can specify the path to the request config file or use the default one (@/i18n/request.ts)
*
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
*/
const withNextIntl = createNextIntlPlugin();
/**
* withContentCollections must be the outermost plugin
*

View File

@ -7,14 +7,7 @@ import LogoCloud from "@/components/nsui/logo-cloud";
import Pricing from "@/components/nsui/pricing";
import StatsSection from "@/components/nsui/stats";
import WallOfLoveSection from "@/components/nsui/testimonials";
import { siteConfig } from "@/config/site";
import { constructMetadata } from "@/lib/metadata";
import { getTranslations, setRequestLocale } from 'next-intl/server';
export const metadata = constructMetadata({
title: "",
canonicalUrl: `${siteConfig.url}/`,
});
import { getTranslations } from 'next-intl/server';
interface HomePageProps {
params: Promise<{ locale: string }>;
@ -23,21 +16,11 @@ interface HomePageProps {
export default async function HomePage(props: HomePageProps) {
const params = await props.params;
const { locale } = params;
// Enable static rendering
setRequestLocale(locale);
// Use getTranslations instead of useTranslations for async server components
const t = await getTranslations('HomePage');
return (
<>
<div className="mt-12 flex flex-col gap-16">
<div className="text-center">
<h1>{t('title')}</h1>
</div>
<HeroSection />
<LogoCloud />

View File

@ -0,0 +1,13 @@
import { marketingConfig } from "@/config/marketing";
import { Footer } from "@/components/layout/footer";
import { Navbar } from "@/components/marketing/navbar";
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col min-h-screen">
<Navbar scroll={true} config={marketingConfig} />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}

View File

@ -1,5 +1,13 @@
import { notFound } from 'next/navigation';
/**
* Catching unknown routes
*
* all requests that are matched within the [locale] segment will render
* the not-found page when an unknown route is encountered (e.g. /en/unknown).
*
* https://next-intl.dev/docs/environments/error-files#catching-unknown-routes
*/
export default function CatchAllPage() {
notFound();
}

View File

@ -1,7 +1,9 @@
import { LoginForm } from "@/components/auth/login-form";
import { siteConfig } from "@/config/site";
import { Link } from "@/i18n/navigation";
import { constructMetadata } from "@/lib/metadata";
import { Routes } from "@/routes";
import { useTranslations } from "next-intl";
export const metadata = constructMetadata({
title: "Login",
@ -10,25 +12,26 @@ export const metadata = constructMetadata({
});
const LoginPage = () => {
const t = useTranslations("AuthPage.login");
return (
<div className="flex flex-col gap-4">
<LoginForm />
<div className="text-balance text-center text-xs text-muted-foreground">
By clicking continue, you agree to our{" "}
<a
href="/terms"
{t("byClickingContinue")}
<Link
href={Routes.TermsOfService}
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</a>{" "}
and{" "}
<a
href="/privacy"
{t("termsOfService")}
</Link>{" "}
{t("and")}{" "}
<Link
href={Routes.PrivacyPolicy}
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</a>
.
{t("privacyPolicy")}
</Link>
</div>
</div>
);

View File

@ -1,54 +1,13 @@
"use client";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { Loader2Icon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import {lazy} from "react";
/**
* Learned how to recover from a server component error in Next.js from @asidorenko_
*
* https://x.com/asidorenko_/status/1841547623712407994
* Move error content to a separate chunk and load it only when needed
*
* Note that error.tsx is loaded right after your app has initialized.
* If your app is performance-sensitive and you want to avoid loading translation functionality
* from next-intl as part of this bundle, you can export a lazy reference from your error file.
* https://next-intl.dev/docs/environments/error-files#errorjs
*/
export default function ErrorPage({ reset }: { reset: () => void }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
<Logo className="size-12" />
<h1 className="text-2xl text-center">Oops! Something went wrong!</h1>
<div className="flex items-center gap-4">
<Button
type="submit"
variant="default"
disabled={isPending}
onClick={() => {
startTransition(() => {
router.refresh();
reset();
});
}}
>
{isPending ? (
<Loader2Icon className="mr-2 size-4 animate-spin" />
) : (
""
)}
Try again
</Button>
<Button
type="submit"
variant="outline"
onClick={() => router.push("/")}
>
Back to home
</Button>
</div>
</div>
);
}
export default lazy(() => import('@/components/error/error'));

View File

@ -8,7 +8,7 @@ import { cn } from "@/lib/utils";
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { ReactNode } from 'react';
import { Toaster } from 'sonner';
@ -21,19 +21,13 @@ interface LocaleLayoutProps {
params: Promise<{ locale: string }>;
};
// 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')
// };
// }
/**
* 1. Locale Layout
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#layout
*
* 2. NextIntlClientProvider
* https://next-intl.dev/docs/usage/configuration#nextintlclientprovider
*/
export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
const { locale } = await params;
@ -42,11 +36,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
notFound();
}
// Enable static rendering
setRequestLocale(locale);
// Providing all messages to the client
// side is the easiest way to get started
// Providing all messages to the client side
const messages = await getMessages();
return (
@ -59,14 +49,9 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
GeistSans.variable,
GeistMono.variable,
)}>
{/* https://next-intl.dev/docs/usage/configuration#nextintlclientprovider */}
<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>
{children}
<Toaster richColors position="top-right" offset={64} />

View File

@ -1,24 +1,30 @@
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
import Link from "next/link";
/**
* Note that `app/[locale]/[...rest]/page.tsx`
* is necessary for this page to render.
*
* https://next-intl.dev/docs/environments/error-files#not-foundjs
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
*/
export default function NotFound() {
const t = useTranslations('NotFoundPage');
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>
<h1 className="text-4xl font-bold">{t('title')}</h1>
<p className="text-balance text-center text-xl font-medium px-4">
Sorry, the page you are looking for does not exist.
{t('message')}
</p>
<Button asChild size="lg" variant="default">
<Link href="/">Back to home</Link>
<Link href="/">{t('backToHome')}</Link>
</Button>
</div>
);

View File

@ -7,6 +7,8 @@ interface Props {
/**
* Since we have a `not-found.tsx` page on the root, a layout file
* is required, even if it's just passing children through.
*
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
*/
export default function RootLayout({children}: Props) {
return children;

View File

@ -3,9 +3,13 @@
import Error from "next/error";
/**
* Catching non-localized requests
*
* 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()`.
*
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
*/
export default function GlobalNotFound() {
return (

View File

@ -1,23 +1,25 @@
"use client";
import { Button } from "@/components/ui/button";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Link } from "@/i18n/navigation";
import { ComponentProps } from "react";
interface BottomButtonProps {
href: string;
href: string
label: string;
}
export const BottomButton = ({ href, label }: BottomButtonProps) => {
return (
<Button
variant="link"
className="font-normal w-full text-muted-foreground"
size="sm"
asChild
<Link
href={href as ComponentProps<typeof Link>["href"]}
className={cn(
buttonVariants({ variant: "link", size: "sm" }),
"font-normal w-full text-muted-foreground hover:underline underline-offset-4 hover:text-primary"
)}
>
<a href={href} className="hover:underline underline-offset-4 hover:text-primary">
{label}
</a>
</Button>
{label}
</Link>
);
};

View File

@ -1,18 +1,21 @@
import { AuthCard } from "@/components/auth/auth-card";
import { Routes } from "@/routes";
import { TriangleAlertIcon } from "lucide-react";
import { useTranslations } from "next-intl";
export const ErrorCard = () => {
const t = useTranslations("AuthPage.error");
return (
<AuthCard
headerLabel="Something went wrong!"
headerLabel={t("title")}
bottomButtonHref={`${Routes.Login}`}
bottomButtonLabel="Back to login"
bottomButtonLabel={t("backToLogin")}
className="border-none"
>
<div className="w-full flex justify-center items-center py-4 gap-2">
<TriangleAlertIcon className="text-destructive size-4" />
<p className="font-medium text-destructive">Please try again.</p>
<p className="font-medium text-destructive">{t("tryAgain")}</p>
</div>
</AuthCard>
);

View File

@ -21,11 +21,14 @@ import type * as z from "zod";
import { Icons } from "@/components/icons/icons";
import { authClient } from "@/lib/auth-client";
import { Routes } from "@/routes";
import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
export const ForgotPasswordForm = () => {
export const ForgotPasswordForm = ({ className }: { className?: string }) => {
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const [isPending, setIsPending] = useState(false);
const t = useTranslations("AuthPage.forgotPassword");
const form = useForm<z.infer<typeof ForgotPasswordSchema>>({
resolver: zodResolver(ForgotPasswordSchema),
@ -51,7 +54,7 @@ export const ForgotPasswordForm = () => {
},
onSuccess: (ctx) => {
// console.log("forgotPassword, success:", ctx.data);
setSuccess("Please check your email inbox");
setSuccess(t("checkEmail"));
},
onError: (ctx) => {
console.log("forgotPassword, error:", ctx.error);
@ -62,10 +65,10 @@ export const ForgotPasswordForm = () => {
return (
<AuthCard
headerLabel="Froget password?"
bottomButtonLabel="Back to login"
headerLabel={t("title")}
bottomButtonLabel={t("backToLogin")}
bottomButtonHref={`${Routes.Login}`}
className="border-none"
className={cn("border-none", className)}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@ -75,7 +78,7 @@ export const ForgotPasswordForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t("email")}</FormLabel>
<FormControl>
<Input
{...field}
@ -102,7 +105,7 @@ export const ForgotPasswordForm = () => {
) : (
""
)}
<span>Send reset password email</span>
<span>{t("send")}</span>
</Button>
</form>
</Form>

View File

@ -14,11 +14,13 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Link } from "@/i18n/navigation";
import { authClient } from "@/lib/auth-client";
import { LoginSchema } from "@/lib/schemas";
import { cn } from "@/lib/utils";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@ -28,6 +30,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const urlError = searchParams.get("error");
const t = useTranslations("AuthPage.login");
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
@ -74,8 +77,8 @@ export const LoginForm = ({ className }: { className?: string }) => {
return (
<AuthCard
headerLabel="Welcome back"
bottomButtonLabel="Don't have an account? Sign up"
headerLabel={t("welcomeBack")}
bottomButtonLabel={t("signUp")}
bottomButtonHref={`${Routes.Register}`}
showSocialLoginButton
className={cn("border-none", className)}
@ -88,7 +91,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t("email")}</FormLabel>
<FormControl>
<Input
{...field}
@ -107,19 +110,19 @@ export const LoginForm = ({ className }: { className?: string }) => {
render={({ field }) => (
<FormItem>
<div className="flex justify-between items-center">
<FormLabel>Password</FormLabel>
<FormLabel>{t("password")}</FormLabel>
<Button
size="sm"
variant="link"
asChild
className="px-0 font-normal text-muted-foreground"
>
<a
<Link
href={`${Routes.ForgotPassword}`}
className="text-xs hover:underline hover:underline-offset-4 hover:text-primary"
>
Forgot password?
</a>
{t("forgotPassword")}
</Link>
</Button>
</div>
<FormControl>
@ -148,7 +151,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
) : (
""
)}
<span>Sign in</span>
<span>{t("signIn")}</span>
</Button>
</form>
</Form>

View File

@ -18,6 +18,7 @@ import { authClient } from "@/lib/auth-client";
import { RegisterSchema } from "@/lib/schemas";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@ -26,6 +27,7 @@ import type * as z from "zod";
export const RegisterForm = () => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const t = useTranslations("AuthPage.register");
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
@ -64,7 +66,7 @@ export const RegisterForm = () => {
onSuccess: (ctx) => {
// sign up success, user information stored in ctx.data
// console.log("register, success:", ctx.data);
setSuccess('Please check your email for verification');
setSuccess(t("checkEmail"));
},
onError: (ctx) => {
// sign up fail, display the error message
@ -76,8 +78,8 @@ export const RegisterForm = () => {
return (
<AuthCard
headerLabel="Create an account"
bottomButtonLabel="Already have an account? Sign in"
headerLabel={t("createAccount")}
bottomButtonLabel={t("signIn")}
bottomButtonHref={`${Routes.Login}`}
showSocialLoginButton
className="border-none"
@ -90,7 +92,7 @@ export const RegisterForm = () => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input {...field} disabled={isPending} placeholder="name" />
</FormControl>
@ -103,7 +105,7 @@ export const RegisterForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t("email")}</FormLabel>
<FormControl>
<Input
{...field}
@ -121,7 +123,7 @@ export const RegisterForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("password")}</FormLabel>
<FormControl>
<Input
{...field}
@ -148,7 +150,7 @@ export const RegisterForm = () => {
) : (
""
)}
<span>Sign up</span>
<span>{t("signUp")}</span>
</Button>
</form>
</Form>

View File

@ -18,6 +18,7 @@ import { authClient } from "@/lib/auth-client";
import { ResetPasswordSchema } from "@/lib/schemas";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@ -32,10 +33,10 @@ export const ResetPasswordForm = () => {
}
const router = useRouter();
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const [isPending, setIsPending] = useState(false);
const t = useTranslations("AuthPage.resetPassword");
const form = useForm<z.infer<typeof ResetPasswordSchema>>({
resolver: zodResolver(ResetPasswordSchema),
@ -73,8 +74,8 @@ export const ResetPasswordForm = () => {
return (
<AuthCard
headerLabel="Reset password"
bottomButtonLabel="Back to login"
headerLabel={t("title")}
bottomButtonLabel={t("backToLogin")}
bottomButtonHref={`${Routes.Login}`}
className="border-none"
>
@ -86,7 +87,7 @@ export const ResetPasswordForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("password")}</FormLabel>
<FormControl>
<Input
{...field}
@ -113,7 +114,7 @@ export const ResetPasswordForm = () => {
) : (
""
)}
<span>Reset password</span>
<span>{t("reset")}</span>
</Button>
</form>
</Form>

View File

@ -8,6 +8,7 @@ import { GitHubIcon } from "@/components/icons/github";
import { GoogleIcon } from "@/components/icons/google";
import { authClient } from "@/lib/auth-client";
import { Routes } from "@/routes";
import { useTranslations } from "next-intl";
/**
* social login buttons
@ -16,6 +17,7 @@ export const SocialLoginButton = () => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const [isLoading, setIsLoading] = useState<"google" | "github" | null>(null);
const t = useTranslations("AuthPage.login");
const onClick = async (provider: "google" | "github") => {
await authClient.signIn.social({
@ -76,7 +78,7 @@ export const SocialLoginButton = () => {
) : (
<GoogleIcon className="size-5 mr-2" />
)}
<span>Login with Google</span>
<span>{t("signInWithGoogle")}</span>
</Button>
<Button
size="lg"
@ -90,7 +92,7 @@ export const SocialLoginButton = () => {
) : (
<GitHubIcon className="size-5 mr-2" />
)}
<span>Login with GitHub</span>
<span>{t("signInWithGitHub")}</span>
</Button>
</div>
);

View File

@ -0,0 +1,59 @@
"use client";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { Loader2Icon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
/**
* 1. Note that error.tsx is loaded right after your app has initialized.
* If your app is performance-sensitive and you want to avoid loading translation functionality
* from next-intl as part of this bundle, you can export a lazy reference from your error file.
* https://next-intl.dev/docs/environments/error-files#errorjs
*
* 2. Learned how to recover from a server component error in Next.js from @asidorenko_
* https://x.com/asidorenko_/status/1841547623712407994
*/
export default function Error({ reset }: { reset: () => void }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const t = useTranslations('ErrorPage');
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
<Logo className="size-12" />
<h1 className="text-2xl text-center">{t('title')}</h1>
<div className="flex items-center gap-4">
<Button
type="submit"
variant="default"
disabled={isPending}
onClick={() => {
startTransition(() => {
router.refresh();
reset();
});
}}
>
{isPending ? (
<Loader2Icon className="mr-2 size-4 animate-spin" />
) : (
""
)}
{t('tryAgain')}
</Button>
<Button
type="submit"
variant="outline"
onClick={() => router.push("/")}
>
{t('backToHome')}
</Button>
</div>
</div>
);
}

View File

@ -18,6 +18,15 @@ import { useLocale } from "next-intl";
import { useParams } from "next/navigation";
import { useEffect, useTransition } from "react";
/**
* 1. useRouter
* By combining usePathname with useRouter, you can change the locale for the current page
* programmatically by navigating to the same pathname, while overriding the locale.
* Depending on if youre using the pathnames setting, you optionally have to forward params
* to potentially resolve an internal pathname.
*
* https://next-intl.dev/docs/routing/navigation#userouter
*/
export default function LocaleSelector() {
const router = useRouter();
const pathname = usePathname();
@ -34,8 +43,42 @@ export default function LocaleSelector() {
setCurrentLocale(nextLocale);
startTransition(() => {
// // For root path, just use the string
// if (pathname === "/") {
// router.replace("/", { locale: nextLocale });
// return;
// }
// // For blog index
// if (pathname === "/blog") {
// router.replace("/blog", { locale: nextLocale });
// return;
// }
// // For dynamic routes, reconstruct with the correct params
// if (pathname.startsWith("/blog/")) {
// if (pathname.includes("category")) {
// router.replace({
// pathname: "/blog/category/[slug]",
// params: { slug: params.slug as string }
// }, { locale: nextLocale });
// } else {
// router.replace({
// pathname: "/blog/[...slug]",
// params: { slug: Array.isArray(params.slug) ? params.slug : [params.slug as string] }
// }, { locale: nextLocale });
// }
// return;
// }
// // Fallback for other routes
// router.replace("/", { locale: nextLocale });
router.replace(
{ pathname }, // if your want to redirect to the current page
// @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 }
);
});

View File

@ -7,6 +7,8 @@ import { ComponentProps } from "react";
/**
* This component is used to render a link in the navigation bar.
*
* https://next-intl.dev/docs/routing/navigation#link
*/
export default function NavigationLink({
href,

View File

@ -1,11 +1,11 @@
import { defineRouting } from "next-intl/routing";
export const LOCALES = ["en", "zh"];
export const DEFAULT_LOCALE = "en";
export const LOCALE_LIST: Record<string, string> = {
en: "🇬🇧 English",
zh: "🇨🇳 中文",
};
export const LOCALES = Object.keys(LOCALE_LIST);
/**
* Next.js internationalized routing
@ -27,6 +27,15 @@ export const routing = defineRouting({
// https://next-intl.dev/docs/routing#pathnames
pathnames: {
"/": "/",
"/blog": "/blog",
"/blog/[...slug]": "/blog/[...slug]",
"/blog/category/[slug]": "/blog/category/[slug]",
// 认证相关路径
"/auth/login": "/auth/login",
"/auth/register": "/auth/register",
"/auth/forgot-password": "/auth/forgot-password",
"/auth/reset-password": "/auth/reset-password",
"/auth/error": "/auth/error",
},
});

View File

@ -7,4 +7,4 @@ export const authClient = createAuthClient({
// https://www.better-auth.com/docs/plugins/admin#add-the-client-plugin
adminClient(),
]
})
});

View File

@ -8,7 +8,7 @@ export enum Routes {
Pricing = '/#pricing',
FAQ = '/#faq',
TermsOfUse = '/terms-of-use',
TermsOfService = '/terms-of-service',
PrivacyPolicy = '/privacy-policy',
CookiePolicy = '/cookie-policy',