feat: 增加服务器组件国际化

This commit is contained in:
liuyonghe0111 2025-12-23 15:20:36 +08:00
parent f2df1504df
commit d44aa0af82
8 changed files with 63 additions and 35 deletions

View File

@ -1,4 +1,7 @@
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
// 指定 i18n 配置文件路径,让 next-intl 知道支持的语言
const withNextIntl = createNextIntlPlugin('./src/lib/i18n.ts');
const nextConfig: NextConfig = {
/* config options here */
@ -32,4 +35,4 @@ const nextConfig: NextConfig = {
},
};
export default nextConfig;
export default withNextIntl(nextConfig);

View File

@ -4,10 +4,12 @@ import { Chip } from '@/components/ui/chip';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { IconButton } from '@/components/ui/button';
import Link from 'next/link';
import { getTranslations } from 'next-intl/server';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const character = await fetchCharacter(id);
const t = await getTranslations('character');
return (
<div className="flex h-full flex-col items-center mx-auto max-w-[752px] px-4 pt-4 sm:pt-10">
@ -45,7 +47,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<IconButton variant="ghost" size="small" iconfont="icon-Like-fill" />
9.9k
</div>
<div className="text-txt-tertiary">Liked</div>
<div className="text-txt-tertiary">{t('liked')}</div>
</div>
<div className="w-full flex items-center flex-col">
<div className="text-[rgba(255,64,180,1)]">
@ -57,11 +59,11 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
/>
123456
</div>
<div className="text-txt-tertiary">Hot</div>
<div className="text-txt-tertiary">{t('hot')}</div>
</div>
</div>
<div className="mt-4 rounded-2xl bg-white/10 p-6">
<div className="txt-headline-s">Introduction</div>
<div className="txt-headline-s">{t('introduction')}</div>
<div className="mt-4">{character?.description}</div>
</div>
</div>

View File

@ -22,6 +22,11 @@ export default {
check_in: 'Check-in',
check_in_desc: 'Daily Free crush coinsh',
},
character: {
liked: 'Liked',
hot: 'Hot',
introduction: 'Introduction',
},
chat: {
chats: 'Chats',
drawer: {

View File

@ -22,6 +22,11 @@ export default {
check_in: '签到',
check_in_desc: '每日签到,免费获取金币',
},
character: {
liked: '喜欢',
hot: '热度',
introduction: '人物简介',
},
chat: {
chats: '聊天',
drawer: {

View File

@ -13,13 +13,13 @@ export default function LocaleSwitch() {
const { locale, setLocale } = useLocale();
return (
<Select value={locale} onValueChange={(value) => setLocale(value as 'zh' | 'en')}>
<Select value={locale} onValueChange={(value) => setLocale(value as 'zh-CN' | 'en-US')}>
<SelectTrigger className="rounded-full h-8 w-16 px-3">
<SelectValue />
</SelectTrigger>
<SelectContent className="min-w-0 w-22">
<SelectItem value="zh"></SelectItem>
<SelectItem value="en">En</SelectItem>
<SelectItem value="zh-CN"></SelectItem>
<SelectItem value="en-US">En</SelectItem>
</SelectContent>
</Select>
);

View File

@ -7,7 +7,7 @@ import Cookies from 'js-cookie';
import { useMemoizedFn } from 'ahooks';
import { useLayoutStore } from '@/stores';
type Locale = 'zh' | 'en';
type Locale = 'zh-CN' | 'en-US';
interface LocaleContextType {
locale: Locale;
setLocale: (locale: Locale) => void;
@ -33,15 +33,15 @@ function setLocaleToCookie(locale: Locale) {
function getLocaleFromCookie(): Locale {
const cookieLocale = Cookies.get('locale') as Locale | undefined;
if (cookieLocale && (cookieLocale === 'zh' || cookieLocale === 'en')) {
if (cookieLocale && (cookieLocale === 'zh-CN' || cookieLocale === 'en-US')) {
return cookieLocale;
}
setLocaleToCookie('en');
return 'en';
setLocaleToCookie('en-US');
return 'en-US';
}
export function IntlProvider({ children }: IntlProviderProps) {
const [locale, setLocaleState] = useState<Locale>('en');
const [locale, setLocaleState] = useState<Locale>('en-US');
const [messages, setMessages] = useState<Record<string, any>>();
const router = useRouter();
@ -52,22 +52,23 @@ export function IntlProvider({ children }: IntlProviderProps) {
const loadLocale = useMemoizedFn(async (locale: Locale) => {
// 动态加载, 提升首屏加载速度
const messages = await import(`@/locales/${locale}.ts`);
const messages = await import(`@/i18n/${locale}.ts`);
setMessages(messages.default);
});
useEffect(() => {
const cookieLocale = getLocaleFromCookie();
if (cookieLocale) {
loadLocale(cookieLocale);
setLocaleState(cookieLocale);
}
}, []);
const setLocale = useMemoizedFn((newLocale: Locale) => {
const setLocale = useMemoizedFn(async (newLocale: Locale) => {
setLocaleState(newLocale);
setLocaleToCookie(newLocale);
loadLocale(newLocale);
await loadLocale(newLocale);
router.refresh();
});

20
src/lib/i18n.ts Normal file
View File

@ -0,0 +1,20 @@
import { cookies } from 'next/headers';
import { getRequestConfig } from 'next-intl/server';
// 内存缓存:存储已经加载过的语言包
const locales: Record<string, any> = {};
export default getRequestConfig(async () => {
const store = await cookies();
const cookieLocale = store.get('locale')?.value || 'en-US';
// 如果缓存中没有这个语言,才去加载
if (!locales[cookieLocale]) {
locales[cookieLocale] = (await import(`@/i18n/${cookieLocale}.ts`)).default;
}
return {
locale: cookieLocale,
messages: locales[cookieLocale], // 直接从缓存中读取
};
});

View File

@ -32,10 +32,6 @@ function generateDeviceId(userAgent?: string): string {
export default function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// console.log('🔄 [MIDDLEWARE] 开始处理路径:', pathname)
// console.log('🔄 [MIDDLEWARE] 请求方法:', request.method)
// console.log('🔄 [MIDDLEWARE] User-Agent:', request.headers.get('user-agent')?.substring(0, 50))
// console.log('🔄 [MIDDLEWARE] 请求头:', Object.fromEntries(request.headers.entries()))
// 获取现有设备ID
let deviceId = request.cookies.get(DEVICE_ID_COOKIE_NAME)?.value;
@ -46,9 +42,8 @@ export default function proxy(request: NextRequest) {
const userAgent = request.headers.get('user-agent') || undefined;
deviceId = generateDeviceId(userAgent);
needSetCookie = true;
// console.log('🆕 [MIDDLEWARE] 生成新设备ID:', deviceId)
} else {
console.log('✅ [MIDDLEWARE] 获取现有设备ID:', deviceId);
// console.log('✅ [MIDDLEWARE] 获取现有设备ID:', deviceId);
}
// 认证逻辑
@ -59,14 +54,6 @@ export default function proxy(request: NextRequest) {
(route) => pathname.startsWith(route) && !pathname.startsWith('/login/fields')
);
// console.log('🔑 [MIDDLEWARE] 认证状态:', {
// isAuthenticated,
// pathname,
// isProtectedRoute,
// isAuthRoute,
// token: token ? '存在' : '不存在'
// });
// 如果是受保护的路由但用户未登录,重定向到登录页
if (isProtectedRoute && !isAuthenticated) {
console.log('🚫 [MIDDLEWARE] 重定向到登录页:', pathname);
@ -96,6 +83,17 @@ export default function proxy(request: NextRequest) {
},
});
// 如果locale cookie不存在设置为en-US
if (!request.cookies.get('locale')?.value) {
response.cookies.set('locale', 'en-US', {
maxAge: 365 * 24 * 60 * 60, // 365天
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
}
// 如果需要设置设备ID cookie
if (needSetCookie) {
response.cookies.set(DEVICE_ID_COOKIE_NAME, deviceId, {
@ -107,12 +105,6 @@ export default function proxy(request: NextRequest) {
});
}
if (pathname.startsWith('/@')) {
const userId = pathname.slice(2); // 去掉/@
return NextResponse.rewrite(new URL(`/user/${userId}`, request.url));
}
console.log('✅ [MIDDLEWARE] 成功处理完毕:', pathname);
return response;
}