# 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/agent': ['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/agent (应用列表)

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/agent');

    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/agent');
    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
**预计开始日期**: [填写]
**预计完成日期**: [填写]
**实际完成日期**: [填写]
