# 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 = { // 应用相关 '/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 (
{message}
); }; /** * 轻量级 Loading (用于页面内部) */ export const I18nInlineLoading: React.FC = () => { return ( ); }; ``` **估计时间**: 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 && ( )} {setLayout()} ); } return ( <> {showHead && ( )} {scripts?.map((item, i) => )} {shouldUseLayout ? ( {setLayout()} ) : ( setLayout() )} ); } function App(props: AppPropsWithLayout) { return ( }> ); } // ❌ 移除 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 ; // } // 原有代码... } ``` **步骤 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 = { '/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 ( ); } \`\`\` ## 性能特性 - ✅ 路由切换时间减少 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 **预计开始日期**: [填写] **预计完成日期**: [填写] **实际完成日期**: [填写]