- 查看详细错误信息
+ {rt('view_error_details_9e7e4e37')}
{error.stack}
@@ -115,7 +123,10 @@ export function ErrorInline({
onRetry,
className,
}: Omit) {
- const errorMessage = message || error?.message || '发生未知错误';
+ const rt = useTranslations('runtime');
+
+ const errorMessage =
+ message || error?.message || rt('unknown_error_e83a8ca8');
return (
- 重试
+ {rt('retry_b8784c8d')}
)}
diff --git a/frontend/components/layout/loading.tsx b/frontend/components/layout/loading.tsx
index 4347d6dd..cb671edb 100644
--- a/frontend/components/layout/loading.tsx
+++ b/frontend/components/layout/loading.tsx
@@ -1,3 +1,4 @@
+import { useTranslations } from 'next-intl';
import * as React from 'react';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
@@ -87,12 +88,16 @@ interface LoadingStateProps extends React.ComponentProps<'div'> {
* 用于统一显示加载中的状态
*/
export function LoadingState({
- title = '加载中',
- description = '正在获取活动数据...',
+ title = undefined,
+ description = undefined,
icon: Icon = FileTextIcon,
className,
iconSize = 'md',
}: LoadingStateProps) {
+ const rt = useTranslations('runtime');
+ title ??= rt('loading_d04fcbda');
+ description ??= rt('fetching_data_9ac5e428');
+
const iconSizes = { sm: 'size-8', md: 'size-10', lg: 'size-14' };
const iconInnerSizes = { sm: 'size-4', md: 'size-5', lg: 'size-7' };
diff --git a/frontend/components/providers/intl-provider.tsx b/frontend/components/providers/intl-provider.tsx
index 0796f23b..de368ebc 100644
--- a/frontend/components/providers/intl-provider.tsx
+++ b/frontend/components/providers/intl-provider.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useLayoutEffect, useState } from 'react';
import { NextIntlClientProvider } from 'next-intl';
import type { AbstractIntlMessages } from 'next-intl';
import type { ReactNode } from 'react';
@@ -25,20 +25,17 @@ export function AppIntlProvider({
messages: ssrMessages,
children,
}: Props) {
- // Static export (NEXT_STANDALONE_EXPORT) pre-renders every page at build time
- // with the default locale, so the SSR locale is stale at runtime. Re-resolve
- // from the NEXT_LOCALE cookie / browser language on the client. The first
- // render stays identical to SSR to avoid a hydration mismatch, then flips —
- // the brief flash matches the theme-hydration pattern (suppressHydrationWarning
- // is already set on /).
+ // Static exports start in the default English locale. Keep hydration aligned
+ // with that HTML, then apply an explicit preference before the hydrated UI
+ // paints instead of switching languages in a passive effect after paint.
const [clientLocale, setClientLocale] = useState(null);
- useEffect(() => {
+ useLayoutEffect(() => {
const resolved = resolveClientLocale();
if (resolved !== ssrLocale) {
setClientLocale(resolved);
- document.documentElement.lang = resolved;
}
+ document.documentElement.lang = resolved;
}, [ssrLocale]);
const locale = clientLocale ?? ssrLocale;
diff --git a/frontend/components/providers/title-updater.tsx b/frontend/components/providers/title-updater.tsx
index 63ff76e2..013649cb 100644
--- a/frontend/components/providers/title-updater.tsx
+++ b/frontend/components/providers/title-updater.tsx
@@ -4,8 +4,10 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { ConfigService } from '@/lib/services/config';
import { usePathname } from 'next/navigation';
+import { useTranslations } from 'next-intl';
export function SiteTitleUpdater() {
+ const t = useTranslations('providers.titleUpdater');
const pathname = usePathname();
const publicConfigQuery = useQuery({
queryKey: ['public-config'],
@@ -18,17 +20,17 @@ export function SiteTitleUpdater() {
// Determine the page suffix based on path
let suffix = '';
if (pathname === '/login') {
- suffix = ' - 登录';
+ suffix = ` - ${t('login')}`;
} else if (pathname === '/register') {
- suffix = ' - 注册';
+ suffix = ` - ${t('register')}`;
} else if (pathname.startsWith('/admin')) {
- suffix = ' - 后台管理';
+ suffix = ` - ${t('admin')}`;
} else if (pathname === '/') {
- suffix = ' - 总览';
+ suffix = ` - ${t('dashboard')}`;
}
document.title = `${siteName}${suffix}`;
- }, [publicConfigQuery.data?.site_name, pathname]);
+ }, [publicConfigQuery.data?.site_name, pathname, t]);
return null;
}
diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx
index fbc5b549..20846acf 100644
--- a/frontend/components/ui/calendar.tsx
+++ b/frontend/components/ui/calendar.tsx
@@ -1,6 +1,8 @@
'use client';
import * as React from 'react';
+import { useLocale } from 'next-intl';
+import { enUS, zhCN } from 'date-fns/locale';
import {
ChevronDownIcon,
ChevronLeftIcon,
@@ -19,14 +21,18 @@ function Calendar({
buttonVariant = 'ghost',
formatters,
components,
+ locale: localeOverride,
...props
}: React.ComponentProps & {
buttonVariant?: React.ComponentProps['variant'];
}) {
+ const uiLocale = useLocale();
+ const locale = localeOverride ?? (uiLocale === 'zh-CN' ? zhCN : enUS);
const defaultClassNames = getDefaultClassNames();
return (
- date.toLocaleString('default', { month: 'short' }),
+ date.toLocaleString(locale.code ?? uiLocale, { month: 'short' }),
...formatters,
}}
classNames={{
diff --git a/frontend/components/ui/tags-input.tsx b/frontend/components/ui/tags-input.tsx
index 7c3304ca..fd576d0c 100644
--- a/frontend/components/ui/tags-input.tsx
+++ b/frontend/components/ui/tags-input.tsx
@@ -1,5 +1,6 @@
'use client';
+import { useTranslations } from 'next-intl';
import * as React from 'react';
import { X } from 'lucide-react';
@@ -25,13 +26,16 @@ function normalizeTag(raw: string) {
function TagsInput({
value,
onChange,
- placeholder = '输入后按 Enter 添加',
+ placeholder = undefined,
disabled = false,
className,
id,
validateTag,
'aria-invalid': ariaInvalid,
}: TagsInputProps) {
+ const rt = useTranslations('runtime');
+ placeholder ??= rt('type_and_press_enter_to_add_e30b7b5f');
+
const [draft, setDraft] = React.useState('');
const inputRef = React.useRef(null);
@@ -128,7 +132,7 @@ function TagsInput({