mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-14 02:46:37 +08:00
fix(frontend): 修复静态导出下切换语言无效的问题
This commit is contained in:
@@ -16,6 +16,7 @@ sidebar: false
|
|||||||
### 🛠 修复
|
### 🛠 修复
|
||||||
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
|
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
|
||||||
- 修复 OpenTelemetry Resource 绑定 semconv schema 版本导致 SDK 升级后可能无法启动的问题。
|
- 修复 OpenTelemetry Resource 绑定 semconv schema 版本导致 SDK 升级后可能无法启动的问题。
|
||||||
|
- 修复静态导出(build:embed)部署下切换语言无效的问题:此前页面在构建时固定为默认中文,运行时不再读取 `NEXT_LOCALE`;现在客户端会按 cookie/浏览器语言重新解析并切换界面语言与 `html lang`。
|
||||||
|
|
||||||
### 💄 其他/体验
|
### 💄 其他/体验
|
||||||
- 前端使用 `next/font` 自托管 Inter,并忽略浏览器扩展改写 `body` 属性引起的 hydration 警告。
|
- 前端使用 `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';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { NextIntlClientProvider } from 'next-intl';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
import type { AbstractIntlMessages } from 'next-intl';
|
import type { AbstractIntlMessages } from 'next-intl';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { resolveClientLocale } from '@/i18n/client';
|
||||||
import type { AppLocale } from '@/i18n/config';
|
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 = {
|
type Props = {
|
||||||
locale: AppLocale;
|
locale: AppLocale;
|
||||||
@@ -11,11 +20,33 @@ type Props = {
|
|||||||
children: ReactNode;
|
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 (
|
return (
|
||||||
<NextIntlClientProvider
|
<NextIntlClientProvider
|
||||||
locale={locale}
|
locale={locale}
|
||||||
messages={messages}
|
messages={clientLocale ? messagesByLocale[clientLocale] : ssrMessages}
|
||||||
timeZone='Asia/Shanghai'
|
timeZone='Asia/Shanghai'
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user