fix(frontend): 修复静态导出下切换语言无效的问题

This commit is contained in:
ryan
2026-08-16 17:29:14 +08:00
parent 5a8722ff07
commit 5aaaf8f197
3 changed files with 108 additions and 3 deletions
+1
View File
@@ -16,6 +16,7 @@ sidebar: false
### 🛠 修复
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
- 修复 OpenTelemetry Resource 绑定 semconv schema 版本导致 SDK 升级后可能无法启动的问题。
- 修复静态导出(build:embed)部署下切换语言无效的问题:此前页面在构建时固定为默认中文,运行时不再读取 `NEXT_LOCALE`;现在客户端会按 cookie/浏览器语言重新解析并切换界面语言与 `html lang`。
### 💄 其他/体验
- 前端使用 `next/font` 自托管 Inter,并忽略浏览器扩展改写 `body` 属性引起的 hydration 警告。
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import { useTranslations } from 'next-intl';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import enMessages from '@/messages/en.json';
import zhCNMessages from '@/messages/zh-CN.json';
import { AppIntlProvider } from './intl-provider';
function Probe() {
const t = useTranslations('common');
return <button>{t('save')}</button>;
}
const zhSave = (zhCNMessages.common as Record<string, string>).save;
const enSave = (enMessages.common as Record<string, string>).save;
describe('AppIntlProvider (static export locale resolution)', () => {
beforeEach(() => {
// Deterministic browser language (jsdom defaults to en-US).
Object.defineProperty(navigator, 'languages', {
value: ['zh-CN'],
configurable: true,
});
Object.defineProperty(navigator, 'language', {
value: 'zh-CN',
configurable: true,
});
document.documentElement.lang = 'zh-CN'; // as pre-rendered by SSR
});
afterEach(() => {
document.cookie = 'NEXT_LOCALE=; Max-Age=0; Path=/';
});
it('flips to the NEXT_LOCALE cookie locale after mount when SSR locale is stale', async () => {
// Static export pre-renders with the build-time default (zh-CN) even when
// the user previously chose en — the exact reported bug.
document.cookie = 'NEXT_LOCALE=en; Path=/';
render(
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
<Probe />
</AppIntlProvider>,
);
expect(await screen.findByText(enSave)).toBeInTheDocument();
expect(document.documentElement.lang).toBe('en');
});
it('keeps the SSR locale when the cookie matches it', () => {
document.cookie = 'NEXT_LOCALE=zh-CN; Path=/';
render(
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
<Probe />
</AppIntlProvider>,
);
expect(screen.getByText(zhSave)).toBeInTheDocument();
expect(document.documentElement.lang).toBe('zh-CN');
});
it('does not flip without a cookie preference', () => {
render(
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
<Probe />
</AppIntlProvider>,
);
expect(screen.getByText(zhSave)).toBeInTheDocument();
});
});
@@ -1,9 +1,18 @@
'use client';
import { useEffect, useState } from 'react';
import { NextIntlClientProvider } from 'next-intl';
import type { AbstractIntlMessages } from 'next-intl';
import type { ReactNode } from 'react';
import { resolveClientLocale } from '@/i18n/client';
import type { AppLocale } from '@/i18n/config';
import enMessages from '@/messages/en.json';
import zhCNMessages from '@/messages/zh-CN.json';
const messagesByLocale: Record<AppLocale, AbstractIntlMessages> = {
'zh-CN': zhCNMessages,
en: enMessages,
};
type Props = {
locale: AppLocale;
@@ -11,14 +20,36 @@ type Props = {
children: ReactNode;
};
export function AppIntlProvider({ locale, messages, children }: Props) {
export function AppIntlProvider({
locale: ssrLocale,
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 <html>/<body>).
const [clientLocale, setClientLocale] = useState<AppLocale | null>(null);
useEffect(() => {
const resolved = resolveClientLocale();
if (resolved !== ssrLocale) {
setClientLocale(resolved);
document.documentElement.lang = resolved;
}
}, [ssrLocale]);
const locale = clientLocale ?? ssrLocale;
return (
<NextIntlClientProvider
locale={locale}
messages={messages}
messages={clientLocale ? messagesByLocale[clientLocale] : ssrMessages}
timeZone='Asia/Shanghai'
>
{children}
</NextIntlClientProvider>
);
}
}