mirror of
https://github.com/beilunyang/moemail.git
synced 2025-12-24 11:30:51 +08:00
feat(i18n): enhance localization support with new languages(zh-tw/ja)
This commit is contained in:
@@ -70,7 +70,7 @@ export async function generateMetadata({
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: locale === "zh-CN" ? "zh_CN" : locale,
|
||||
locale: locale === "zh-CN" ? "zh_CN" : locale === "zh-TW" ? "zh_TW" : locale,
|
||||
url: `${baseUrl}/${locale}`,
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
@@ -145,4 +145,3 @@ export default async function LocaleLayout({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { i18n } from "@/i18n/config"
|
||||
import { useLocaleSwitcher } from "@/hooks/use-locale-switcher"
|
||||
import { LOCALE_LABELS } from "@/i18n/config"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Languages } from "lucide-react"
|
||||
import {
|
||||
@@ -13,38 +12,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function FloatingLanguageSwitcher() {
|
||||
const t = useTranslations("common")
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const switchLocale = (newLocale: string) => {
|
||||
if (newLocale === locale) return
|
||||
|
||||
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`
|
||||
|
||||
const segments = pathname.split("/")
|
||||
if (i18n.locales.includes(segments[1] as any)) {
|
||||
segments[1] = newLocale
|
||||
} else {
|
||||
segments.splice(1, 0, newLocale)
|
||||
}
|
||||
const newPath = segments.join("/")
|
||||
|
||||
router.push(newPath)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
const getLanguageName = (loc: string) => {
|
||||
switch (loc) {
|
||||
case "en":
|
||||
return "English"
|
||||
case "zh-CN":
|
||||
return "简体中文"
|
||||
default:
|
||||
return loc
|
||||
}
|
||||
}
|
||||
const { locale, locales, switchLocale } = useLocaleSwitcher()
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
@@ -54,19 +22,19 @@ export function FloatingLanguageSwitcher() {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-white dark:bg-background rounded-full shadow-lg group relative border-primary/20 hover:border-primary/40 transition-all"
|
||||
aria-label="Switch language"
|
||||
>
|
||||
<Languages className="h-5 w-5 text-primary group-hover:scale-110 transition-transform" />
|
||||
<span className="sr-only">{t("lang.switch")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="mb-2">
|
||||
{i18n.locales.map((loc) => (
|
||||
{locales.map((loc) => (
|
||||
<DropdownMenuItem
|
||||
key={loc}
|
||||
onClick={() => switchLocale(loc)}
|
||||
className={locale === loc ? "bg-accent" : ""}
|
||||
>
|
||||
{getLanguageName(loc)}
|
||||
{LOCALE_LABELS[loc]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { i18n } from "@/i18n/config"
|
||||
import { useLocaleSwitcher } from "@/hooks/use-locale-switcher"
|
||||
import { LOCALE_LABELS } from "@/i18n/config"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,49 +12,25 @@ import { Button } from "@/components/ui/button"
|
||||
import { Languages } from "lucide-react"
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const t = useTranslations("common")
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const switchLocale = (newLocale: string) => {
|
||||
if (newLocale === locale) return
|
||||
|
||||
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`
|
||||
|
||||
const segments = pathname.split("/")
|
||||
if (i18n.locales.includes(segments[1] as any)) {
|
||||
segments[1] = newLocale
|
||||
} else {
|
||||
segments.splice(1, 0, newLocale)
|
||||
}
|
||||
const newPath = segments.join("/")
|
||||
|
||||
router.push(newPath)
|
||||
router.refresh()
|
||||
}
|
||||
const { locale, locales, switchLocale } = useLocaleSwitcher()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Button variant="ghost" size="icon" aria-label="Switch language">
|
||||
<Languages className="h-5 w-5" />
|
||||
<span className="sr-only">{t("lang.switch")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => switchLocale("en")}
|
||||
className={locale === "en" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("lang.en")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => switchLocale("zh-CN")}
|
||||
className={locale === "zh-CN" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("lang.zhCN")}
|
||||
</DropdownMenuItem>
|
||||
{locales.map((loc) => (
|
||||
<DropdownMenuItem
|
||||
key={loc}
|
||||
onClick={() => switchLocale(loc)}
|
||||
className={locale === loc ? "bg-accent" : ""}
|
||||
>
|
||||
{LOCALE_LABELS[loc]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
38
app/hooks/use-locale-switcher.ts
Normal file
38
app/hooks/use-locale-switcher.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback } from "react"
|
||||
import { useLocale } from "next-intl"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { i18n, type Locale } from "@/i18n/config"
|
||||
|
||||
export function useLocaleSwitcher() {
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const switchLocale = useCallback(
|
||||
(newLocale: Locale) => {
|
||||
if (newLocale === locale) return
|
||||
|
||||
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`
|
||||
|
||||
const segments = pathname.split("/")
|
||||
if (i18n.locales.includes(segments[1] as Locale)) {
|
||||
segments[1] = newLocale
|
||||
} else {
|
||||
segments.splice(1, 0, newLocale)
|
||||
}
|
||||
|
||||
router.push(segments.join("/"))
|
||||
router.refresh()
|
||||
},
|
||||
[locale, pathname, router]
|
||||
)
|
||||
|
||||
return {
|
||||
locale,
|
||||
switchLocale,
|
||||
locales: i18n.locales,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
export const locales = ['en', 'zh-CN'] as const
|
||||
export const locales = ['en', 'zh-CN', 'zh-TW', 'ja'] as const
|
||||
export type Locale = typeof locales[number]
|
||||
|
||||
export const LOCALE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
ja: "日本語",
|
||||
}
|
||||
|
||||
export const defaultLocale: Locale = 'en'
|
||||
|
||||
export const i18n = {
|
||||
@@ -8,4 +15,3 @@ export const i18n = {
|
||||
defaultLocale,
|
||||
localePrefix: 'as-needed',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
"app": {
|
||||
"name": "MoeMail"
|
||||
},
|
||||
"lang": {
|
||||
"en": "English",
|
||||
"zhCN": "简体中文",
|
||||
"switch": "Switch Language"
|
||||
},
|
||||
"actions": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
@@ -22,5 +17,3 @@
|
||||
},
|
||||
"github": "Get Source Code"
|
||||
}
|
||||
|
||||
|
||||
|
||||
43
app/i18n/messages/ja/auth.json
Normal file
43
app/i18n/messages/ja/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"signButton": {
|
||||
"login": "ログイン/登録",
|
||||
"logout": "ログアウト",
|
||||
"userAvatar": "ユーザーアバター",
|
||||
"linked": "連携済み"
|
||||
},
|
||||
"loginForm": {
|
||||
"title": "MoeMailへようこそ",
|
||||
"subtitle": "かわいい使い捨てメールサービス (。・∀・)ノ",
|
||||
"tabs": {
|
||||
"login": "ログイン",
|
||||
"register": "登録"
|
||||
},
|
||||
"fields": {
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"confirmPassword": "パスワードを確認"
|
||||
},
|
||||
"actions": {
|
||||
"login": "ログイン",
|
||||
"register": "登録",
|
||||
"or": "または",
|
||||
"githubLogin": "GitHub アカウントでログイン"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "ユーザー名を入力してください",
|
||||
"passwordRequired": "パスワードを入力してください",
|
||||
"confirmPasswordRequired": "パスワードを確認してください",
|
||||
"usernameInvalid": "ユーザー名に @ を含めることはできません",
|
||||
"passwordTooShort": "パスワードは8文字以上である必要があります",
|
||||
"passwordMismatch": "パスワードが一致しません"
|
||||
},
|
||||
"toast": {
|
||||
"loginFailed": "ログインに失敗しました",
|
||||
"loginFailedDesc": "ユーザー名またはパスワードが正しくありません",
|
||||
"registerFailed": "登録に失敗しました",
|
||||
"registerFailedDesc": "しばらくしてからもう一度お試しください",
|
||||
"autoLoginFailed": "自動ログインに失敗しました。手動でログインしてください"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
app/i18n/messages/ja/common.json
Normal file
20
app/i18n/messages/ja/common.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "MoeMail"
|
||||
},
|
||||
"actions": {
|
||||
"ok": "OK",
|
||||
"cancel": "キャンセル",
|
||||
"save": "保存",
|
||||
"delete": "削除"
|
||||
},
|
||||
"nav": {
|
||||
"home": "ホーム",
|
||||
"login": "ログイン",
|
||||
"profile": "プロフィール",
|
||||
"logout": "ログアウト",
|
||||
"backToMailbox": "メールボックスに戻る"
|
||||
},
|
||||
"github": "ソースコードを入手"
|
||||
}
|
||||
|
||||
168
app/i18n/messages/ja/emails.json
Normal file
168
app/i18n/messages/ja/emails.json
Normal file
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"noPermission": {
|
||||
"title": "権限がありません",
|
||||
"description": "このページにアクセスする権限がありません。サイト管理者に連絡してください",
|
||||
"adminContact": "管理者の連絡先",
|
||||
"backToHome": "ホームに戻る"
|
||||
},
|
||||
"layout": {
|
||||
"myEmails": "マイメールボックス",
|
||||
"selectEmail": "メールボックスを選択してメッセージを表示",
|
||||
"messageContent": "メール内容",
|
||||
"selectMessage": "メールを選択して詳細を表示",
|
||||
"backToEmailList": "← メールボックス一覧に戻る",
|
||||
"backToMessageList": "← メッセージ一覧に戻る"
|
||||
},
|
||||
"list": {
|
||||
"emailCount": "{count}/{max} 個のメールボックス",
|
||||
"emailCountUnlimited": "{count}/∞ 個のメールボックス",
|
||||
"loading": "読み込み中...",
|
||||
"loadingMore": "さらに読み込み中...",
|
||||
"noEmails": "まだメールボックスがありません。作成しましょう!",
|
||||
"expiresAt": "有効期限",
|
||||
"permanent": "永久",
|
||||
"deleteConfirm": "削除の確認",
|
||||
"deleteDescription": "メールボックス {email} を削除しますか?この操作によってメールボックス内のすべてのメールが削除され、元に戻せません。",
|
||||
"deleteSuccess": "メールボックスを削除しました",
|
||||
"deleteFailed": "メールボックスの削除に失敗しました",
|
||||
"error": "エラー",
|
||||
"success": "成功"
|
||||
},
|
||||
"create": {
|
||||
"title": "メールボックスを作成",
|
||||
"name": "メールボックスのプレフィックス",
|
||||
"namePlaceholder": "空白の場合はランダムに生成",
|
||||
"domain": "ドメイン",
|
||||
"domainPlaceholder": "ドメインを選択",
|
||||
"expiryTime": "有効期間",
|
||||
"oneHour": "1時間",
|
||||
"oneDay": "1日",
|
||||
"threeDays": "3日",
|
||||
"permanent": "永久",
|
||||
"create": "作成",
|
||||
"creating": "作成中...",
|
||||
"success": "メールボックスを作成しました",
|
||||
"failed": "メールボックスの作成に失敗しました"
|
||||
},
|
||||
"messages": {
|
||||
"received": "受信箱",
|
||||
"sent": "送信済み",
|
||||
"noMessages": "メールはまだありません",
|
||||
"messageCount": "件のメール",
|
||||
"from": "差出人",
|
||||
"to": "宛先",
|
||||
"subject": "件名",
|
||||
"date": "日付",
|
||||
"loading": "読み込み中...",
|
||||
"loadingMore": "さらに読み込み中..."
|
||||
},
|
||||
"send": {
|
||||
"title": "メールを送信",
|
||||
"from": "差出人",
|
||||
"to": "宛先",
|
||||
"toPlaceholder": "宛先メールアドレス",
|
||||
"subject": "件名",
|
||||
"subjectPlaceholder": "メールの件名",
|
||||
"content": "内容",
|
||||
"contentPlaceholder": "メール内容(HTML 対応)",
|
||||
"send": "送信",
|
||||
"sending": "送信中...",
|
||||
"success": "メールを送信しました",
|
||||
"failed": "メールの送信に失敗しました",
|
||||
"dailyLimitReached": "1日の送信上限に達しました",
|
||||
"dailyLimit": "1日の上限: {count}/{max}",
|
||||
"dailyLimitUnit": "通/日"
|
||||
},
|
||||
"messageView": {
|
||||
"loading": "メール詳細を読み込み中...",
|
||||
"loadError": "メール詳細の取得に失敗しました",
|
||||
"networkError": "ネットワークエラーが発生しました。しばらくしてから再試行してください",
|
||||
"retry": "クリックして再試行",
|
||||
"from": "差出人",
|
||||
"to": "宛先",
|
||||
"time": "時間",
|
||||
"htmlFormat": "HTML 表示",
|
||||
"textFormat": "テキスト表示"
|
||||
},
|
||||
"share": {
|
||||
"title": "メールボックスを共有",
|
||||
"description": "共有リンクを作成して、このメールボックス内のメールを他の人に見せましょう",
|
||||
"createLink": "リンクを作成",
|
||||
"creating": "作成中...",
|
||||
"loading": "読み込み中...",
|
||||
"expiryTime": "リンクの有効期間",
|
||||
"oneHour": "1時間",
|
||||
"oneDay": "1日",
|
||||
"threeDays": "3日",
|
||||
"oneWeek": "1週間",
|
||||
"permanent": "永久",
|
||||
"activeLinks": "現在の共有リンク",
|
||||
"noLinks": "共有リンクはまだありません",
|
||||
"createdAt": "作成日時",
|
||||
"expiresAt": "期限",
|
||||
"expired": "期限切れ",
|
||||
"copy": "リンクをコピー",
|
||||
"copied": "コピーしました",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"delete": "削除",
|
||||
"deleteConfirm": "共有リンクを削除しますか?",
|
||||
"deleteDescription": "この操作は取り消せません。共有リンクはすぐに無効になります。",
|
||||
"cancel": "キャンセル",
|
||||
"deleteSuccess": "削除しました",
|
||||
"deleteFailed": "削除に失敗しました",
|
||||
"createSuccess": "共有リンクを作成しました",
|
||||
"createFailed": "共有リンクの作成に失敗しました",
|
||||
"shareButton": "共有"
|
||||
},
|
||||
"shareMessage": {
|
||||
"title": "メールを共有",
|
||||
"description": "共有リンクを作成して、このメールを他の人に見せましょう",
|
||||
"createLink": "リンクを作成",
|
||||
"creating": "作成中...",
|
||||
"loading": "読み込み中...",
|
||||
"expiryTime": "リンクの有効期間",
|
||||
"oneHour": "1時間",
|
||||
"oneDay": "1日",
|
||||
"threeDays": "3日",
|
||||
"oneWeek": "1週間",
|
||||
"permanent": "永久",
|
||||
"activeLinks": "現在の共有リンク",
|
||||
"noLinks": "共有リンクはまだありません",
|
||||
"createdAt": "作成日時",
|
||||
"expiresAt": "期限",
|
||||
"expired": "期限切れ",
|
||||
"copy": "リンクをコピー",
|
||||
"copied": "コピーしました",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"delete": "削除",
|
||||
"deleteConfirm": "共有リンクを削除しますか?",
|
||||
"deleteDescription": "この操作は取り消せません。共有リンクはすぐに無効になります。",
|
||||
"cancel": "キャンセル",
|
||||
"deleteSuccess": "削除しました",
|
||||
"deleteFailed": "削除に失敗しました",
|
||||
"createSuccess": "共有リンクを作成しました",
|
||||
"createFailed": "共有リンクの作成に失敗しました",
|
||||
"shareButton": "メールを共有"
|
||||
},
|
||||
"shared": {
|
||||
"loading": "読み込み中...",
|
||||
"emailNotFound": "メールボックスにアクセスできません",
|
||||
"messageNotFound": "メールにアクセスできません",
|
||||
"linkExpired": "共有リンクが存在しないか期限切れです",
|
||||
"linkInvalid": "リンクが無効です",
|
||||
"linkInvalidDescription": "この共有リンクは期限切れか存在しない可能性があります",
|
||||
"sharedMailbox": "共有メールボックス",
|
||||
"sharedMessage": "共有メール",
|
||||
"expiresAt": "メールボックスの期限",
|
||||
"permanent": "永久",
|
||||
"createOwnEmail": "自分の使い捨てメールを作成する",
|
||||
"brand": {
|
||||
"title": "MoeMail",
|
||||
"subtitle": "かわいい使い捨てメールサービス",
|
||||
"cta": "今すぐ体験",
|
||||
"officialSite": "公式サイト",
|
||||
"copyright": "© 2024 MoeMail. かわいい使い捨てメールサービス"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
app/i18n/messages/ja/home.json
Normal file
27
app/i18n/messages/ja/home.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"title": "MoeMail",
|
||||
"subtitle": "かわいい使い捨てメールサービス",
|
||||
"features": {
|
||||
"privacy": {
|
||||
"title": "プライバシー保護",
|
||||
"description": "本物のメールアドレスを保護します"
|
||||
},
|
||||
"instant": {
|
||||
"title": "メールボックス共有",
|
||||
"description": "メールボックスを他の人と共有できます"
|
||||
},
|
||||
"expiry": {
|
||||
"title": "自動期限切れ",
|
||||
"description": "有効期限が切れると自動的に無効になります"
|
||||
},
|
||||
"openapi": {
|
||||
"title": "オープンAPI",
|
||||
"description": "完全な OpenAPI インターフェースを提供"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"enterMailbox": "メールボックスに入る",
|
||||
"getStarted": "今すぐ始める"
|
||||
}
|
||||
}
|
||||
|
||||
6
app/i18n/messages/ja/metadata.json
Normal file
6
app/i18n/messages/ja/metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "MoeMail - かわいい使い捨てメールサービス · オープンAPI",
|
||||
"description": "安全で高速な使い捨てメールアドレスでプライバシーを守り、スパムを遠ざけます。メールボックス共有、即時受信、期限到来での自動失効に対応。完全な OpenAPI を提供し、開発者の統合や自動テストに最適です。",
|
||||
"keywords": "使い捨てメール, 一時メール, 匿名メール, メール共有, プライバシー保護, スパム対策, 即時受信, 自動失効, 安全なメール, 登録確認, テスト用メール, 電話番号不要, 開発テスト, 自動テスト, メールAPI, OpenAPI, APIインターフェース, RESTful API, APIキー, 開発者ツール, MoeMail"
|
||||
}
|
||||
|
||||
131
app/i18n/messages/ja/profile.json
Normal file
131
app/i18n/messages/ja/profile.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"title": "プロフィール",
|
||||
"card": {
|
||||
"title": "ユーザー情報",
|
||||
"name": "ユーザー名",
|
||||
"role": "ロール",
|
||||
"roles": {
|
||||
"EMPEROR": "皇帝",
|
||||
"DUKE": "公爵",
|
||||
"KNIGHT": "騎士",
|
||||
"CIVILIAN": "市民"
|
||||
}
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"description": "OpenAPI にアクセスするための API キーを作成・管理します",
|
||||
"create": "API Key を作成",
|
||||
"name": "キー名",
|
||||
"namePlaceholder": "キー名を入力",
|
||||
"key": "API Key",
|
||||
"createdAt": "作成日時",
|
||||
"copy": "コピー",
|
||||
"delete": "削除",
|
||||
"noKeys": "API Key はまだありません",
|
||||
"createSuccess": "API Key を作成しました",
|
||||
"createFailed": "API Key の作成に失敗しました",
|
||||
"deleteConfirm": "削除の確認",
|
||||
"deleteDescription": "API Key {name} を削除しますか?この操作は元に戻せません。",
|
||||
"deleteSuccess": "API Key を削除しました",
|
||||
"deleteFailed": "API Key の削除に失敗しました",
|
||||
"viewDocs": "ドキュメントを見る",
|
||||
"docs": {
|
||||
"getConfig": "システム設定を取得",
|
||||
"generateEmail": "使い捨てメールを生成",
|
||||
"getEmails": "メールボックス一覧を取得",
|
||||
"getMessages": "メール一覧を取得",
|
||||
"getMessage": "メールを1件取得",
|
||||
"createEmailShare": "メールボックス共有リンクを作成",
|
||||
"getEmailShares": "メールボックス共有リンク一覧を取得",
|
||||
"deleteEmailShare": "メールボックス共有リンクを削除",
|
||||
"createMessageShare": "メール共有リンクを作成",
|
||||
"getMessageShares": "メール共有リンク一覧を取得",
|
||||
"deleteMessageShare": "メール共有リンクを削除",
|
||||
"notes": "注意:",
|
||||
"note1": "YOUR_API_KEY を実際の API Key に置き換えてください",
|
||||
"note2": "/api/config エンドポイントで利用可能なメールボックスドメインなどのシステム設定を取得できます",
|
||||
"note3": "emailId はメールボックスの一意な識別子です",
|
||||
"note4": "messageId はメールの一意な識別子です",
|
||||
"note5": "expiryTime はメールボックスの有効期間(ミリ秒)です。利用可能な値: 3600000(1時間)、86400000(1日)、604800000(7日)、0(永久)",
|
||||
"note6": "domain はメールボックスのドメインで、/api/config エンドポイントで利用可能な一覧を取得できます",
|
||||
"note7": "cursor はページネーション用で、前回のレスポンスから nextCursor を取得してください",
|
||||
"note8": "すべてのリクエストには X-API-Key ヘッダーが必要です",
|
||||
"note9": "expiresIn は共有リンクの有効期間(ミリ秒)で、0 は永久を意味します",
|
||||
"note10": "shareId は共有記録の一意な識別子です"
|
||||
}
|
||||
},
|
||||
"emailService": {
|
||||
"title": "Resend 送信サービス設定",
|
||||
"configRoleLabel": "設定可能なロール権限",
|
||||
"enable": "メール送信を有効にする",
|
||||
"fixedRoleLimits": "固定権限ルール",
|
||||
"emperorLimit": "皇帝は制限なく無制限に送信できます",
|
||||
"civilianLimit": "市民は送信できません",
|
||||
"enableDescription": "有効にすると Resend を利用してメールを送信します",
|
||||
"apiKey": "Resend API Key",
|
||||
"apiKeyPlaceholder": "Resend API Key を入力",
|
||||
"dailyLimit": "1日の上限",
|
||||
"roleLimits": "送信機能を許可するロール",
|
||||
"save": "設定を保存",
|
||||
"saving": "保存中...",
|
||||
"saveSuccess": "設定を保存しました",
|
||||
"saveFailed": "設定の保存に失敗しました",
|
||||
"unlimited": "無制限",
|
||||
"disabled": "送信権限は無効です",
|
||||
"enabled": "送信権限が有効です"
|
||||
},
|
||||
"webhook": {
|
||||
"title": "Webhook 設定",
|
||||
"description": "新しいメールを受信した際に指定した URL に通知します",
|
||||
"description2": "この URL に新しいメール情報を含む POST リクエストを送信します",
|
||||
"description3": "データ形式を見る",
|
||||
"enable": "Webhook を有効化",
|
||||
"url": "Webhook URL",
|
||||
"urlPlaceholder": "Webhook URL を入力",
|
||||
"test": "テスト",
|
||||
"testing": "テスト中...",
|
||||
"save": "設定を保存",
|
||||
"saving": "保存中...",
|
||||
"saveSuccess": "設定を保存しました",
|
||||
"saveFailed": "設定の保存に失敗しました",
|
||||
"testSuccess": "Webhook テストに成功しました",
|
||||
"testFailed": "Webhook テストに失敗しました",
|
||||
"docs": {
|
||||
"intro": "新しいメールを受信すると、設定した URL に POST リクエストを送信します。リクエストヘッダーには以下が含まれます:",
|
||||
"exampleBody": "リクエストボディ例:",
|
||||
"subject": "メール件名",
|
||||
"content": "メールテキスト内容",
|
||||
"html": "メール HTML 内容"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"title": "サイト設定",
|
||||
"description": "サイトの設定を管理(皇帝のみ利用可能)",
|
||||
"defaultRole": "新規ユーザーのデフォルトロール",
|
||||
"emailDomains": "メールボックスドメイン",
|
||||
"emailDomainsPlaceholder": "複数のドメインはカンマで区切ります",
|
||||
"adminContact": "管理者連絡先",
|
||||
"adminContactPlaceholder": "メールまたはその他の連絡先",
|
||||
"maxEmails": "ユーザーあたりの最大メールボックス数",
|
||||
"save": "設定を保存",
|
||||
"saving": "保存中...",
|
||||
"saveSuccess": "設定を保存しました",
|
||||
"saveFailed": "設定の保存に失敗しました"
|
||||
},
|
||||
"promote": {
|
||||
"title": "ロール管理",
|
||||
"description": "ユーザーロールを管理(皇帝のみ利用可能)",
|
||||
"search": "ユーザーを検索",
|
||||
"searchPlaceholder": "ユーザー名またはメールを入力",
|
||||
"username": "ユーザー名",
|
||||
"email": "メール",
|
||||
"role": "ロール",
|
||||
"actions": "操作",
|
||||
"promote": "設定",
|
||||
"noUsers": "ユーザーが見つかりません",
|
||||
"loading": "読み込み中...",
|
||||
"updateSuccess": "ユーザーロールを更新しました",
|
||||
"updateFailed": "ユーザーロールの更新に失敗しました"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
"app": {
|
||||
"name": "MoeMail"
|
||||
},
|
||||
"lang": {
|
||||
"en": "English",
|
||||
"zhCN": "简体中文",
|
||||
"switch": "切换语言"
|
||||
},
|
||||
"actions": {
|
||||
"ok": "确定",
|
||||
"cancel": "取消",
|
||||
@@ -22,5 +17,3 @@
|
||||
},
|
||||
"github": "获取网站源代码"
|
||||
}
|
||||
|
||||
|
||||
|
||||
43
app/i18n/messages/zh-TW/auth.json
Normal file
43
app/i18n/messages/zh-TW/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"signButton": {
|
||||
"login": "登入/註冊",
|
||||
"logout": "登出",
|
||||
"userAvatar": "使用者頭像",
|
||||
"linked": "已關聯"
|
||||
},
|
||||
"loginForm": {
|
||||
"title": "歡迎使用 MoeMail",
|
||||
"subtitle": "萌萌噠臨時郵箱服務 (。・∀・)ノ",
|
||||
"tabs": {
|
||||
"login": "登入",
|
||||
"register": "註冊"
|
||||
},
|
||||
"fields": {
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"confirmPassword": "確認密碼"
|
||||
},
|
||||
"actions": {
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"or": "或者",
|
||||
"githubLogin": "使用 GitHub 帳號登入"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "請輸入使用者名稱",
|
||||
"passwordRequired": "請輸入密碼",
|
||||
"confirmPasswordRequired": "請確認密碼",
|
||||
"usernameInvalid": "使用者名稱不能包含 @ 符號",
|
||||
"passwordTooShort": "密碼長度必須大於等於8位",
|
||||
"passwordMismatch": "兩次輸入的密碼不一致"
|
||||
},
|
||||
"toast": {
|
||||
"loginFailed": "登入失敗",
|
||||
"loginFailedDesc": "使用者名稱或密碼錯誤",
|
||||
"registerFailed": "註冊失敗",
|
||||
"registerFailedDesc": "請稍後再試",
|
||||
"autoLoginFailed": "自動登入失敗,請手動登入"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
app/i18n/messages/zh-TW/common.json
Normal file
19
app/i18n/messages/zh-TW/common.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "MoeMail"
|
||||
},
|
||||
"actions": {
|
||||
"ok": "確定",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"delete": "刪除"
|
||||
},
|
||||
"nav": {
|
||||
"home": "首頁",
|
||||
"login": "登入",
|
||||
"profile": "個人中心",
|
||||
"logout": "登出",
|
||||
"backToMailbox": "返回郵箱"
|
||||
},
|
||||
"github": "取得網站原始碼"
|
||||
}
|
||||
168
app/i18n/messages/zh-TW/emails.json
Normal file
168
app/i18n/messages/zh-TW/emails.json
Normal file
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"noPermission": {
|
||||
"title": "權限不足",
|
||||
"description": "你沒有權限訪問此頁面,請聯絡網站管理員",
|
||||
"adminContact": "管理員聯絡方式",
|
||||
"backToHome": "返回首頁"
|
||||
},
|
||||
"layout": {
|
||||
"myEmails": "我的郵箱",
|
||||
"selectEmail": "選擇郵箱查看訊息",
|
||||
"messageContent": "郵件內容",
|
||||
"selectMessage": "選擇郵件查看詳情",
|
||||
"backToEmailList": "← 返回郵箱列表",
|
||||
"backToMessageList": "← 返回訊息列表"
|
||||
},
|
||||
"list": {
|
||||
"emailCount": "{count}/{max} 個郵箱",
|
||||
"emailCountUnlimited": "{count}/∞ 個郵箱",
|
||||
"loading": "載入中...",
|
||||
"loadingMore": "載入更多...",
|
||||
"noEmails": "還沒有郵箱,建立一個吧!",
|
||||
"expiresAt": "過期時間",
|
||||
"permanent": "永久有效",
|
||||
"deleteConfirm": "確認刪除",
|
||||
"deleteDescription": "確定要刪除郵箱 {email} 嗎?此操作將同時刪除該郵箱中的所有郵件,且不可恢復。",
|
||||
"deleteSuccess": "郵箱已刪除",
|
||||
"deleteFailed": "刪除郵箱失敗",
|
||||
"error": "錯誤",
|
||||
"success": "成功"
|
||||
},
|
||||
"create": {
|
||||
"title": "建立郵箱",
|
||||
"name": "郵箱前綴",
|
||||
"namePlaceholder": "留空則隨機生成",
|
||||
"domain": "網域",
|
||||
"domainPlaceholder": "選擇網域",
|
||||
"expiryTime": "有效期",
|
||||
"oneHour": "1 小時",
|
||||
"oneDay": "1 天",
|
||||
"threeDays": "3 天",
|
||||
"permanent": "永久",
|
||||
"create": "建立",
|
||||
"creating": "建立中...",
|
||||
"success": "郵箱建立成功",
|
||||
"failed": "建立郵箱失敗"
|
||||
},
|
||||
"messages": {
|
||||
"received": "收件匣",
|
||||
"sent": "已發送",
|
||||
"noMessages": "暫無郵件",
|
||||
"messageCount": "封郵件",
|
||||
"from": "寄件者",
|
||||
"to": "收件者",
|
||||
"subject": "主旨",
|
||||
"date": "日期",
|
||||
"loading": "載入中...",
|
||||
"loadingMore": "載入更多..."
|
||||
},
|
||||
"send": {
|
||||
"title": "發送郵件",
|
||||
"from": "寄件者",
|
||||
"to": "收件者",
|
||||
"toPlaceholder": "收件者郵箱地址",
|
||||
"subject": "主旨",
|
||||
"subjectPlaceholder": "郵件主旨",
|
||||
"content": "內容",
|
||||
"contentPlaceholder": "郵件內容(支援 HTML)",
|
||||
"send": "發送",
|
||||
"sending": "發送中...",
|
||||
"success": "郵件發送成功",
|
||||
"failed": "發送郵件失敗",
|
||||
"dailyLimitReached": "已達每日發送上限",
|
||||
"dailyLimit": "每日限額:{count}/{max}",
|
||||
"dailyLimitUnit": "封/天"
|
||||
},
|
||||
"messageView": {
|
||||
"loading": "載入郵件詳情...",
|
||||
"loadError": "取得郵件詳情失敗",
|
||||
"networkError": "網路錯誤,請稍後再試",
|
||||
"retry": "點擊重試",
|
||||
"from": "寄件者",
|
||||
"to": "收件者",
|
||||
"time": "時間",
|
||||
"htmlFormat": "HTML 格式",
|
||||
"textFormat": "純文字格式"
|
||||
},
|
||||
"share": {
|
||||
"title": "分享郵箱",
|
||||
"description": "建立分享連結,讓其他人可以查看此郵箱中的郵件",
|
||||
"createLink": "建立連結",
|
||||
"creating": "建立中...",
|
||||
"loading": "載入中...",
|
||||
"expiryTime": "連結有效期",
|
||||
"oneHour": "1 小時",
|
||||
"oneDay": "1 天",
|
||||
"threeDays": "3 天",
|
||||
"oneWeek": "1 週",
|
||||
"permanent": "永久",
|
||||
"activeLinks": "目前分享連結",
|
||||
"noLinks": "暫無分享連結",
|
||||
"createdAt": "建立時間",
|
||||
"expiresAt": "過期時間",
|
||||
"expired": "已過期",
|
||||
"copy": "複製連結",
|
||||
"copied": "已複製",
|
||||
"copyFailed": "複製失敗",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": "確認刪除分享連結?",
|
||||
"deleteDescription": "此操作無法撤銷,分享連結將立即失效。",
|
||||
"cancel": "取消",
|
||||
"deleteSuccess": "刪除成功",
|
||||
"deleteFailed": "刪除失敗",
|
||||
"createSuccess": "分享連結建立成功",
|
||||
"createFailed": "建立分享連結失敗",
|
||||
"shareButton": "分享"
|
||||
},
|
||||
"shareMessage": {
|
||||
"title": "分享郵件",
|
||||
"description": "建立分享連結,讓其他人可以查看這封郵件",
|
||||
"createLink": "建立連結",
|
||||
"creating": "建立中...",
|
||||
"loading": "載入中...",
|
||||
"expiryTime": "連結有效期",
|
||||
"oneHour": "1 小時",
|
||||
"oneDay": "1 天",
|
||||
"threeDays": "3 天",
|
||||
"oneWeek": "1 週",
|
||||
"permanent": "永久",
|
||||
"activeLinks": "目前分享連結",
|
||||
"noLinks": "暫無分享連結",
|
||||
"createdAt": "建立時間",
|
||||
"expiresAt": "過期時間",
|
||||
"expired": "已過期",
|
||||
"copy": "複製連結",
|
||||
"copied": "已複製",
|
||||
"copyFailed": "複製失敗",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": "確認刪除分享連結?",
|
||||
"deleteDescription": "此操作無法撤銷,分享連結將立即失效。",
|
||||
"cancel": "取消",
|
||||
"deleteSuccess": "刪除成功",
|
||||
"deleteFailed": "刪除失敗",
|
||||
"createSuccess": "分享連結建立成功",
|
||||
"createFailed": "建立分享連結失敗",
|
||||
"shareButton": "分享郵件"
|
||||
},
|
||||
"shared": {
|
||||
"loading": "載入中...",
|
||||
"emailNotFound": "無法訪問郵箱",
|
||||
"messageNotFound": "無法訪問郵件",
|
||||
"linkExpired": "分享連結不存在或已過期",
|
||||
"linkInvalid": "連結無效",
|
||||
"linkInvalidDescription": "此分享連結可能已過期或不存在",
|
||||
"sharedMailbox": "分享郵箱",
|
||||
"sharedMessage": "分享郵件",
|
||||
"expiresAt": "郵箱過期時間",
|
||||
"permanent": "永久有效",
|
||||
"createOwnEmail": "建立自己的臨時郵箱",
|
||||
"brand": {
|
||||
"title": "MoeMail",
|
||||
"subtitle": "萌萌噠臨時郵箱服務",
|
||||
"cta": "立即體驗",
|
||||
"officialSite": "官網",
|
||||
"copyright": "© 2024 MoeMail. 萌萌噠臨時郵箱服務"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
app/i18n/messages/zh-TW/home.json
Normal file
27
app/i18n/messages/zh-TW/home.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"title": "MoeMail",
|
||||
"subtitle": "萌萌噠臨時郵箱服務",
|
||||
"features": {
|
||||
"privacy": {
|
||||
"title": "隱私保護",
|
||||
"description": "保護您的真實郵箱地址"
|
||||
},
|
||||
"instant": {
|
||||
"title": "郵箱分享",
|
||||
"description": "將郵箱分享給其他人使用"
|
||||
},
|
||||
"expiry": {
|
||||
"title": "自動過期",
|
||||
"description": "到期自動失效"
|
||||
},
|
||||
"openapi": {
|
||||
"title": "開放 API",
|
||||
"description": "提供完整的 OpenAPI 介面"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"enterMailbox": "進入郵箱",
|
||||
"getStarted": "開始使用"
|
||||
}
|
||||
}
|
||||
|
||||
6
app/i18n/messages/zh-TW/metadata.json
Normal file
6
app/i18n/messages/zh-TW/metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "MoeMail - 萌萌噠臨時郵箱服務 · 開放 API",
|
||||
"description": "安全、快速、一次性的臨時郵箱地址,保護您的隱私,遠離垃圾郵件。支援郵箱分享、即時收件,到期自動失效。提供完整的 OpenAPI 介面,方便開發者整合與自動化測試。",
|
||||
"keywords": "臨時郵箱, 一次性郵箱, 匿名郵箱, 郵箱分享, 隱私保護, 垃圾郵件過濾, 即時收件, 自動過期, 安全郵箱, 註冊驗證, 臨時帳號, 無需手機號, 開發測試, 自動化測試, 郵件API, OpenAPI, API介面, RESTful API, API Key, 開發者工具, MoeMail"
|
||||
}
|
||||
|
||||
131
app/i18n/messages/zh-TW/profile.json
Normal file
131
app/i18n/messages/zh-TW/profile.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"title": "個人中心",
|
||||
"card": {
|
||||
"title": "使用者資訊",
|
||||
"name": "使用者名稱",
|
||||
"role": "角色",
|
||||
"roles": {
|
||||
"EMPEROR": "皇帝",
|
||||
"DUKE": "公爵",
|
||||
"KNIGHT": "騎士",
|
||||
"CIVILIAN": "平民"
|
||||
}
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"description": "建立與管理用於存取 OpenAPI 的 API 金鑰",
|
||||
"create": "建立 API Key",
|
||||
"name": "金鑰名稱",
|
||||
"namePlaceholder": "輸入金鑰名稱",
|
||||
"key": "API Key",
|
||||
"createdAt": "建立時間",
|
||||
"copy": "複製",
|
||||
"delete": "刪除",
|
||||
"noKeys": "暫無 API Key",
|
||||
"createSuccess": "API Key 建立成功",
|
||||
"createFailed": "建立 API Key 失敗",
|
||||
"deleteConfirm": "確認刪除",
|
||||
"deleteDescription": "確定要刪除 API Key {name} 嗎?此操作不可恢復。",
|
||||
"deleteSuccess": "API Key 已刪除",
|
||||
"deleteFailed": "刪除 API Key 失敗",
|
||||
"viewDocs": "查看使用文件",
|
||||
"docs": {
|
||||
"getConfig": "取得系統設定",
|
||||
"generateEmail": "產生臨時郵箱",
|
||||
"getEmails": "取得郵箱清單",
|
||||
"getMessages": "取得郵件清單",
|
||||
"getMessage": "取得單封郵件",
|
||||
"createEmailShare": "建立郵箱分享連結",
|
||||
"getEmailShares": "取得郵箱分享連結清單",
|
||||
"deleteEmailShare": "刪除郵箱分享連結",
|
||||
"createMessageShare": "建立郵件分享連結",
|
||||
"getMessageShares": "取得郵件分享連結清單",
|
||||
"deleteMessageShare": "刪除郵件分享連結",
|
||||
"notes": "注意:",
|
||||
"note1": "請將 YOUR_API_KEY 替換為你的實際 API Key",
|
||||
"note2": "/api/config 介面可取得系統設定,包括可用的郵箱網域清單",
|
||||
"note3": "emailId 是郵箱的唯一識別碼",
|
||||
"note4": "messageId 是郵件的唯一識別碼",
|
||||
"note5": "expiryTime 是郵箱的有效期(毫秒),可選值:3600000(1 小時)、86400000(1 天)、604800000(7 天)、0(永久)",
|
||||
"note6": "domain 是郵箱網域,可透過 /api/config 介面取得可用網域清單",
|
||||
"note7": "cursor 用於分頁,從上一次請求的回應中取得 nextCursor",
|
||||
"note8": "所有請求都需要包含 X-API-Key 請求標頭",
|
||||
"note9": "expiresIn 是分享連結的有效期(毫秒),0 表示永久有效",
|
||||
"note10": "shareId 是分享紀錄的唯一識別碼"
|
||||
}
|
||||
},
|
||||
"emailService": {
|
||||
"title": "Resend 發件服務設定",
|
||||
"configRoleLabel": "可設定的角色權限",
|
||||
"enable": "啟用郵件服務",
|
||||
"fixedRoleLimits": "固定權限規則",
|
||||
"emperorLimit": "皇帝可以無限發件,不受任何限制",
|
||||
"civilianLimit": "永遠不能發件",
|
||||
"enableDescription": "開啟後將使用 Resend 發送郵件",
|
||||
"apiKey": "Resend API Key",
|
||||
"apiKeyPlaceholder": "輸入 Resend API Key",
|
||||
"dailyLimit": "每日限額",
|
||||
"roleLimits": "允許使用發件功能的角色",
|
||||
"save": "儲存設定",
|
||||
"saving": "儲存中...",
|
||||
"saveSuccess": "設定儲存成功",
|
||||
"saveFailed": "儲存設定失敗",
|
||||
"unlimited": "無限制",
|
||||
"disabled": "未啟用發件權限",
|
||||
"enabled": "已啟用發件權限"
|
||||
},
|
||||
"webhook": {
|
||||
"title": "Webhook 設定",
|
||||
"description": "當收到新郵件時通知指定的 URL",
|
||||
"description2": "我們會向此 URL 發送 POST 請求,包含新郵件的相關資訊",
|
||||
"description3": "查看資料格式說明",
|
||||
"enable": "啟用 Webhook",
|
||||
"url": "Webhook URL",
|
||||
"urlPlaceholder": "輸入 webhook URL",
|
||||
"test": "測試",
|
||||
"testing": "測試中...",
|
||||
"save": "儲存設定",
|
||||
"saving": "儲存中...",
|
||||
"saveSuccess": "設定儲存成功",
|
||||
"saveFailed": "儲存設定失敗",
|
||||
"testSuccess": "Webhook 測試成功",
|
||||
"testFailed": "Webhook 測試失敗",
|
||||
"docs": {
|
||||
"intro": "當收到新郵件時,我們會向設定的 URL 發送 POST 請求,請求標頭包含:",
|
||||
"exampleBody": "請求體範例:",
|
||||
"subject": "郵件主旨",
|
||||
"content": "郵件文字內容",
|
||||
"html": "郵件 HTML 內容"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"title": "網站設定",
|
||||
"description": "設定網站選項(僅皇帝可用)",
|
||||
"defaultRole": "新使用者預設角色",
|
||||
"emailDomains": "郵箱網域",
|
||||
"emailDomainsPlaceholder": "多個網域以逗號分隔",
|
||||
"adminContact": "管理員聯絡方式",
|
||||
"adminContactPlaceholder": "郵箱或其他聯絡方式",
|
||||
"maxEmails": "每個使用者最大郵箱數",
|
||||
"save": "儲存設定",
|
||||
"saving": "儲存中...",
|
||||
"saveSuccess": "設定儲存成功",
|
||||
"saveFailed": "儲存設定失敗"
|
||||
},
|
||||
"promote": {
|
||||
"title": "角色管理",
|
||||
"description": "管理使用者角色(僅皇帝可用)",
|
||||
"search": "搜尋使用者",
|
||||
"searchPlaceholder": "輸入使用者名稱或郵箱",
|
||||
"username": "使用者名稱",
|
||||
"email": "郵箱",
|
||||
"role": "角色",
|
||||
"actions": "操作",
|
||||
"promote": "設為",
|
||||
"noUsers": "未找到使用者",
|
||||
"loading": "載入中...",
|
||||
"updateSuccess": "使用者角色更新成功",
|
||||
"updateFailed": "更新使用者角色失敗"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { auth } from "@/lib/auth"
|
||||
import { NextResponse } from "next/server"
|
||||
import { i18n } from "@/i18n/config"
|
||||
import { i18n, type Locale } from "@/i18n/config"
|
||||
import { PERMISSIONS } from "@/lib/permissions"
|
||||
import { checkPermission } from "@/lib/auth"
|
||||
import { Permission } from "@/lib/permissions"
|
||||
@@ -63,7 +63,9 @@ export async function middleware(request: Request) {
|
||||
const hasLocalePrefix = i18n.locales.includes(maybeLocale as any)
|
||||
if (!hasLocalePrefix) {
|
||||
const cookieLocale = request.headers.get('Cookie')?.match(/NEXT_LOCALE=([^;]+)/)?.[1]
|
||||
const targetLocale = (cookieLocale && i18n.locales.includes(cookieLocale as any)) ? cookieLocale : i18n.defaultLocale
|
||||
const acceptLanguage = request.headers.get('Accept-Language')
|
||||
const preferredLocale = resolvePreferredLocale(cookieLocale, acceptLanguage)
|
||||
const targetLocale = preferredLocale ?? i18n.defaultLocale
|
||||
const redirectURL = new URL(`/${targetLocale}${pathname}${url.search}`, request.url)
|
||||
return NextResponse.redirect(redirectURL)
|
||||
}
|
||||
@@ -71,6 +73,61 @@ export async function middleware(request: Request) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
function resolvePreferredLocale(cookieLocale: string | undefined, acceptLanguageHeader: string | null): Locale | null {
|
||||
if (cookieLocale && i18n.locales.includes(cookieLocale as Locale)) {
|
||||
return cookieLocale as Locale
|
||||
}
|
||||
|
||||
if (!acceptLanguageHeader) return null
|
||||
|
||||
const candidates = parseAcceptLanguage(acceptLanguageHeader)
|
||||
for (const lang of candidates) {
|
||||
const match = matchLocale(lang)
|
||||
if (match) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseAcceptLanguage(header: string): string[] {
|
||||
return header
|
||||
.split(',')
|
||||
.map((part) => {
|
||||
const [lang, ...params] = part.trim().split(';')
|
||||
const qualityParam = params.find((param) => param.trim().startsWith('q='))
|
||||
const quality = qualityParam ? parseFloat(qualityParam.split('=')[1]) : 1
|
||||
return { lang: lang.toLowerCase(), quality: isNaN(quality) ? 1 : quality }
|
||||
})
|
||||
.sort((a, b) => b.quality - a.quality)
|
||||
.map((entry) => entry.lang)
|
||||
}
|
||||
|
||||
function matchLocale(lang: string): Locale | null {
|
||||
const exactMatch = i18n.locales.find((locale) => locale.toLowerCase() === lang)
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
const base = lang.split('-')[0]
|
||||
|
||||
// Handle Chinese variants with explicit regions or scripts
|
||||
if (base === 'zh') {
|
||||
if (lang.includes('tw') || lang.includes('hk') || lang.includes('mo') || lang.includes('hant')) {
|
||||
return 'zh-TW'
|
||||
}
|
||||
if (lang.includes('cn') || lang.includes('sg') || lang.includes('hans')) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
// default Chinese fallback
|
||||
return 'zh-CN'
|
||||
}
|
||||
|
||||
const baseMatch = i18n.locales.find((locale) => locale.toLowerCase().split('-')[0] === base)
|
||||
if (baseMatch) return baseMatch
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next|.*\\..*).*)', // all pages excluding static assets
|
||||
@@ -80,4 +137,4 @@ export const config = {
|
||||
'/api/config/:path*',
|
||||
'/api/api-keys/:path*',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user