From d175017b51aa29d91b7703084ff995cfa19d7719 Mon Sep 17 00:00:00 2001 From: beilunyang <786220806@qq.com> Date: Mon, 13 Oct 2025 00:57:32 +0800 Subject: [PATCH] feat: add internationalization support with next-intl --- README.md | 2 + app/[locale]/layout.tsx | 148 ++++++++++++++++++ app/{ => [locale]}/login/page.tsx | 14 +- app/{ => [locale]}/moe/page.tsx | 14 +- app/{ => [locale]}/page.tsx | 28 ++-- app/{ => [locale]}/profile/page.tsx | 14 +- app/components/auth/login-form.tsx | 68 ++++---- app/components/auth/sign-button.tsx | 15 +- app/components/emails/create-dialog.tsx | 53 ++++--- app/components/emails/email-list.tsx | 35 +++-- .../emails/message-list-container.tsx | 6 +- app/components/emails/message-list.tsx | 30 ++-- app/components/emails/message-view.tsx | 27 ++-- app/components/emails/send-dialog.tsx | 36 +++-- app/components/emails/three-column-layout.tsx | 16 +- app/components/float-menu.tsx | 7 +- app/components/home/action-button.tsx | 7 +- app/components/layout/header.tsx | 2 + app/components/layout/language-switcher.tsx | 62 ++++++++ app/components/no-permission-dialog.tsx | 14 +- app/components/profile/api-key-panel.tsx | 95 +++++------ .../profile/email-service-config.tsx | 49 +++--- app/components/profile/profile-card.tsx | 35 +++-- app/components/profile/promote-panel.tsx | 45 +++--- app/components/profile/webhook-config.tsx | 53 ++++--- .../profile/website-config-panel.tsx | 37 +++-- app/i18n/config.ts | 11 ++ app/i18n/messages/en/auth.json | 43 +++++ app/i18n/messages/en/common.json | 26 +++ app/i18n/messages/en/emails.json | 87 ++++++++++ app/i18n/messages/en/home.json | 23 +++ app/i18n/messages/en/metadata.json | 6 + app/i18n/messages/en/profile.json | 123 +++++++++++++++ app/i18n/messages/zh-CN/auth.json | 43 +++++ app/i18n/messages/zh-CN/common.json | 26 +++ app/i18n/messages/zh-CN/emails.json | 87 ++++++++++ app/i18n/messages/zh-CN/home.json | 23 +++ app/i18n/messages/zh-CN/metadata.json | 6 + app/i18n/messages/zh-CN/profile.json | 123 +++++++++++++++ app/i18n/request.ts | 18 +++ app/layout.tsx | 102 +----------- middleware.ts | 75 +++++---- next-intl.config.ts | 9 ++ next.config.ts | 15 +- package.json | 1 + pnpm-lock.yaml | 109 +++++++++++++ 46 files changed, 1436 insertions(+), 432 deletions(-) create mode 100644 app/[locale]/layout.tsx rename app/{ => [locale]}/login/page.tsx (60%) rename app/{ => [locale]}/moe/page.tsx (77%) rename app/{ => [locale]}/page.tsx (71%) rename app/{ => [locale]}/profile/page.tsx (69%) create mode 100644 app/components/layout/language-switcher.tsx create mode 100644 app/i18n/config.ts create mode 100644 app/i18n/messages/en/auth.json create mode 100644 app/i18n/messages/en/common.json create mode 100644 app/i18n/messages/en/emails.json create mode 100644 app/i18n/messages/en/home.json create mode 100644 app/i18n/messages/en/metadata.json create mode 100644 app/i18n/messages/en/profile.json create mode 100644 app/i18n/messages/zh-CN/auth.json create mode 100644 app/i18n/messages/zh-CN/common.json create mode 100644 app/i18n/messages/zh-CN/emails.json create mode 100644 app/i18n/messages/zh-CN/home.json create mode 100644 app/i18n/messages/zh-CN/metadata.json create mode 100644 app/i18n/messages/zh-CN/profile.json create mode 100644 app/i18n/request.ts create mode 100644 next-intl.config.ts diff --git a/README.md b/README.md index a72f67b..dd7a984 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ - 🔔 **Webhook 通知**:支持通过 webhook 接收新邮件通知 - 🛡️ **权限系统**:支持基于角色的权限控制系统 - 🔑 **OpenAPI**:支持通过 API Key 访问 OpenAPI +- 🌍 **多语言支持**:支持中文和英文界面,可自由切换 ## 技术栈 @@ -64,6 +65,7 @@ - **邮件处理**: [Cloudflare Email Workers](https://developers.cloudflare.com/email-routing/) - **类型安全**: [TypeScript](https://www.typescriptlang.org/) - **ORM**: [Drizzle ORM](https://orm.drizzle.team/) +- **国际化**: [next-intl](https://next-intl-docs.vercel.app/) 支持多语言 ## 本地运行 diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..593ed12 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -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 { + 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 = {} + 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 ( + + + + + + + + + + + + + + {children} + + + + + + + + ) +} + + diff --git a/app/login/page.tsx b/app/[locale]/login/page.tsx similarity index 60% rename from app/login/page.tsx rename to app/[locale]/login/page.tsx index c997f12..f496d35 100644 --- a/app/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -1,14 +1,21 @@ 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() { +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("/") + redirect(`/${locale}`) } return ( @@ -16,4 +23,5 @@ export default async function LoginPage() { ) -} \ No newline at end of file +} + diff --git a/app/moe/page.tsx b/app/[locale]/moe/page.tsx similarity index 77% rename from app/moe/page.tsx rename to app/[locale]/moe/page.tsx index 73f596b..62aa304 100644 --- a/app/moe/page.tsx +++ b/app/[locale]/moe/page.tsx @@ -5,14 +5,21 @@ 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() { +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("/") + redirect(`/${locale}`) } const hasPermission = await checkPermission(PERMISSIONS.MANAGE_EMAIL) @@ -28,4 +35,5 @@ export default async function MoePage() { ) -} \ No newline at end of file +} + diff --git a/app/page.tsx b/app/[locale]/page.tsx similarity index 71% rename from app/page.tsx rename to app/[locale]/page.tsx index 4ea074e..ffad3bd 100644 --- a/app/page.tsx +++ b/app/[locale]/page.tsx @@ -3,11 +3,20 @@ 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() { +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 (
@@ -21,29 +30,29 @@ export default async function Home() {

- MoeMail + {t("title")}

- 萌萌哒临时邮箱服务 + {t("subtitle")}

} - title="隐私保护" - description="保护您的真实邮箱地址" + title={t("features.privacy.title")} + description={t("features.privacy.description")} /> } - title="即时收件" - description="实时接收邮件通知" + title={t("features.instant.title")} + description={t("features.instant.description")} /> } - title="自动过期" - description="到期自动失效" + title={t("features.expiry.title")} + description={t("features.expiry.description")} />
@@ -57,3 +66,4 @@ export default async function Home() {
) } + diff --git a/app/profile/page.tsx b/app/[locale]/profile/page.tsx similarity index 69% rename from app/profile/page.tsx rename to app/[locale]/profile/page.tsx index 543dc9d..be8a222 100644 --- a/app/profile/page.tsx +++ b/app/[locale]/profile/page.tsx @@ -2,14 +2,21 @@ 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() { +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("/") + redirect(`/${locale}`) } return ( @@ -22,4 +29,5 @@ export default async function ProfilePage() { ) -} \ No newline at end of file +} + diff --git a/app/components/auth/login-form.tsx b/app/components/auth/login-form.tsx index eaf9f15..764a3ca 100644 --- a/app/components/auth/login-form.tsx +++ b/app/components/auth/login-form.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import { signIn } from "next-auth/react" +import { useTranslations } from "next-intl" import { useToast } from "@/components/ui/use-toast" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -34,25 +35,26 @@ export function LoginForm() { const [loading, setLoading] = useState(false) const [errors, setErrors] = useState({}) const { toast } = useToast() + const t = useTranslations("auth.loginForm") const validateLoginForm = () => { const newErrors: FormErrors = {} - if (!username) newErrors.username = "请输入用户名" - if (!password) newErrors.password = "请输入密码" - if (username.includes('@')) newErrors.username = "用户名不能包含 @ 符号" - if (password && password.length < 8) newErrors.password = "密码长度必须大于等于8位" + if (!username) newErrors.username = t("errors.usernameRequired") + if (!password) newErrors.password = t("errors.passwordRequired") + if (username.includes('@')) newErrors.username = t("errors.usernameInvalid") + if (password && password.length < 8) newErrors.password = t("errors.passwordTooShort") setErrors(newErrors) return Object.keys(newErrors).length === 0 } const validateRegisterForm = () => { const newErrors: FormErrors = {} - if (!username) newErrors.username = "请输入用户名" - if (!password) newErrors.password = "请输入密码" - if (username.includes('@')) newErrors.username = "用户名不能包含 @ 符号" - if (password && password.length < 8) newErrors.password = "密码长度必须大于等于8位" - if (!confirmPassword) newErrors.confirmPassword = "请确认密码" - if (password !== confirmPassword) newErrors.confirmPassword = "两次输入的密码不一致" + if (!username) newErrors.username = t("errors.usernameRequired") + if (!password) newErrors.password = t("errors.passwordRequired") + if (username.includes('@')) newErrors.username = t("errors.usernameInvalid") + if (password && password.length < 8) newErrors.password = t("errors.passwordTooShort") + if (!confirmPassword) newErrors.confirmPassword = t("errors.confirmPasswordRequired") + if (password !== confirmPassword) newErrors.confirmPassword = t("errors.passwordMismatch") setErrors(newErrors) return Object.keys(newErrors).length === 0 } @@ -70,8 +72,8 @@ export function LoginForm() { if (result?.error) { toast({ - title: "登录失败", - description: "用户名或密码错误", + title: t("toast.loginFailed"), + description: t("toast.loginFailedDesc"), variant: "destructive", }) setLoading(false) @@ -81,8 +83,8 @@ export function LoginForm() { window.location.href = "/" } catch (error) { toast({ - title: "登录失败", - description: error instanceof Error ? error.message : "请稍后重试", + title: t("toast.loginFailed"), + description: error instanceof Error ? error.message : t("toast.registerFailedDesc"), variant: "destructive", }) setLoading(false) @@ -104,8 +106,8 @@ export function LoginForm() { if (!response.ok) { toast({ - title: "注册失败", - description: data.error || "请稍后重试", + title: t("toast.registerFailed"), + description: data.error || t("toast.registerFailedDesc"), variant: "destructive", }) setLoading(false) @@ -121,8 +123,8 @@ export function LoginForm() { if (result?.error) { toast({ - title: "登录失败", - description: "自动登录失败,请手动登录", + title: t("toast.loginFailed"), + description: t("toast.autoLoginFailed"), variant: "destructive", }) setLoading(false) @@ -132,8 +134,8 @@ export function LoginForm() { window.location.href = "/" } catch (error) { toast({ - title: "注册失败", - description: error instanceof Error ? error.message : "请稍后重试", + title: t("toast.registerFailed"), + description: error instanceof Error ? error.message : t("toast.registerFailedDesc"), variant: "destructive", }) setLoading(false) @@ -155,17 +157,17 @@ export function LoginForm() { - 欢迎使用 MoeMail + {t("title")} - 萌萌哒临时邮箱服务 (。・∀・)ノ + {t("subtitle")} - 登录 - 注册 + {t("tabs.login")} + {t("tabs.register")}
@@ -180,7 +182,7 @@ export function LoginForm() { "h-9 pl-9 pr-3", errors.username && "border-destructive focus-visible:ring-destructive" )} - placeholder="用户名" + placeholder={t("fields.username")} value={username} onChange={(e) => { setUsername(e.target.value) @@ -204,7 +206,7 @@ export function LoginForm() { errors.password && "border-destructive focus-visible:ring-destructive" )} type="password" - placeholder="密码" + placeholder={t("fields.password")} value={password} onChange={(e) => { setPassword(e.target.value) @@ -226,7 +228,7 @@ export function LoginForm() { disabled={loading} > {loading && } - 登录 + {t("actions.login")}
@@ -235,7 +237,7 @@ export function LoginForm() {
- 或者 + {t("actions.or")}
@@ -246,7 +248,7 @@ export function LoginForm() { onClick={handleGithubLogin} > - 使用 GitHub 账号登录 + {t("actions.githubLogin")} @@ -262,7 +264,7 @@ export function LoginForm() { "h-9 pl-9 pr-3", errors.username && "border-destructive focus-visible:ring-destructive" )} - placeholder="用户名" + placeholder={t("fields.username")} value={username} onChange={(e) => { setUsername(e.target.value) @@ -286,7 +288,7 @@ export function LoginForm() { errors.password && "border-destructive focus-visible:ring-destructive" )} type="password" - placeholder="密码" + placeholder={t("fields.password")} value={password} onChange={(e) => { setPassword(e.target.value) @@ -310,7 +312,7 @@ export function LoginForm() { errors.confirmPassword && "border-destructive focus-visible:ring-destructive" )} type="password" - placeholder="确认密码" + placeholder={t("fields.confirmPassword")} value={confirmPassword} onChange={(e) => { setConfirmPassword(e.target.value) @@ -332,7 +334,7 @@ export function LoginForm() { disabled={loading} > {loading && } - 注册 + {t("actions.register")} diff --git a/app/components/auth/sign-button.tsx b/app/components/auth/sign-button.tsx index f00e01c..a7ee1c2 100644 --- a/app/components/auth/sign-button.tsx +++ b/app/components/auth/sign-button.tsx @@ -5,6 +5,7 @@ import Image from "next/image" import { signOut, useSession } from "next-auth/react" import { LogIn } from "lucide-react" import { useRouter } from 'next/navigation' +import { useTranslations, useLocale } from "next-intl" import Link from "next/link" import { cn } from "@/lib/utils" @@ -14,7 +15,9 @@ interface SignButtonProps { export function SignButton({ size = "default" }: SignButtonProps) { const router = useRouter() + const locale = useLocale() const { data: session, status } = useSession() + const t = useTranslations("auth.signButton") const loading = status === "loading" if (loading) { @@ -23,9 +26,9 @@ export function SignButton({ size = "default" }: SignButtonProps) { if (!session?.user) { return ( - ) } @@ -33,13 +36,13 @@ export function SignButton({ size = "default" }: SignButtonProps) { return (
{session.user.image && ( {session.user.name{session.user.name} -
) diff --git a/app/components/emails/create-dialog.tsx b/app/components/emails/create-dialog.tsx index c5dc980..84ef207 100644 --- a/app/components/emails/create-dialog.tsx +++ b/app/components/emails/create-dialog.tsx @@ -1,6 +1,7 @@ "use client" import { useEffect, useState } from "react" +import { useTranslations } from "next-intl" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" @@ -19,7 +20,10 @@ interface CreateDialogProps { } export function CreateDialog({ onEmailCreated }: CreateDialogProps) { - const { config } = useConfig() + const { config } = useConfig() + const t = useTranslations("emails.create") + const tList = useTranslations("emails.list") + const tCommon = useTranslations("common.actions") const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [emailName, setEmailName] = useState("") @@ -37,8 +41,8 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) { const createEmail = async () => { if (!emailName.trim()) { toast({ - title: "错误", - description: "请输入邮箱名", + title: tList("error"), + description: t("namePlaceholder"), variant: "destructive" }) return @@ -59,7 +63,7 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) { if (!response.ok) { const data = await response.json() toast({ - title: "错误", + title: tList("error"), description: (data as { error: string }).error, variant: "destructive" }) @@ -67,16 +71,16 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) { } toast({ - title: "成功", - description: "已创建新的临时邮箱" + title: tList("success"), + description: t("success") }) onEmailCreated() setOpen(false) setEmailName("") } catch { toast({ - title: "错误", - description: "创建邮箱失败", + title: tList("error"), + description: t("failed"), variant: "destructive" }) } finally { @@ -95,19 +99,19 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) { - 创建新的临时邮箱 + {t("title")}
setEmailName(e.target.value)} - placeholder="输入邮箱名" + placeholder={t("namePlaceholder")} className="flex-1" /> {(config?.emailDomainsArray?.length ?? 0) > 1 && ( @@ -133,25 +137,28 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) {
- + - {EXPIRY_OPTIONS.map((option) => ( -
- - -
- ))} + {EXPIRY_OPTIONS.map((option, index) => { + const labels = [t("oneHour"), t("oneDay"), t("threeDays"), t("permanent")] + return ( +
+ + +
+ ) + })}
- 完整邮箱地址将为: + {t("domain")}: {emailName ? (
{`${emailName}@${currentDomain}`} @@ -169,10 +176,10 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) {
diff --git a/app/components/emails/email-list.tsx b/app/components/emails/email-list.tsx index 0c9f575..088a07d 100644 --- a/app/components/emails/email-list.tsx +++ b/app/components/emails/email-list.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react" import { useSession } from "next-auth/react" +import { useTranslations } from "next-intl" import { CreateDialog } from "./create-dialog" import { Mail, RefreshCw, Trash2 } from "lucide-react" import { cn } from "@/lib/utils" @@ -45,6 +46,8 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { const { data: session } = useSession() const { config } = useConfig() const { role } = useUserRole() + const t = useTranslations("emails.list") + const tCommon = useTranslations("common.actions") const [emails, setEmails] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) @@ -125,7 +128,7 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { if (!response.ok) { const data = await response.json() toast({ - title: "错误", + title: t("error"), description: (data as { error: string }).error, variant: "destructive" }) @@ -136,8 +139,8 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { setTotal(prev => prev - 1) toast({ - title: "成功", - description: "邮箱已删除" + title: t("success"), + description: t("deleteSuccess") }) if (selectedEmailId === email.id) { @@ -145,8 +148,8 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { } } catch { toast({ - title: "错误", - description: "删除邮箱失败", + title: t("error"), + description: t("deleteFailed"), variant: "destructive" }) } finally { @@ -172,9 +175,9 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { {role === ROLES.EMPEROR ? ( - `${total}/∞ 个邮箱` + t("emailCountUnlimited", { count: total }) ) : ( - `${total}/${config?.maxEmails || EMAIL_CONFIG.MAX_ACTIVE_EMAILS} 个邮箱` + t("emailCount", { count: total, max: config?.maxEmails || EMAIL_CONFIG.MAX_ACTIVE_EMAILS }) )}
@@ -183,7 +186,7 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) {
{loading ? ( -
加载中...
+
{t("loading")}
) : emails.length > 0 ? (
{emails.map(email => ( @@ -200,9 +203,9 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) {
{email.address}
{new Date(email.expiresAt).getFullYear() === 9999 ? ( - "永久有效" + t("permanent") ) : ( - `过期时间: ${new Date(email.expiresAt).toLocaleString()}` + `${t("expiresAt")}: ${new Date(email.expiresAt).toLocaleString()}` )}
@@ -221,13 +224,13 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { ))} {loadingMore && (
- 加载更多... + {t("loadingMore")}
)}
) : (
- 还没有邮箱,创建一个吧! + {t("noEmails")}
)}
@@ -236,18 +239,18 @@ export function EmailList({ onEmailSelect, selectedEmailId }: EmailListProps) { setEmailToDelete(null)}> - 确认删除 + {t("deleteConfirm")} - 确定要删除邮箱 {emailToDelete?.address} 吗?此操作将同时删除该邮箱中的所有邮件,且不可恢复。 + {t("deleteDescription", { email: emailToDelete?.address })} - 取消 + {tCommon("cancel")} emailToDelete && handleDelete(emailToDelete)} > - 删除 + {tCommon("delete")} diff --git a/app/components/emails/message-list-container.tsx b/app/components/emails/message-list-container.tsx index 3214aa6..9a9a41b 100644 --- a/app/components/emails/message-list-container.tsx +++ b/app/components/emails/message-list-container.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useTranslations } from "next-intl" import { Send, Inbox } from "lucide-react" import { Tabs, SlidingTabsList, SlidingTabsTrigger, TabsContent } from "@/components/ui/tabs" import { MessageList } from "./message-list" @@ -17,6 +18,7 @@ interface MessageListContainerProps { } export function MessageListContainer({ email, onMessageSelect, selectedMessageId, refreshTrigger }: MessageListContainerProps) { + const t = useTranslations("emails.messages") const [activeTab, setActiveTab] = useState<'received' | 'sent'>('received') const { canSend: canSendEmails } = useSendPermission() @@ -33,11 +35,11 @@ export function MessageListContainer({ email, onMessageSelect, selectedMessageId - 收件箱 + {t("received")} - 已发送 + {t("sent")} diff --git a/app/components/emails/message-list.tsx b/app/components/emails/message-list.tsx index 2015849..8f71332 100644 --- a/app/components/emails/message-list.tsx +++ b/app/components/emails/message-list.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useEffect, useRef } from "react" +import { useTranslations } from "next-intl" import {Mail, Calendar, RefreshCw, Trash2} from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" @@ -47,6 +48,9 @@ interface MessageResponse { } export function MessageList({ email, messageType, onMessageSelect, selectedMessageId, refreshTrigger }: MessageListProps) { + const t = useTranslations("emails.messages") + const tList = useTranslations("emails.list") + const tCommon = useTranslations("common.actions") const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) @@ -149,7 +153,7 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa if (!response.ok) { const data = await response.json() toast({ - title: "错误", + title: tList("error"), description: (data as { error: string }).error, variant: "destructive" }) @@ -160,8 +164,8 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa setTotal(prev => prev - 1) toast({ - title: "成功", - description: "邮件已删除" + title: tList("success"), + description: tList("deleteSuccess") }) if (selectedMessageId === message.id) { @@ -169,8 +173,8 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa } } catch { toast({ - title: "错误", - description: "删除邮件失败", + title: tList("error"), + description: tList("deleteFailed"), variant: "destructive" }) } finally { @@ -215,13 +219,13 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa - {total > 0 ? `${total} 封邮件` : "暂无邮件"} + {total > 0 ? `${total} ${t("noMessages")}` : t("noMessages")}
{loading ? ( -
加载中...
+
{t("loading")}
) : messages.length > 0 ? (
{messages.map(message => ( @@ -263,13 +267,13 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa ))} {loadingMore && (
- 加载更多... + {t("loadingMore")}
)}
) : (
- {messageType === 'sent' ? '暂无发送的邮件' : '暂无收到的邮件'} + {t("noMessages")}
)}
@@ -277,18 +281,18 @@ export function MessageList({ email, messageType, onMessageSelect, selectedMessa setMessageToDelete(null)}> - 确认删除 + {tList("deleteConfirm")} - 确定要删除邮件 {messageToDelete?.subject} 吗? + {tList("deleteDescription", { email: messageToDelete?.subject })} - 取消 + {tCommon("cancel")} messageToDelete && handleDelete(messageToDelete)} > - 删除 + {tCommon("delete")} diff --git a/app/components/emails/message-view.tsx b/app/components/emails/message-view.tsx index b66adfa..75b7f0e 100644 --- a/app/components/emails/message-view.tsx +++ b/app/components/emails/message-view.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useEffect, useRef } from "react" +import { useTranslations } from "next-intl" import { Loader2 } from "lucide-react" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { Label } from "@/components/ui/label" @@ -28,6 +29,8 @@ interface MessageViewProps { type ViewMode = "html" | "text" export function MessageView({ emailId, messageId, messageType = 'received' }: MessageViewProps) { + const t = useTranslations("emails.messageView") + const tList = useTranslations("emails.list") const [message, setMessage] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -48,10 +51,10 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me if (!response.ok) { const errorData = await response.json() - const errorMessage = (errorData as { error?: string }).error || '获取邮件详情失败' + const errorMessage = (errorData as { error?: string }).error || t("loadError") setError(errorMessage) toast({ - title: "错误", + title: tList("error"), description: errorMessage, variant: "destructive" }) @@ -64,10 +67,10 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me setViewMode("text") } } catch (error) { - const errorMessage = "网络错误,请稍后重试" + const errorMessage = t("networkError") setError(errorMessage) toast({ - title: "错误", + title: tList("error"), description: errorMessage, variant: "destructive" }) @@ -78,7 +81,7 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me } fetchMessage() - }, [emailId, messageId, messageType, toast]) + }, [emailId, messageId, messageType, toast, t, tList]) const updateIframeContent = () => { if (viewMode === "html" && message?.html && iframeRef.current) { @@ -182,7 +185,7 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me return (
- 加载邮件详情... + {t("loading")}
) } @@ -195,7 +198,7 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me onClick={() => window.location.reload()} className="text-xs text-primary hover:underline" > - 点击重试 + {t("retry")} ) @@ -209,12 +212,12 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me

{message.subject}

{message.from_address && ( -

发件人:{message.from_address}

+

{t("from")}: {message.from_address}

)} {message.to_address && ( -

收件人:{message.to_address}

+

{t("to")}: {message.to_address}

)} -

时间:{new Date(message.sent_at || message.received_at || 0).toLocaleString()}

+

{t("time")}: {new Date(message.sent_at || message.received_at || 0).toLocaleString()}

@@ -231,7 +234,7 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me htmlFor="html" className="text-xs cursor-pointer" > - HTML 格式 + {t("htmlFormat")}
@@ -240,7 +243,7 @@ export function MessageView({ emailId, messageId, messageType = 'received' }: Me htmlFor="text" className="text-xs cursor-pointer" > - 纯文本格式 + {t("textFormat")}
diff --git a/app/components/emails/send-dialog.tsx b/app/components/emails/send-dialog.tsx index c497f7f..e97a9c5 100644 --- a/app/components/emails/send-dialog.tsx +++ b/app/components/emails/send-dialog.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useTranslations } from "next-intl" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" @@ -21,6 +22,9 @@ interface SendDialogProps { } export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogProps) { + const t = useTranslations("emails.send") + const tList = useTranslations("emails.list") + const tCommon = useTranslations("common.actions") const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [to, setTo] = useState("") @@ -31,8 +35,8 @@ export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogPr const handleSend = async () => { if (!to.trim() || !subject.trim() || !content.trim()) { toast({ - title: "错误", - description: "收件人、主题和内容都是必填项", + title: tList("error"), + description: t("toPlaceholder") + ", " + t("subjectPlaceholder") + ", " + t("contentPlaceholder"), variant: "destructive" }) return @@ -49,7 +53,7 @@ export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogPr if (!response.ok) { const data = await response.json() toast({ - title: "错误", + title: tList("error"), description: (data as { error: string }).error, variant: "destructive" }) @@ -57,8 +61,8 @@ export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogPr } toast({ - title: "成功", - description: "邮件已发送" + title: tList("success"), + description: t("success") }) setOpen(false) setTo("") @@ -69,8 +73,8 @@ export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogPr } catch { toast({ - title: "错误", - description: "发送邮件失败", + title: tList("error"), + description: t("failed"), variant: "destructive" }) } finally { @@ -90,46 +94,46 @@ export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogPr className="h-8 gap-2 hover:bg-primary/10 hover:text-primary transition-colors" > - 发送邮件 + {t("title")} -

使用此邮箱发送新邮件

+

{t("title")}

- 发送新邮件 + {t("title")}
- 发件人: {fromAddress} + {t("from")}: {fromAddress}
) => setTo(e.target.value)} - placeholder="收件人邮箱地址" + placeholder={t("toPlaceholder")} /> ) => setSubject(e.target.value)} - placeholder="邮件主题" + placeholder={t("subjectPlaceholder")} />