FastGPT/.claude/design/i18n优化实施计划.md
Archer 44e9299d5e
Some checks are pending
Document deploy / sync-images (push) Waiting to run
Document deploy / generate-timestamp (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.cn suffix:cn]) (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.io suffix:io]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.cn kube_config:KUBE_CONFIG_CN suffix:cn]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.io kube_config:KUBE_CONFIG_IO suffix:io]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / get-vars (push) Waiting to run
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:amd64 runs-on:ubuntu-24.04]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:arm64 runs-on:ubuntu-24.04-arm]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / release-fastgpt-images (push) Blocked by required conditions
V4.13.2 features (#5792)
* add manual create http toolset (#5743)

* add manual create http toolset

* optimize code

* optimize

* fix

* fix

* rename filename

* feat: integrate ts-rest (#5741)

* feat: integrate ts-rest

* chore: classify core contract and pro contract

* chore: update lockfile

* chore: tweak dir structure

* chore: tweak dir structure

* update tsrest code (#5755)

* doc

* update tsrest code

* fix http toolset (#5753)

* fix http toolset

* fix

* perf: http toolset

* fix: toolresponse result (#5760)

* doc

* fix: toolresponse result

* fix: mongo watch

* remove log

* feat: integrated to minio (#5748)

* feat: migrate to minio

* feat: migrate apps' and dataset's avatar to minio

* feat: migrate more avatars to minio

* fix: lock file

* feat: migrate copyright settings' logo to minio

* feat: integrate minio

* chore: improve code

* chore: rename variables

* refactor: s3 class

* fix: s3 and mongo operations

* chore: add session for avatar source

* fix: init s3 buckets

* fix: bugbot issues

* expired time code

* perf: avatar code

* union type

* export favouriteContract

* empty bucket check

---------

Co-authored-by: archer <545436317@qq.com>

* refactor: zod schema to generate OpenAPI instead (#5771)

* doc

* fix: text split code (#5773)

* fix: toolresponse result

* remove log

* stream remove

* fix: text split code

* fix: workflow (#5779)

* fix: toolresponse result

* remove log

* fix: value check

* fix: workflow

* openapi doc

* perf: bucket delete cron

* doc

* feat: apikey health

* feat: export variables

* api code move

* perf: workflow performance (#5783)

* perf: reactflow context

* perf: workflow context split

* perf: nodeList computed map

* perf: nodes dependen

* perf: workflow performance

* workflow performance

* removel og

* lock

* version

* loop drag

* reactflow size

* reactflow size

* fix: s3init (#5784)

* doc

* fix: s3init

* perf: dynamic import

* remove moongose dep

* worker build

* worker code

* perf: worker build

* fix: error throw

* doc

* doc

* fix: build

* fix: dockerfile

* nextjs config

* fix: worker

* fix: build (#5791)

* fix: build

* vector cache code

* fix: app info modal avatar upload method replace (#5787)

* fix: app info modal avatar upload method replace

* chore: replace all useSelectFile with useUploadAvatar

* remove invalid code

* add size

* Update projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/CommonInputForm.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowInitContext.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 19:08:21 +08:00

1316 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FastGPT i18n 客户端化优化实施计划
## 项目背景
当前 FastGPT 所有页面都使用 `getServerSideProps` + `serviceSideProps` 来加载国际化翻译,导致每次路由切换都需要服务端处理,严重影响性能。由于大部分页面不需要 SEO可以将 i18n 迁移到客户端,消除服务端阻塞。
**预期改善**: 路由切换时间减少 40-50% (从 1560ms 降至 900ms 左右)
---
## 目标
1. ✅ 移除大部分页面的 `getServerSideProps`,改为纯客户端渲染
2. ✅ 实现客户端 i18n 按需加载和预加载策略
3. ✅ 保留需要 SEO 的页面的服务端渲染
4. ✅ 确保翻译功能完整性,无闪烁和加载失败
---
## 技术方案
### 架构对比
```
旧架构 (服务端 i18n):
用户点击路由
→ 服务端 getServerSideProps
→ 加载 i18n 文件 (阻塞)
→ 服务端渲染 HTML
→ 返回客户端水合
= 780ms+
新架构 (客户端 i18n):
用户点击路由
→ 立即显示页面骨架
→ 并行加载: bundle + i18n + 数据
→ React 渲染完成
= 400-500ms
```
### 核心技术栈
- **i18next**: 核心 i18n 库
- **react-i18next**: React 集成
- **i18next-http-backend**: HTTP 后端加载翻译文件
- **i18next-browser-languagedetector**: 浏览器语言检测
---
## 实施步骤
### Phase 1: 基础设施搭建 (第 1 周)
#### 1.1 安装依赖
```bash
cd projects/app
pnpm add i18next-browser-languagedetector i18next-http-backend
```
#### 1.2 创建客户端 i18n 配置
**文件**: `projects/app/src/web/i18n/client.ts`
```typescript
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
// 默认语言
fallbackLng: 'zh',
lng: 'zh',
// 预加载核心命名空间
ns: ['common'],
defaultNS: 'common',
// 后端加载配置
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
// 请求超时
requestOptions: {
cache: 'no-cache',
},
},
// 语言检测配置
detection: {
order: ['cookie', 'localStorage', 'navigator'],
caches: ['cookie', 'localStorage'],
cookieName: 'NEXT_LOCALE',
lookupCookie: 'NEXT_LOCALE',
lookupLocalStorage: 'NEXT_LOCALE',
},
// React 配置
react: {
useSuspense: true,
},
// 开发配置
debug: process.env.NODE_ENV === 'development',
// 性能配置
load: 'currentOnly', // 只加载当前语言
preload: ['zh'], // 预加载中文
interpolation: {
escapeValue: false,
},
});
export default i18n;
```
**估计时间**: 2 小时
#### 1.3 创建页面命名空间映射
**文件**: `projects/app/src/web/i18n/namespaceMap.ts`
```typescript
/**
* 页面路径到 i18n 命名空间的映射
* 用于自动加载页面所需的翻译文件
*/
export const pageNamespaces: Record<string, string[]> = {
// 应用相关
'/app/detail': ['app', 'chat', 'workflow', 'publish', 'file'],
'/dashboard/apps': ['app'],
// 数据集相关
'/dataset/list': ['dataset'],
'/dataset/detail': ['dataset'],
// 账户相关
'/account/setting': ['user'],
'/account/apikey': ['user'],
'/account/bill': ['user'],
'/account/usage': ['user'],
'/account/promotion': ['user'],
'/account/inform': ['user'],
'/account/team': ['user'],
'/account/model': ['user'],
'/account/info': ['user'],
'/account/thirdParty': ['user'],
// 仪表板相关
'/dashboard/evaluation': ['app'],
'/dashboard/templateMarket': ['app'],
'/dashboard/mcpServer': ['app'],
// 其他页面
'/more': ['common'],
'/price': ['common'],
// 需要 SEO 的页面保持服务端渲染
// '/chat/share': 使用 getServerSideProps
// '/login': 使用 getServerSideProps
};
/**
* 判断页面是否需要服务端渲染
*/
export const needsSSR = (pathname: string): boolean => {
const ssrPages = [
'/chat/share', // 分享页面需要 SEO
'/price', // 定价页面需要 SEO (可选)
'/login', // 登录页首屏体验
'/login/provider',
'/login/fastlogin',
];
return ssrPages.some(page => pathname.startsWith(page));
};
/**
* 获取页面需要的命名空间
*/
export const getPageNamespaces = (pathname: string): string[] => {
// 精确匹配
if (pageNamespaces[pathname]) {
return pageNamespaces[pathname];
}
// 模糊匹配 (例如 /dashboard/[pluginGroupId] 匹配 /dashboard/*)
for (const [path, namespaces] of Object.entries(pageNamespaces)) {
if (pathname.startsWith(path)) {
return namespaces;
}
}
// 默认只加载 common
return [];
};
```
**估计时间**: 1 小时
#### 1.4 创建页面级 i18n Hook
**文件**: `projects/app/src/web/i18n/usePageI18n.ts`
```typescript
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
import i18n from './client';
import { getPageNamespaces } from './namespaceMap';
/**
* 页面级 i18n Hook
* 自动加载当前页面需要的命名空间
*/
export function usePageI18n(pathname?: string) {
const router = useRouter();
const { i18n: i18nInstance } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const currentPath = pathname || router.pathname;
const namespaces = getPageNamespaces(currentPath);
if (namespaces.length === 0) {
setIsReady(true);
return;
}
setIsLoading(true);
// 加载所有需要的命名空间
Promise.all(
namespaces.map(ns => {
// 检查是否已加载
if (i18nInstance.hasResourceBundle(i18nInstance.language, ns)) {
return Promise.resolve();
}
return i18nInstance.loadNamespaces(ns);
})
)
.then(() => {
setIsReady(true);
})
.catch(error => {
console.error('Failed to load i18n namespaces:', error);
setIsReady(true); // 即使失败也继续渲染
})
.finally(() => {
setIsLoading(false);
});
}, [pathname, router.pathname, i18nInstance]);
return { isLoading, isReady };
}
```
**估计时间**: 1 小时
#### 1.5 创建预加载 Hook
**文件**: `projects/app/src/web/i18n/useI18nPreload.ts`
```typescript
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import i18n from './client';
import { getPageNamespaces } from './namespaceMap';
/**
* i18n 预加载 Hook
* 在链接悬停时预加载目标页面的翻译
*/
export function useI18nPreload() {
const router = useRouter();
useEffect(() => {
// 预加载当前路由的命名空间
const currentNamespaces = getPageNamespaces(router.pathname);
currentNamespaces.forEach(ns => {
if (!i18n.hasResourceBundle(i18n.language, ns)) {
i18n.loadNamespaces(ns);
}
});
// 监听链接悬停事件
const handleMouseEnter = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest('a[href^="/"]');
if (link) {
const href = link.getAttribute('href');
if (href) {
const namespaces = getPageNamespaces(href);
namespaces.forEach(ns => {
if (!i18n.hasResourceBundle(i18n.language, ns)) {
// 预加载但不阻塞
i18n.loadNamespaces(ns).catch(() => {
// 静默失败
});
}
});
}
}
};
// 使用捕获阶段监听所有链接
document.addEventListener('mouseenter', handleMouseEnter, true);
return () => {
document.removeEventListener('mouseenter', handleMouseEnter, true);
};
}, [router.pathname]);
}
```
**估计时间**: 1 小时
#### 1.6 创建 Loading 组件
**文件**: `projects/app/src/components/i18n/I18nLoading.tsx`
```typescript
import React from 'react';
import { Box, Spinner, Center } from '@chakra-ui/react';
/**
* i18n 加载状态组件
* 在翻译文件加载时显示
*/
export const I18nLoading: React.FC<{ message?: string }> = ({
message = '加载中...'
}) => {
return (
<Center h="100vh" w="100%">
<Box textAlign="center">
<Spinner size="lg" color="primary.500" mb={4} />
<Box color="myGray.600" fontSize="sm">
{message}
</Box>
</Box>
</Center>
);
};
/**
* 轻量级 Loading (用于页面内部)
*/
export const I18nInlineLoading: React.FC = () => {
return (
<Box display="inline-flex" alignItems="center" ml={2}>
<Spinner size="sm" color="primary.500" />
</Box>
);
};
```
**估计时间**: 0.5 小时
#### 1.7 确保翻译文件可访问
检查翻译文件位置,确保客户端可以访问:
```bash
# 检查现有翻译文件位置
ls -la projects/app/public/locales/
# 或
ls -la packages/web/i18n/
```
如果翻译文件不在 `public/locales/`,需要:
**选项 A**: 复制到 public 目录
```bash
# 创建目录
mkdir -p projects/app/public/locales
# 复制翻译文件
cp -r packages/web/i18n/* projects/app/public/locales/
```
**选项 B**: 配置 next.config.js 重写
```javascript
// projects/app/next.config.js
const nextConfig = {
// ... 现有配置
async rewrites() {
return [
{
source: '/locales/:lng/:ns.json',
destination: '/api/locales/:lng/:ns',
},
];
},
};
```
然后创建 API 路由:
**文件**: `projects/app/src/pages/api/locales/[lng]/[ns].ts`
```typescript
import type { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
import fs from 'fs';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { lng, ns } = req.query;
if (typeof lng !== 'string' || typeof ns !== 'string') {
return res.status(400).json({ error: 'Invalid parameters' });
}
// 翻译文件路径
const filePath = path.join(
process.cwd(),
'../../packages/web/i18n',
lng,
`${ns}.json`
);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Translation file not found' });
}
const content = fs.readFileSync(filePath, 'utf-8');
// 设置缓存头
res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600');
res.setHeader('Content-Type', 'application/json');
return res.status(200).send(content);
}
```
**估计时间**: 1 小时
**Phase 1 总计**: 7.5 小时 (约 1 个工作日)
---
### Phase 2: 修改 _app.tsx (第 2 周 - Day 1)
#### 2.1 修改 _app.tsx
**文件**: `projects/app/src/pages/_app.tsx`
```typescript
import '@scalar/api-reference-react/style.css';
import type { AppProps } from 'next/app';
import Script from 'next/script';
import Layout from '@/components/Layout';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/web/i18n/client';
import { useI18nPreload } from '@/web/i18n/useI18nPreload';
import QueryClientContext from '@/web/context/QueryClient';
import ChakraUIContext from '@/web/context/ChakraUI';
import { useInitApp } from '@/web/context/useInitApp';
import '@/web/styles/reset.scss';
import NextHead from '@/components/common/NextHead';
import { type ReactElement, useEffect, Suspense } from 'react';
import { type NextPage } from 'next';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import SystemStoreContextProvider from '@fastgpt/web/context/useSystem';
import { useRouter } from 'next/router';
import { I18nLoading } from '@/components/i18n/I18nLoading';
type NextPageWithLayout = NextPage & {
setLayout?: (page: ReactElement) => JSX.Element;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
const routesWithCustomHead = ['/chat', '/chat/share', '/app/detail/', '/dataset/detail'];
const routesWithoutLayout = ['/openapi'];
function AppContent({ Component, pageProps }: AppPropsWithLayout) {
const { feConfigs, scripts, title } = useInitApp();
// 启用预加载
useI18nPreload();
useEffect(() => {
document.addEventListener(
'wheel',
function (e) {
if (e.ctrlKey && Math.abs(e.deltaY) !== 0) {
e.preventDefault();
}
},
{ passive: false }
);
}, []);
const setLayout = Component.setLayout || ((page) => <>{page}</>);
const router = useRouter();
const showHead = !router?.pathname || !routesWithCustomHead.includes(router.pathname);
const shouldUseLayout = !router?.pathname || !routesWithoutLayout.includes(router.pathname);
if (router.pathname === '/openapi') {
return (
<>
{showHead && (
<NextHead
title={title}
desc={process.env.SYSTEM_DESCRIPTION}
icon={getWebReqUrl(feConfigs?.favicon || process.env.SYSTEM_FAVICON)}
/>
)}
{setLayout(<Component {...pageProps} />)}
</>
);
}
return (
<>
{showHead && (
<NextHead
title={title}
desc={process.env.SYSTEM_DESCRIPTION}
icon={getWebReqUrl(feConfigs?.favicon || process.env.SYSTEM_FAVICON)}
/>
)}
{scripts?.map((item, i) => <Script key={i} strategy="lazyOnload" {...item}></Script>)}
<QueryClientContext>
<SystemStoreContextProvider device={pageProps.deviceSize}>
<ChakraUIContext>
{shouldUseLayout ? (
<Layout>{setLayout(<Component {...pageProps} />)}</Layout>
) : (
setLayout(<Component {...pageProps} />)
)}
</ChakraUIContext>
</SystemStoreContextProvider>
</QueryClientContext>
</>
);
}
function App(props: AppPropsWithLayout) {
return (
<I18nextProvider i18n={i18n}>
<Suspense fallback={<I18nLoading message="正在加载翻译..." />}>
<AppContent {...props} />
</Suspense>
</I18nextProvider>
);
}
// ❌ 移除 appWithTranslation
// export default appWithTranslation(App);
// ✅ 直接导出
export default App;
```
**估计时间**: 1 小时
**Phase 2 总计**: 1 小时
---
### Phase 3: 逐页迁移 (第 2-3 周)
#### 3.1 优先级页面列表
```yaml
P0 - 高频访问页面 (优先迁移):
- /app/detail (应用编辑页)
- /dataset/list (数据集列表)
- /dataset/detail (数据集详情)
- /dashboard/apps (应用列表)
P1 - 中频访问页面:
- /account/setting (账户设置)
- /account/team (团队管理)
- /account/apikey (API 密钥)
- /account/usage (用量统计)
- /dashboard/evaluation (评测)
- /dashboard/templateMarket (模板市场)
P2 - 低频访问页面:
- /account/bill (账单)
- /account/promotion (推广)
- /account/inform (通知)
- /account/model (模型)
- /account/info (个人信息)
- /account/thirdParty (第三方登录)
- /dashboard/mcpServer (MCP 服务器)
- /more (更多)
保持 SSR (需要 SEO):
- /chat/share (分享页面)
- /login (登录页)
- /login/provider (第三方登录)
- /login/fastlogin (快速登录)
- /price (定价页 - 可选)
```
#### 3.2 页面迁移模板
对于每个页面,执行以下步骤:
**步骤 1**: 删除 `getServerSideProps`
```typescript
// ❌ 删除这段代码
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['app', 'chat', 'user']))
}
};
}
```
**步骤 2**: 添加 `usePageI18n` hook
```typescript
import { usePageI18n } from '@/web/i18n/usePageI18n';
function PageComponent() {
// 添加这行
const { isReady } = usePageI18n();
// 可选:显示加载状态
// if (!isReady) {
// return <Loading />;
// }
// 原有代码...
}
```
**步骤 3**: 测试翻译
```bash
# 启动开发服务器
pnpm dev
# 访问页面,检查:
# 1. 翻译是否正常显示
# 2. 控制台是否有错误
# 3. 切换语言是否生效
```
#### 3.3 迁移示例:/app/detail
**文件**: `projects/app/src/pages/app/detail/index.tsx`
**修改前**:
```typescript
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['app', 'chat', 'user', 'file', 'publish', 'workflow']))
}
};
}
```
**修改后**:
```typescript
import { usePageI18n } from '@/web/i18n/usePageI18n';
const AppDetail = () => {
const { setAppId, setSource } = useChatStore();
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const route2Tab = useContextSelector(AppContext, (e) => e.route2Tab);
// ✅ 添加 i18n 加载
usePageI18n('/app/detail');
useEffect(() => {
setSource('test');
if (appDetail._id) {
setAppId(appDetail._id);
if (!appDetail.permission.hasWritePer) {
route2Tab(TabEnum.logs);
}
}
}, [appDetail._id]);
// 其余代码不变...
};
// ❌ 删除 getServerSideProps
```
**估计时间**: 每个页面 15-30 分钟
**Phase 3 总计**:
- P0 页面 (4 个): 2 小时
- P1 页面 (6 个): 3 小时
- P2 页面 (7 个): 3.5 小时
- **总计**: 8.5 小时 (约 1-1.5 个工作日)
---
### Phase 4: 保留 SSR 页面处理 (第 3 周 - Day 1)
对于需要 SEO 的页面,保持使用 `getServerSideProps`,但需要确保它们仍然使用 `appWithTranslation`
#### 4.1 创建混合模式支持
**文件**: `projects/app/src/web/i18n/withSSRI18n.tsx`
```typescript
import { appWithTranslation } from 'next-i18next';
import type { AppProps } from 'next/app';
/**
* 为需要 SSR 的页面提供 i18n 支持
* 这些页面需要保留 getServerSideProps
*/
export const withSSRI18n = (App: any) => {
return appWithTranslation(App);
};
```
#### 4.2 检查 SSR 页面
确保以下页面保留 `getServerSideProps`
**文件**: `projects/app/src/pages/chat/share.tsx`
```typescript
// ✅ 保留 getServerSideProps
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['chat', 'common']))
}
};
}
```
**文件**: `projects/app/src/pages/login/index.tsx`
```typescript
// ✅ 保留 getServerSideProps
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['common']))
}
};
}
```
**估计时间**: 1 小时
**Phase 4 总计**: 1 小时
---
### Phase 5: 测试与优化 (第 3 周 - Day 2-3)
#### 5.1 功能测试清单
```yaml
基础功能:
- [ ] 首次访问页面,翻译正常加载
- [ ] 路由切换,翻译不丢失
- [ ] 语言切换功能正常 (中文/英文/日文)
- [ ] 刷新页面,语言设置保持
性能测试:
- [ ] 路由切换时间测量 (目标 < 500ms)
- [ ] i18n 文件加载时间 (目标 < 100ms)
- [ ] 首屏加载时间 (目标 < 2s)
边缘情况:
- [ ] 网络断开时的降级处理
- [ ] 翻译文件加载失败的提示
- [ ] 不存在的命名空间处理
- [ ] 并发路由切换
浏览器兼容性:
- [ ] Chrome 最新版
- [ ] Firefox 最新版
- [ ] Safari 最新版
- [ ] Edge 最新版
- [ ] 移动端浏览器
SSR 页面:
- [ ] /chat/share 正常渲染和 SEO
- [ ] /login 首屏体验
- [ ] 爬虫可索引内容
```
#### 5.2 性能测试脚本
**文件**: `projects/app/test/i18n-performance.test.ts`
```typescript
import { test, expect } from '@playwright/test';
test.describe('i18n Performance', () => {
test('should load translations within 200ms', async ({ page }) => {
await page.goto('http://localhost:3000/dashboard/apps');
const startTime = Date.now();
// 等待翻译加载完成
await page.waitForFunction(() => {
return window.i18next && window.i18next.isInitialized;
});
const loadTime = Date.now() - startTime;
console.log(`i18n loaded in ${loadTime}ms`);
expect(loadTime).toBeLessThan(200);
});
test('should switch routes without translation flash', async ({ page }) => {
await page.goto('http://localhost:3000/dashboard/apps');
await page.waitForLoadState('networkidle');
// 点击链接切换路由
const startTime = Date.now();
await page.click('a[href="/app/detail?appId=test"]');
// 等待新页面加载
await page.waitForSelector('[data-testid="app-detail"]');
const switchTime = Date.now() - startTime;
console.log(`Route switched in ${switchTime}ms`);
expect(switchTime).toBeLessThan(500);
// 检查翻译是否正常
const hasTranslation = await page.evaluate(() => {
return document.body.textContent?.includes('应用') ||
document.body.textContent?.includes('App');
});
expect(hasTranslation).toBe(true);
});
});
```
#### 5.3 性能优化
**优化 1**: 添加翻译文件缓存
```typescript
// projects/app/src/web/i18n/client.ts
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
// ... 其他配置
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
// ✅ 添加缓存配置
requestOptions: {
cache: 'default', // 使用浏览器缓存
},
},
});
```
**优化 2**: Service Worker 缓存翻译文件
**文件**: `projects/app/public/sw.js`
```javascript
const CACHE_NAME = 'i18n-cache-v1';
const I18N_URLS = [
'/locales/zh/common.json',
'/locales/en/common.json',
'/locales/ja/common.json',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(I18N_URLS);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/locales/')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
}
});
```
**优化 3**: 预加载关键翻译
```typescript
// projects/app/src/web/i18n/client.ts
i18n.init({
// ... 其他配置
// ✅ 预加载关键命名空间
preload: ['zh', 'en'], // 预加载中英文
ns: ['common'], // 预加载 common 命名空间
});
```
**估计时间**:
- 功能测试: 4 小时
- 性能测试: 2 小时
- 优化实施: 2 小时
- **总计**: 8 小时 (1 个工作日)
**Phase 5 总计**: 8 小时
---
### Phase 6: 监控与文档 (第 3 周 - Day 4)
#### 6.1 添加性能监控
**文件**: `projects/app/src/web/i18n/monitoring.ts`
```typescript
import i18n from './client';
/**
* i18n 性能监控
*/
export const setupI18nMonitoring = () => {
// 监听语言切换
i18n.on('languageChanged', (lng) => {
console.log(`[i18n] Language changed to: ${lng}`);
// 发送到分析服务
if (typeof window !== 'undefined' && window.umami) {
window.umami.track('i18n-language-change', { language: lng });
}
});
// 监听命名空间加载
i18n.on('loaded', (loaded) => {
const namespaces = Object.keys(loaded);
console.log(`[i18n] Loaded namespaces:`, namespaces);
});
// 监听加载失败
i18n.on('failedLoading', (lng, ns, msg) => {
console.error(`[i18n] Failed to load ${lng}/${ns}:`, msg);
// 发送错误到监控服务
if (typeof window !== 'undefined' && window.umami) {
window.umami.track('i18n-load-error', {
language: lng,
namespace: ns,
error: msg,
});
}
});
};
// 在 _app.tsx 中调用
// setupI18nMonitoring();
```
#### 6.2 更新文档
**文件**: `projects/app/docs/i18n-migration.md`
```markdown
# i18n 客户端化迁移指南
## 架构变更
从服务端 i18n (`next-i18next` + `getServerSideProps`) 迁移到客户端 i18n (`i18next` + HTTP backend)。
## 开发指南
### 添加新页面
1.`src/web/i18n/namespaceMap.ts` 中添加页面映射:
\`\`\`typescript
export const pageNamespaces: Record<string, string[]> = {
'/your/new/page': ['common', 'your-namespace'],
};
\`\`\`
2. 在页面组件中使用 `usePageI18n`:
\`\`\`typescript
import { usePageI18n } from '@/web/i18n/usePageI18n';
function YourPage() {
usePageI18n('/your/new/page');
// 其余代码...
}
\`\`\`
3. 不需要 `getServerSideProps`
### 添加新翻译
翻译文件位置: `projects/app/public/locales/[lang]/[namespace].json`
### 切换语言
\`\`\`typescript
import { useTranslation } from 'react-i18next';
function LanguageSwitcher() {
const { i18n } = useTranslation();
return (
<button onClick={() => i18n.changeLanguage('en')}>
Switch to English
</button>
);
}
\`\`\`
## 性能特性
- ✅ 路由切换时间减少 40-50%
- ✅ 按需加载翻译文件
- ✅ 自动预加载链接目标的翻译
- ✅ 浏览器缓存支持
## 注意事项
- 需要 SEO 的页面仍使用 `getServerSideProps`
- 首次访问略慢 (多 100-200ms)
- 配合 Service Worker 可离线使用
\`\`\`
#### 6.3 团队培训文档
**文件**: `projects/app/docs/i18n-team-guide.md`
```markdown
# i18n 客户端化 - 团队指南
## 背景
我们将 i18n 从服务端渲染迁移到客户端,以提升路由切换性能。
## 主要变化
### 之前
\`\`\`typescript
// 每个页面都需要
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['app', 'chat']))
}
};
}
\`\`\`
### 之后
\`\`\`typescript
// 大部分页面不需要 getServerSideProps
import { usePageI18n } from '@/web/i18n/usePageI18n';
function MyPage() {
usePageI18n(); // 自动加载所需翻译
// ...
}
\`\`\`
## 常见问题
### Q: 为什么翻译显示为 key?
A: 翻译文件正在加载中,稍等片刻或检查网络请求。
### Q: 如何添加新的翻译命名空间?
A: 在 `namespaceMap.ts` 中添加页面映射。
### Q: 性能提升了多少?
A: 路由切换快了约 40-50%,从 1.5s 降至 0.9s。
### Q: 哪些页面保留了 SSR?
A: `/chat/share`, `/login` 等需要 SEO 的页面。
## 支持
遇到问题请联系前端团队或查看完整文档。
\`\`\`
**估计时间**: 4 小时
**Phase 6 总计**: 4 小时
---
## 总体时间估算
| 阶段 | 任务 | 估计时间 |
|------|------|---------|
| Phase 1 | 基础设施搭建 | 7.5 小时 |
| Phase 2 | 修改 _app.tsx | 1 小时 |
| Phase 3 | 逐页迁移 | 8.5 小时 |
| Phase 4 | SSR 页面处理 | 1 小时 |
| Phase 5 | 测试与优化 | 8 小时 |
| Phase 6 | 监控与文档 | 4 小时 |
| **总计** | | **30 小时** |
**实施周期**: 3 周 (按每周 10 小时计算)
---
## 风险与缓解
### 风险 1: 翻译闪烁 (用户看到 key)
**风险等级**: 🟡 中
**缓解措施**:
- 使用 `Suspense` 显示 loading 状态
- 预加载 `common` 命名空间
- 实施预加载策略
### 风险 2: 翻译加载失败
**风险等级**: 🟡 中
**缓解措施**:
- 添加错误边界处理
- 提供降级显示 (显示 key)
- 监控加载失败率
- 实施重试机制
### 风险 3: SEO 影响
**风险等级**: 🟢 低
**缓解措施**:
- 保留需要 SEO 的页面的 SSR
- 测试爬虫可访问性
- 监控搜索引擎收录
### 风险 4: 首屏变慢
**风险等级**: 🟡 中
**缓解措施**:
- 预加载关键命名空间
- 使用 Service Worker 缓存
- 显示友好的 loading 状态
---
## 成功标准
### 性能指标
```yaml
路由切换时间:
当前: 1560ms (开发环境)
目标: < 900ms (开发环境)
改善: 40-50%
i18n 加载时间:
当前: 150-300ms (服务端阻塞)
目标: < 100ms (客户端异步)
改善: 50-70%
首屏时间:
当前: 无变化
目标: 无退化 (可接受 +100ms)
```
### 功能指标
```yaml
翻译完整性: 100% (所有翻译正常显示)
语言切换: 正常 (中/英/日三语)
浏览器兼容: 主流浏览器全支持
SEO 页面: 无影响
```
### 质量指标
```yaml
翻译闪烁率: < 1%
加载失败率: < 0.1%
用户投诉: 0
开发效率: 提升 (无需 getServerSideProps)
```
---
## 验收标准
### 最终验收清单
- [ ] 所有 P0/P1/P2 页面完成迁移
- [ ] SSR 页面功能正常
- [ ] 路由切换时间达标 (< 900ms)
- [ ] 所有功能测试通过
- [ ] 所有性能测试通过
- [ ] 文档完整并同步到团队
- [ ] 团队培训完成
- [ ] 监控系统运行正常
- [ ] 生产环境灰度发布成功
- [ ] 用户反馈收集和处理
---
## 回滚计划
如果遇到严重问题需要回滚
### 快速回滚步骤
1. **恢复 _app.tsx**
```bash
git checkout origin/main -- projects/app/src/pages/_app.tsx
```
2. **恢复页面的 getServerSideProps**
```bash
# 批量恢复
git checkout origin/main -- projects/app/src/pages/**/*.tsx
```
3. **删除客户端 i18n 代码**
```bash
rm -rf projects/app/src/web/i18n/
```
4. **重启开发服务器**
```bash
pnpm dev
```
### 回滚决策标准
触发回滚的条件
- 翻译显示异常 > 10%
- 加载失败率 > 5%
- 用户投诉 > 5 个/天
- 性能退化 > 20%
- 关键功能不可用
---
## 后续优化
完成基础迁移后的改进方向:
### 短期优化 (1 个月内)
- 实施 Service Worker 缓存
- 优化预加载策略
- 添加更详细的监控
- 收集用户反馈并改进
### 中期优化 (3 个月内)
- 翻译文件 CDN 加速
- 智能预加载 (基于用户行为)
- 翻译文件分包优化
- 离线翻译支持
### 长期优化 (6 个月内)
- 自动翻译更新系统
- A/B 测试不同加载策略
- 多地域翻译优化
- 翻译质量监控系统
---
## 附录
### A. 相关资源
- [i18next 官方文档](https://www.i18next.com/)
- [react-i18next 文档](https://react.i18next.com/)
- [Next.js i18n 最佳实践](https://nextjs.org/docs/advanced-features/i18n-routing)
### B. 团队联系方式
- **负责人**: [填写]
- **前端团队**: [填写]
- **测试团队**: [填写]
- **紧急联系**: [填写]
### C. 变更记录
| 日期 | 版本 | 变更内容 | 作者 |
|------|------|---------|------|
| 2025-10-18 | 1.0 | 初始版本 | Claude |
---
**文档生成时间**: 2025-10-18
**预计开始日期**: [填写]
**预计完成日期**: [填写]
**实际完成日期**: [填写]