mirror of
https://github.com/beilunyang/moemail.git
synced 2026-09-07 08:16:37 +08:00
feat: add internationalization support with next-intl
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { i18n, type Locale } from "@/i18n/config"
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import { FloatMenu } from "@/components/float-menu"
|
||||
import { ThemeProvider } from "@/components/theme/theme-provider"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { zpix } from "../fonts"
|
||||
import "../globals.css"
|
||||
import { Providers } from "../providers"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: '#826DD9',
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
}
|
||||
|
||||
async function getMessages(locale: Locale) {
|
||||
try {
|
||||
const common = (await import(`@/i18n/messages/${locale}/common.json`)).default
|
||||
const home = (await import(`@/i18n/messages/${locale}/home.json`)).default
|
||||
const auth = (await import(`@/i18n/messages/${locale}/auth.json`)).default
|
||||
const metadata = (await import(`@/i18n/messages/${locale}/metadata.json`)).default
|
||||
const emails = (await import(`@/i18n/messages/${locale}/emails.json`)).default
|
||||
const profile = (await import(`@/i18n/messages/${locale}/profile.json`)).default
|
||||
return { common, home, auth, metadata, emails, profile }
|
||||
} catch (error) {
|
||||
console.error(`Failed to load messages for locale ${locale}:`, error)
|
||||
return { common: {}, home: {}, auth: {}, metadata: {}, emails: {}, profile: {} }
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
const t = await getTranslations({ locale, namespace: "metadata" })
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://moemail.app"
|
||||
|
||||
// Generate hreflang links for all supported locales
|
||||
const languages: Record<string, string> = {}
|
||||
i18n.locales.forEach((loc) => {
|
||||
languages[loc] = `${baseUrl}/${loc}`
|
||||
})
|
||||
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
keywords: t("keywords"),
|
||||
authors: [{ name: "SoftMoe Studio" }],
|
||||
creator: "SoftMoe Studio",
|
||||
publisher: "SoftMoe Studio",
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: locale === "zh-CN" ? "zh_CN" : locale,
|
||||
url: `${baseUrl}/${locale}`,
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
siteName: "MoeMail",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${baseUrl}/${locale}`,
|
||||
languages,
|
||||
},
|
||||
manifest: '/manifest.json',
|
||||
icons: [
|
||||
{ rel: 'apple-touch-icon', url: '/icons/icon-192x192.png' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
if (!i18n.locales.includes(locale)) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const messages = await getMessages(locale)
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="application-name" content="MoeMail" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoeMail" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
zpix.variable,
|
||||
"font-zpix min-h-screen antialiased",
|
||||
"bg-background text-foreground",
|
||||
"transition-colors duration-300"
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange={false}
|
||||
storageKey="temp-mail-theme"
|
||||
>
|
||||
<Providers>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
<FloatMenu />
|
||||
</NextIntlClientProvider>
|
||||
</Providers>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { LoginForm } from "@/components/auth/login-form"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
import type { Locale } from "@/i18n/config"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export default async function LoginPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
const session = await auth()
|
||||
|
||||
if (session?.user) {
|
||||
redirect(`/${locale}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<LoginForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { ThreeColumnLayout } from "@/components/emails/three-column-layout"
|
||||
import { NoPermissionDialog } from "@/components/no-permission-dialog"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
import { checkPermission } from "@/lib/auth"
|
||||
import { PERMISSIONS } from "@/lib/permissions"
|
||||
import type { Locale } from "@/i18n/config"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export default async function MoePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
redirect(`/${locale}`)
|
||||
}
|
||||
|
||||
const hasPermission = await checkPermission(PERMISSIONS.MANAGE_EMAIL)
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 h-screen">
|
||||
<div className="container mx-auto h-full px-4 lg:px-8 max-w-[1600px]">
|
||||
<Header />
|
||||
<main className="h-full">
|
||||
<ThreeColumnLayout />
|
||||
{!hasPermission && <NoPermissionDialog />}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { Shield, Mail, Clock } from "lucide-react"
|
||||
import { ActionButton } from "@/components/home/action-button"
|
||||
import { FeatureCard } from "@/components/home/feature-card"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import type { Locale } from "@/i18n/config"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export default async function Home({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
const session = await auth()
|
||||
const t = await getTranslations({ locale, namespace: "home" })
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 h-screen">
|
||||
<div className="container mx-auto h-full px-4 lg:px-8 max-w-[1600px]">
|
||||
<Header />
|
||||
<main className="h-full">
|
||||
<div className="h-[calc(100vh-4rem)] flex flex-col items-center justify-center text-center px-4 relative">
|
||||
<div className="absolute inset-0 -z-10 bg-grid-primary/5" />
|
||||
|
||||
<div className="w-full max-w-3xl mx-auto space-y-12 py-8">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-wider">
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-purple-600">
|
||||
{t("title")}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-300 tracking-wide">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 px-4 sm:px-0">
|
||||
<FeatureCard
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
title={t("features.privacy.title")}
|
||||
description={t("features.privacy.description")}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Mail className="w-5 h-5" />}
|
||||
title={t("features.instant.title")}
|
||||
description={t("features.instant.description")}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Clock className="w-5 h-5" />}
|
||||
title={t("features.expiry.title")}
|
||||
description={t("features.expiry.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 px-4 sm:px-0">
|
||||
<ActionButton isLoggedIn={!!session} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { ProfileCard } from "@/components/profile/profile-card"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
import type { Locale } from "@/i18n/config"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export default async function ProfilePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale: localeFromParams } = await params
|
||||
const locale = localeFromParams as Locale
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
redirect(`/${locale}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="container mx-auto px-4 lg:px-8 max-w-[1600px]">
|
||||
<Header />
|
||||
<main className="pt-20 pb-5">
|
||||
<ProfileCard user={session.user} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user