mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
fix(frontend): 修复静态导出下切换语言无效的问题
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user