.agents/design/common/client-i18n-csr-migration.md
FastGPT 主应用当前使用 Next.js Pages Router。pages/_app.tsx 通过
appWithTranslation(AppRouter, clientI18nConfig) 提供稳定的 i18n 上下文,各页面再通过
getServerSideProps -> serviceSideProps -> serverSideTranslations 注入页面需要的翻译资源。
现状包括:
/chat/share 需要保留 SSR,用于首屏页面内容、分享应用名称、简介、头像和 Head 信息。getServerSideProps。getServerSideProps。绝大多数只返回
serviceSideProps,但 /chat、/dataset/detail、/login/fastlogin、
/config/tool/marketplace 等页面还返回查询参数或服务端业务数据,不能机械删除。serviceSideProps 还从 NEXT_DEVICE_SIZE Cookie 读取 deviceSize,仅用于给
SystemStoreContextProvider 的 Chakra useMediaQuery 提供 SSR fallback;迁移前置改动已删除该链路,
useSystem().isPc 统一由浏览器媒体查询计算;默认 SSR fallback 为 PC。packages/web/i18n/{language}/{namespace}.json,共 3 种语言、
25 个 namespace,原始 JSON 总量约 680 KB;一次性把全部语言和 namespace 注入首屏
会放大 HTML、JS 和内存开销。本方案采用“小步试点、验证后扩面”:先以 /account/apikey 验证客户端 i18n 基础能力,随后扩展到
账户页面和 /price。当前代码已经完成这一批迁移,剩余页面仍按风险分批推进。
/chat/share 继续 SSR,行为、SEO Head、分享语言 Cookie 和首屏内容保持不变。getServerSideProps,最终不再生成页面级服务端 HTML。language + namespace 不重复解析和注册。localStorage 缓存。common namespace 加载完成前不渲染应用树;业务 namespace 异步加载期间允许暂时展示翻译 key,
避免客户端路由切换进入全页 loading。/chat/share 和其他页面。projects/marketplace;它共享翻译源目录,但保持现有 SSR 读取方式。localStorage。/chat/share 仍依赖服务端。pro/admin 页面;客户端 i18n 基础能力放入 packages/web 并保持参数化,为 admin
后续迁移复用。appWithTranslation 仍是应用级 Provider,但必须向它传入稳定配置,使没有
_nextI18Next pageProps 的 CSR 页面也可以创建 i18n 实例。
当前只写 appWithTranslation(App) 时,实例初始化依赖页面通过
serverSideTranslations 返回的 _nextI18Next。试点页面删除
serviceSideProps 后不能继续依赖该隐式前提。
目标形态:
export default appWithTranslation(App, clientI18nConfig);
clientI18nConfig 只能包含浏览器可序列化的公共配置,不能把服务端绝对 localePath 或 Node.js 模块
打进客户端。公共语言列表、defaultLocale、defaultNS 和 fallback 配置应抽成单一来源,
next-i18next.config.js 在服务端补充文件系统 localePath;测试校验两边核心配置一致。
客户端稳定配置显式使用 localePath: null,避免 next-i18next 在浏览器或 _app 服务端分支自动创建
错误的文件/HTTP backend。serverSideTranslations 不使用这份 override 读取文件,仍通过
next-i18next.config.js 的绝对 localePath 生成 SSR pageProps;appWithTranslation 再把这些 pageProps
中的资源合并进全局实例。
配置需要满足:
defaultNS: 'common'fallbackLng: 'en',与当前默认行为一致localePath: nullreact.useSuspense: false 作为全局默认值;已迁移组件也使用非 Suspense 加载partialBundledLanguages: true,允许 SSR 注入资源与客户端 backend 增量加载并存use 注册浏览器安全的 dynamic-import backendlocalePathserverSideTranslations 的资源合并到同一个实例普通 CSR 页面首次进入时,不能沿用当前 setUserDefaultLng 的“发现 NEXT_LOCALE Cookie 就直接
返回”逻辑。该短路成立的前提是服务端已经按 Cookie 初始化语言;CSR 页面没有这个前提。门禁需要
显式解析 Cookie -> localStorage -> navigator.language,加载目标语言资源并调用
i18n.changeLanguage 后才允许渲染页面。
混合迁移期间 appWithTranslation 在没有 _nextI18Next.initialLocale 时会使用 Next Router 的默认
locale(当前通常为 en)。client-only 路由在进入 appWithTranslation 前只读取语言 Cookie,并把结果
注入 _nextI18Next.initialLocale。没有 Cookie 时允许先使用默认语言,挂载后再由 effect 从 localStorage、
内存或 navigator.language 恢复。client-only boundary 仍负责在资源未就绪时阻止业务页面渲染。稳定
config 的另一个作用是避免 SSR/CSR 路由切换时 Provider 在“存在/不存在”之间切换并导致整棵应用卸载重建。
language + namespace 动态导入客户端使用显式 loader map:
const localeResourceLoaders = {
'zh-CN': {
common: () => import('@fastgpt/web/i18n/zh-CN/common.json'),
account: () => import('@fastgpt/web/i18n/zh-CN/account.json')
},
// 其他语言和 namespace 由同一个生成脚本维护
};
选择动态 import 而不是运行时读取文件系统,原因如下:
localePath 指向的 monorepo 文件系统目录。basePath,无需额外维护 /fastai/locales、/gchat/locales 等路径。projects/marketplace 可以继续通过原有文件系统 localePath SSR,不需要复制或移动翻译源。不使用任意字符串模板直接 import。loader map 必须由受版本控制的生成脚本产生,确保 bundler
可以静态分析所有资源,也确保新增 namespace 或语言时不会漏项。生成结果需要接受
Prettier 格式化,并由测试检查与 I18N_NAMESPACES、支持语言和磁盘文件一致。
生成的 loader map 位于 packages/web,通过一个很薄的自定义 i18next backend 接入:
const dynamicImportBackend = {
type: 'backend' as const,
read(language: SupportLanguage, namespace: I18nNsType[number], callback: ReadCallback) {
loadLocaleResource(language, namespace).then(
(resource) => callback(null, resource),
(error) => callback(error, false)
);
}
};
backend 本身不负责业务路由判断,只把 i18next 发出的 language + namespace 请求转给生成的 loader。
资源注册、已加载判断和 backend 状态由 i18next 管理。必须设置
partialBundledLanguages: true,否则实例中只要存在 SSR 注入的 resources,i18next 就可能把尚未加载的
namespace 误判为 ready。
选择 backend 而不是仅在页面门禁里调用 addResourceBundle,是因为后者无法让
useTranslation(namespace) 自动触发资源加载;注册 backend 后,组件声明 namespace 就能成为实际
加载入口。该 backend 是 packages/web 内的小型适配器,不引入新的 HTTP backend 依赖。翻译 JSON
本身已经由 packages/web 管理,因此 loader、backend、缓存和失败状态也由同一个包维护,
projects/app 与 pro/admin 复用时不会互相依赖项目目录。
页面 ready 条件覆盖“当前语言 + i18next 配置要求的 fallback 语言”。当前默认 fallback 为英文: 英文页面只加载英文资源,中文页面加载对应中文资源和仍缺失的英文 fallback 资源。这样在保持按组件 加载的同时,不会因为中文词条偶尔缺失而丢失现有英文回退行为。
如果验证发现当前构建器无法稳定拆出 JSON chunk,再回退到以下备选方案:构建前复制资源到
projects/app/public/locales,并以版本化 URL 通过 HTTP backend 加载。试点期间不同时实现两套
资源加载机制。
useTranslation 显式声明 namespacenamespace 的正确性来源改为实际使用翻译的组件,而不是中央路由配置。迁移组件时,将无参调用改为 显式声明:
const { t } = useTranslation(['apikey'] as const, {
useSuspense: false
});
这样共享组件被其他页面复用、弹窗改为懒加载或组件新增翻译依赖时,依赖仍与组件一起维护,不需要同步
猜测并修改所有上层路由清单。类型参数继续受 I18nNsType 约束,写错 namespace 在 TypeScript
阶段失败。
规则:
t、Trans 所使用的全部 namespace;不能依赖祖先组件
“碰巧已经加载”。common 由 CSR 初始化门禁先加载,保证 NextHead、Layout 和尚未迁移的公共组件不会展示 key;
已迁移组件统一使用 useClientTranslation(namespace),hook 内置 common 并关闭 Suspense。仅使用
common 的组件调用 useClientTranslation()。
Layout 根部声明 price,而 serviceSideProps 统一预加载 price,因此 SSR 页面无需再逐页声明该 namespace。serviceSideProps(context, namespaces) 数组只能作为迁移扫描起点,必须检查页面实际组件树中的
useTranslation、Trans 和带 namespace 前缀的 key。fetch 或 import 翻译文件,所有声明统一经过 i18next backend。页面根组件的首屏预加载声明为:
useClientTranslation('apikey');
API key 专属文案统一收敛到 apikey,AccountContainer、ApiKeyTable、TagMultiSelect 和
TagManageModal 等试点可达组件分别显式声明自己的直接依赖。
CSR 路由集合与翻译依赖解耦,只负责渲染模式。当前 app 的集合为:
const clientOnlyRoutes = new Set([
'/account/apikey', '/account/inform', '/account/setting', '/account/thirdParty',
'/account/customDomain', '/account/bill', '/account/team', '/account/info',
'/account/usage', '/account/model', '/price'
] as const);
页面只有在组件 namespace 改造和验收完成后才加入该集合,但集合本身不再复制 namespace 数据。
缓存键是 language + namespace,不能只按 namespace 缓存。例如 zh-CN/app 与 en/app
是两份独立资源。
两层缓存职责:
| 层级 | 机制 | 作用域 | 失效方式 |
|---|---|---|---|
| 运行时资源缓存 | i18next resource store | 当前标签页会话 | 页面刷新或应用卸载 |
| 静态资源缓存 | hashed JS/JSON chunk + HTTP cache | 浏览器跨刷新复用 | 新构建产生新 hash |
底层 loadLocaleResource 在模块级维护进行中请求:
const pendingLoads = new Map<string, Promise<void>>();
加载过程为:
useTranslation 把组件声明的 namespace 交给 i18next backend connector。language + namespace 任务,并跳过 resource store
中已有的资源。pendingLoads 已存在相同 key 时复用同一个 dynamic import Promise。pendingLoads 删除;成功资源由 i18next store 缓存,失败项由刷新后的新实例重新加载。同一 i18n 实例内,backend connector 本身也会合并相同资源的并发请求;pendingLoads 再保护底层
dynamic import,防止初始化或实例切换边界重复执行。加载失败写入按
language + namespace 记录的资源错误状态,并触发 failedLoading。顶层
ClientI18nBoundary 将这类错误转换成独立错误态,不能仅依赖 react-i18next 的 Suspense Promise:
该 Promise 在 backend 回调结束时会 resolve,即使底层加载失败,单独使用它可能继续渲染翻译 key。
错误态要求用户确认刷新页面,利用整页重新初始化 i18n 和静态 chunk;当前产品不在错误态内继续重试或
渲染不完整翻译。
不额外把翻译正文存入 localStorage,原因是:
localStorage 需要自行处理版本、原子写入、容量、解析异常和多标签同步。localStorage 和 i18next 内存,收益不足以抵消复杂度。localStorage/Cookie 仅继续存语言偏好,不存翻译正文。
已迁移组件使用 useClientTranslation('业务 namespace')。该共享 hook 内部组合
['common', namespace] 并关闭 Suspense,调用方不重复声明 common,同时保留带 namespace 前缀的
翻译 key 类型检查。
ClientI18nGate 下沉到 packages/web,只负责通用的客户端语言初始化:
<ClientI18nGate
defaultLanguage="en"
storageKey={LANG_KEY}
fallback={<PageLoading />}
>
<ClientI18nBoundary>{children}</ClientI18nBoundary>
</ClientI18nGate>
共享组件不能读取 Next Router、clientOnlyRoutes、FASTGPT_SHARE_LOCALE 或 app/admin 的业务 store。
它只通过 props 接收默认语言、语言偏好 key、loading/error UI,并使用当前
I18nextProvider 中的实例。common 是内置的基础 namespace,不作为数组 prop 传入,避免调用方
重复声明以及不稳定数组引用导致初始化 effect 重复执行。app 默认语言传 en,admin 后续接入时传其配置的 zh-CN;两者都可以
复用 NEXT_LOCALE,但 admin 现存且未接入 i18next 的 NEXT_LOCALE_LANG 需要在 admin 迁移时单独
决定兼容或删除,不能固化进共享 Gate。
客户端路由进入后按以下顺序执行:
flowchart TD
A["解析 router.pathname"] --> B["确定普通页面或 share"]
B -->|"/chat/share"| C["使用 SSR 注入资源并直接渲染"]
B -->|"CSR 页面"| D["解析 Cookie、本地偏好和浏览器语言"]
D --> E["加载目标语言 common 并切换语言"]
E --> F["挂载应用树"]
F --> G["useTranslation 声明组件 namespace"]
G --> H["先渲染组件,业务 key 可暂时显示"]
G --> I["backend 加载缺失资源"]
I --> J["资源就绪后自动更新翻译"]
I -->|"失败"| K["展示错误态并确认刷新"]
初始化门禁先加载当前语言和 fallback 所需的 common,再挂载整个普通页面树,包括 _app 中的
NextHead、Layout 和页面组件,保证应用壳不会先显示 common:* key。组件通过显式
useTranslation 按需加载业务 namespace,但不触发 Suspense;资源未就绪时先展示 key,加载完成后
由 i18next 自动更新组件。
直接访问 client-only 页面时,只在 common 未就绪期间使用稳定的全页 loading。路由切换时:
common 已存在时不显示额外 loading。language + namespace,而不是用单个全局 ready 布尔值。language、namespace 错误上下文。普通页面语言切换流程调整为:
useTranslation 声明。i18n.changeLanguage(targetLanguage);也可以直接使用
changeLanguage 的 backend 加载阶段,但必须接管错误结果。NEXT_LOCALE。i18next 的 namespace 集合会包含当前会话访问过的组件,因此切换语言可能顺带加载少量“已访问但当前 未挂载”的 namespace;它仍不会加载从未使用的全部 25 个 namespace,并换取语言切换时页面不会因 子组件逐个发现资源而出现混合语言。若试点指标表明该增量明显,再增加活动 namespace 引用计数, 不在首版维护第二份路由清单。
如果加载失败,保留原语言,不写入新的语言偏好,避免页面进入“语言已切换但资源不完整”的状态; 全局错误态要求用户确认刷新,刷新后重新读取 Cookie、本地存储和浏览器语言。
SSR 页面只按语言 Cookie 选择首屏语言;没有 Cookie 时使用 Next 默认语言,并允许客户端 effect 挂载后
再从 localStorage、内存或 navigator.language 恢复,因此首次无 Cookie 的语言闪烁属于可接受行为。
客户端成功写入语言时还会写入 localStorage 镜像,普通 CSR 页面可在 Cookie 到期后恢复原语言并重新
续写 Cookie。API 请求继续由客户端发送 x-fastgpt-language,服务端不直接访问浏览器存储。
分享页使用独立的 FASTGPT_SHARE_LOCALE。分享页的 Axios、SSE 和 Skill 流式请求从该 key 读取
Cookie/localStorage/内存,并发送独立的 x-fastgpt-share-language 请求头;服务端让该请求头优先于主站
NEXT_LOCALE,避免分享页语言被主站语言覆盖。普通页面仍只发送 x-fastgpt-language,两条语言链路互不污染。
服务端 API 只有在请求带有分享语言头时才读取 FASTGPT_SHARE_LOCALE Cookie(该 Cookie 的 Path 为 /,
普通页面也可能携带它);未标记的普通请求会忽略分享 Cookie。分享页 SSR 则由 serviceSideProps 显式选择
分享 Cookie,再回退主站 Cookie。
首次初始化也遵循相同顺序:即使已经存在 NEXT_LOCALE Cookie,也必须为 CSR 页面显式加载该语言
并执行 changeLanguage。只有没有 Cookie 时才使用旧版 localStorage 偏好或浏览器语言,并在加载
成功后写回标准化后的 NEXT_LOCALE。
/chat/share 本阶段保持现有 FASTGPT_SHARE_LOCALE、SSR 资源注入与 reload 行为,不与普通页面的
本轮迁移同时修改。
删除 getServerSideProps 只会使 Pages Router 页面变成自动静态优化,并不等于禁用页面 HTML
预渲染。为了验证“除分享页外只在客户端渲染”,应用入口需要设置明确的 client-only boundary:
/chat/share 走原有同步 SSR 渲染路径。{ ssr: false } 的客户端应用壳。因此过渡期不能用简单的“pathname 不是 /chat/share 就 CSR”开关,否则会一次性改变所有页面。
已迁移路由由独立的 clientOnlyRoutes 判定:
const isClientOnlyRoute = (pathname: string) => clientOnlyRoutes.has(pathname);
当前 _app 的结构是:全局 Provider 和 AppShell 始终挂载,已迁移路由只把页面内容交给
ssr: false 的 ClientOnlyPage;未迁移页面和 /chat/share 继续由同一个 AppShell 渲染。
const ClientOnlyPage = dynamic(() => import('@/web/context/ClientOnlyPage'), {
ssr: false
});
function AppRouter(props: AppPropsWithLayout) {
const isClientOnlyRoute = clientOnlyRoutes.has(props.router.pathname);
return <AppShell {...props} clientOnly={isClientOnlyRoute}
renderPage={isClientOnlyRoute ? () => <ClientOnlyPage {...props} /> : undefined} />;
}
export default appWithTranslation(AppRouter, clientI18nConfig);
AppShell 内部只在 clientOnly 路由包裹 ClientI18nGate、ClientI18nBoundary 和
SystemStoreContextProvider.waitForReady,基础语言、common 和设备信息 ready 后才渲染页面内容。
因此已迁移路由不输出页面业务 HTML;Layout 仍是常驻应用壳,且其可能打开的充值弹窗由全局 price
namespace 预加载覆盖。
当全部非分享页面迁移完成后,再切换为“除 /chat/share 外默认 client-only”,并删除过渡兼容分支。
deviceSize 处理在 i18n 试点前先删除 deviceSize 整条链路:
serviceSideProps 不再读取或返回 NEXT_DEVICE_SIZE。_app 不再向 SystemStoreContextProvider 传 pageProps.deviceSize。SystemStoreContextProvider 不再写设备 Cookie/localStorage,只保留
useMediaQuery('(min-width: 900px)'),初始 fallback 使用 PC。waitForReady 等待浏览器首次媒体查询结果,在结果确认前只显示全屏 loading,
不挂载依赖 isPc 的页面树。仍保留 SSR 的 /chat/share、admin 和 marketplace 会使用 PC fallback 生成服务端 HTML,客户端 effect
后切换到真实宽度;这不会造成 hydration mismatch,但移动端可能出现一次布局调整。普通页面进入
client-only boundary 后会等待真实宽度确认,不会把这次调整暴露给用户。设备类型不写入 localStorage:
缓存值可能来自旧窗口或旧设备,只能作为提示,不能作为当前布局依据。该取舍消除了有状态设备 Cookie、
过期尺寸和 i18n helper 混入设备职责的问题。
/account/apikey选择原因:
getServerSideProps 只调用 serviceSideProps,没有服务端业务查询或权限决策。Auth、桌面/移动布局、账户侧栏和多个弹窗,能够真实验证应用壳加载。common、account、apikey,足以验证多个 namespace
的加载和缓存。getServerSideProps,影响范围小。useTranslation(namespace)。_app 增加 i18n ready 门禁和针对已迁移路由的 client-only boundary,保持 AppShell 常驻。/price 的 serviceSideProps import 与 getServerSideProps。/chat/share 代码和 SSR 输出不变。price 作为 serviceSideProps 的全局预加载 namespace,因为常驻 Layout 中的充值弹窗可能在任意页面打开。当前实现的职责边界如下:
| 位置 | 职责 |
|---|---|
packages/web/i18n/clientConfig.ts | 创建可由 app/admin 覆盖 defaultLocale 的稳定客户端公共配置 |
packages/web/i18n/resourceLoaders.generated.ts | 由脚本生成的 language + namespace -> dynamic import 映射 |
packages/web/i18n/dynamicImportBackend.ts | 把 i18next backend read 接到生成的 loader map |
packages/web/i18n/resourceLoaders.ts | dynamic import、pending Promise 去重和资源错误状态 |
packages/web/i18n/ClientI18nGate.tsx | 参数化解析语言,加载初始 namespace 并完成首次语言切换 |
packages/web/i18n/ClientI18nBoundary.tsx | 捕获 namespace 加载错误并提供刷新提示 |
projects/app/src/web/context/AppShell.tsx | 从 _app 抽出的现有应用布局与初始化逻辑,SSR/CSR 共用 |
projects/app/src/web/context/ClientOnlyPage.tsx | 无 SSR 动态入口,给共享 Gate 注入 app 参数后挂载页面 |
scripts/generate-i18n-resource-loaders.mjs | 扫描共享包支持语言和 namespace,生成 loader map |
scripts/check-i18n-resource-loaders.mjs | 校验生成结果与翻译资源一致 |
projects/app/src/pages/_app.tsx | Provider 配置、SSR/CSR 分流及整个应用壳的翻译门禁 |
packages/web/i18n/utils.ts | 语言偏好、语言映射和 namespace 预加载工具 |
projects/app/src/pages/account/*.tsx、projects/app/src/pages/price.tsx | 删除已迁移页面的 getServerSideProps |
packages/web/test/i18n/*.test.ts、packages/service/test/common/middle/i18n.test.ts | loader、缓存、语言映射和请求语言解析测试 |
生成脚本需要接入 app/admin 的开发和构建前置步骤,且 CI 中增加“生成结果没有 diff”的检查,避免开发者
新增 namespace 后只在本地生成但未提交。resourceLoaders.generated.ts 只承载机械映射,不写业务逻辑。
共享 i18n 模块不得使用 @/ 别名或读取 app/admin 路由。各项目负责用自己的 _app、动态
client-only boundary 和默认语言配置组合共享能力。admin 本轮不跟随试点迁移,只要求共享 API 的设计
能支持其后续接入,并通过一个最小 Gate 单元测试覆盖 defaultLanguage="zh-CN" 的参数化行为。
功能验收:
common:*、业务 namespace 原始 key。/account/apikey 和从试点页返回其他页面均正常。/chat/share 的 SSR HTML、Head 和独立语言偏好无回归。NEXT_PUBLIC_BASE_URL=/fastai。渲染验收:
/account/apikey 返回的服务端 HTML 中不包含 API key 页面业务文案或表格结构。/chat/share?shareId=... 返回的服务端 HTML 仍包含分享页对应 Head 信息。getServerSideProps 页面清单中。缓存验收:
dataset、chat、skill 等无关资源。/account/apikey,相同 namespace 不产生新的资源请求或动态 import 执行。质量门槛:
试点上线后至少观察一个完整发布周期,且覆盖三种语言、桌面/移动端和至少一个非根路径部署。 关注:
/account/apikey 首次可交互时间与现有 SSR 基线的差异。/chat/share SSR 请求和页面行为是否保持原有水平。本轮实现已完成代码迁移;直接访问、弹窗、三种语言和 basePath 等浏览器验收仍属于发布后的观察项。
先迁移 /account/apikey 验证架构假设、构建产物、缓存和回滚链路,随后在同一实现上扩展账户页面和价格页。
已完成的账户/价格批次包括:
/account/apikey、/account/bill、/account/inform、/account/setting、
/account/customDomain、/account/thirdParty。/account/info、/account/team、/account/model、/account/usage、/price。仍待迁移的纯 i18n 页面包括:
/dashboard/agent、/dashboard/tool、
/dashboard/templateMarket、/dashboard/systemTool、/dashboard/mcpServer、
/dashboard/evaluation、/dashboard/evaluation/create、/dashboard/create、
/dashboard/skill。/dataset/list、/config/tool、/app/detail、
/skill/detail、/login、/login/provider。每批都需要:把可达组件改为显式 useTranslation(namespace)、删除对应 getServerSideProps、验证
冷/热缓存和语言切换,不能只依赖原 serviceSideProps 数组。
单独设计和迁移以下页面:
/dataset/detail:把 datasetId、currentTab 改为从 router.query 读取并处理
router.isReady。/login/fastlogin:把 code、token、callbackUrl、lastTmbId 改为客户端 query,确保
鉴权 effect 只执行一次且不会在 query 未就绪时误请求。/config/tool/marketplace:把 MARKETPLACE_URL 改为安全的客户端配置来源,禁止暴露其他服务端配置。/chat:把 query、Cookie 判断和 MongoDB MongoOutLink 查询迁移为有鉴权边界的客户端 API;这是
高风险项,必须单独设计。_error、404、根路由和 API 文档页。API 文档页当前用空 getServerSideProps 禁止静态生成,
移除前必须确认 Scalar 组件在 client-only boundary 下正常。/chat/share 保留 SSR。clientOnlyRoutes 白名单,改为除 /chat/share 外默认 CSR。serviceSideProps 的职责和引用范围。getServerSideProps,确认除明确豁免页面外没有残留。I18N_NAMESPACES。useTranslation 声明的新 namespace 发起加载,已有 resource bundle 不重复加载。/account/apikey,断言 loading 后再出现已翻译内容。/chat/share 不经过 CSR i18n 门禁,仍使用 SSR 注入资源。局部开发阶段运行试点相关测试、typecheck 和 lint。试点完成后运行应用 production build,检查:
全量测试只在本轮实现完成后运行一次。
当前批次回滚按以下顺序:
serviceSideProps 与 getServerSideProps。_app.tsx 的 clientOnlyRoutes 移除对应页面,使其不再进入 client-only 分支。/chat/share 始终不参与试点回滚,因为试点不修改其 SSR 方案。禁止在出现翻译加载问题时静默回退为展示 key;应明确回滚该页面或修复 loader。
| 风险 | 防护 |
|---|---|
删除 serviceSideProps 后 Provider 不创建 | 给 appWithTranslation 传入稳定客户端配置并测试无 _nextI18Next 页面 |
| 有语言 Cookie 时仍停留在默认语言 | CSR 门禁不复用 SSR 的 Cookie 短路,始终显式加载并切换目标语言 |
| SSR/CSR 路由切换时短暂使用 Router 默认语言 | 有 Cookie 时在 Provider 初始化前注入;无 Cookie 时接受一次闪烁并由 effect 恢复 |
| client-only 路由切换因业务 namespace 出现全屏 loading | common 就绪后业务 namespace 使用非 Suspense 加载,允许短暂显示翻译 key |
| 组件漏声明 namespace | 类型约束、静态扫描、missingKey/failedLoading 监控和完整页面操作验收 |
| 同时请求导致重复加载 | pendingLoads 按 language + namespace 复用 Promise,并补并发测试 |
| 语言切换时出现混合语言 | 先加载目标语言全部资源,成功后再 changeLanguage |
| 动态 import 被打入公共首包 | production build 检查 chunks 和首屏资源清单 |
| basePath 下资源 404 | 使用 Next 管理的 hashed chunk,并验证 /fastai 部署 |
| namespace 加载失败 | ClientI18nErrorFallback 统一提示“加载失败,请刷新”,确认后执行全局刷新,避免继续渲染不完整翻译 |
| 试点页面仍被静态预渲染 | 使用显式 client-only boundary,并检查返回 HTML |
| SSR 分享页回归 | /chat/share 独立路径和回归测试,保持 serverSideTranslations 注入 |
| 翻译发布后旧缓存不失效 | 使用内容 hash 的构建产物 URL,不使用固定 URL + immutable |
deviceSize pageProps、NEXT_DEVICE_SIZE Cookie 和 Provider SSR fallback 链路/account/apikey 作为首个试点页面language + namespace 加载与缓存模型/chat/share 保持 SSR_nextI18Next pageProps 的页面也有 Providerpackages/web 实现参数化 ClientI18nGate,不依赖 app/admin 路由和业务状态clientOnlyRoutes,只承担已迁移页面的 CSR 分流common 初始化门禁;业务 namespace 使用非 Suspense 加载common 门禁与业务 namespace 后台加载/price(代码已完成,浏览器验收待发布)useTranslation(namespace, { useSuspense: false })common、apikey,账户容器声明 common、accountserviceSideProps 和 getServerSideProps/account/apikey、/account/bill、/account/inform、/account/setting、/account/thirdParty、/account/customDomain、/account/team、/account/info、/account/model、/account/usage、/price 加入 client-only 路由集合price namespace,覆盖任意页面打开充值弹窗的场景zh-Hant-TW 等脚本/地区格式统一归一化为 zh-HantFASTGPT_SHARE_LOCALE 读取语言并发送独立请求头,不读取主站语言覆盖分享语言/account/informcommon、account_informserviceSideProps 和 getServerSideProps/account/inform 加入 client-only 路由集合/account/setting 显式声明 common、account_setting/account/thirdParty 及其弹窗显式声明 common、account_thirdPartyserviceSideProps 和 getServerSidePropscommon、account_custom_domainserviceSideProps 和 getServerSidePropsssr: false/account/customDomain 加入 client-only 路由集合 页面、列表、详情与图表显式声明 common、account_usage
删除页面的 serviceSideProps 和 getServerSideProps
将 /account/usage 加入 client-only 路由集合
充值弹窗仅在打开时延迟加载 user namespace,加载期间不阻塞页面
补齐繁体 account_usage 缺失的翻译 key
验证列表筛选、导出、详情、Dashboard 与充值弹窗
验证已迁移页面服务端 HTML 不含业务页面内容
验证 /chat/share SSR、Head 与语言隔离无回归
运行 Web 测试、typecheck、生成资源校验和 diff check;Service 定向测试在本地受 MongoMemoryServer sandbox 限制,CI 已通过
验证 root 和 /fastai basePath 构建与运行
运行 production build 和全量本地测试
test-web、覆盖率上传及生成资源校验/chat/share 保留业务页面 SSR;任何新增豁免必须单独评审/account/promotion 页面、前端请求和翻译资源inviterId、promotionRate 请求及用户字段