Next.js SSR 国际化极致优化最佳方案(P7+ 详细落地版)

适配场景:NestJS+Next.js SSR架构、20+语言支持、翻译团队统一维护文案(API侧提供全量精准数据)、客户端纯服务端水合翻译JSON、本地/测试/生产环境差异化加载、LRU+Redis双层缓存、API故障本地Baseline兜底、开发侧本地文案补全

核心痛点解决:解决20+语言全量本地打包体积爆炸、翻译团队与开发团队协作低效、客户端二次请求影响性能、API故障导致页面空白、缓存命中率低、Baseline与API数据混淆等核心问题

核心优化目标:本地翻译打包体积<200KB、缓存命中率>90%、SSR首屏P99延迟<10ms、翻译更新线上无感知、开发效率提升80%、生产环境高可用(API故障核心功能正常)

核心说明:本文档全程贴合公司实际业务,明确「本地Baseline」与「API翻译数据」的核心区别——Baseline是纯本地静态核心兜底文件(开发维护),不包含任何API数据;API数据是远程全量精准数据(翻译团队维护),二者仅在本地开发(API缺失补全)和API故障(兜底)时产生动态合并,正常流程无任何交集,彻底解决此前的认知混淆。

一、核心设计原则(贴合公司实际,每一条均对应业务痛点)

所有设计均围绕「文案准确性(翻译团队)、开发效率(前端)、性能(用户)、可用性(生产)」四大核心,拒绝纯技术堆砌,每一条原则均明确设计初衷和落地价值。

  1. 环境差异化加载(核心原则)

    • 设计初衷:解决「翻译团队统一维护需准确性」与「前端开发需高效补全」的矛盾

    • 具体规则:本地开发(dev)「API数据覆盖+本地Baseline补全」,测试/生产(test/prod)「统一走API拉取,本地Baseline仅故障兜底」

    • 核心价值:既保证测试/生产文案由翻译团队统一维护,避免本地文案污染线上;又保证本地开发无需等待API同步,提升开发效率

  2. 纯服务端水合(客户端零请求)

    • 设计初衷:解决客户端单独请求翻译导致的二次网络开销、首屏渲染慢、服务端与客户端文案不一致问题

    • 具体规则:客户端不发起任何翻译相关API请求,完全复用服务端拼接好的翻译JSON(注入全局变量),通过统一Hook读取使用

    • 核心价值:减少客户端网络开销,提升首屏渲染速度,保证全链路文案一致性,降低客户端代码复杂度

  3. 按需精准加载(最小化传输体积)

    • 设计初衷:解决20+语言全量加载导致的传输体积大、加载慢,以及单路由无需其他路由文案的冗余问题

    • 具体规则:服务端仅拉取「当前语言+当前路由+common公共层」的翻译数据,拒绝全量加载任何无关语言/路由的文案

    • 核心价值:最小化翻译数据传输体积,提升API拉取速度和缓存命中率,适配20+语言的规模化扩展

  4. LRU+Redis双层缓存(高性能+高可用)

    • 设计初衷:解决API拉取频率高、响应慢,以及集群部署时缓存不一致、单实例缓存失效导致的性能瓶颈问题

    • 具体规则:服务端一级缓存(LRU内存缓存,单实例高性能)+ 二级缓存(Redis分布式缓存,集群一致性),缓存粒度精准到「语言+路由」,配置合理过期时间和主动失效策略

    • 核心价值:缓存命中率>90%,单实例缓存读取耗时<1ms,集群部署缓存一致,翻译更新及时失效,避免脏数据

  5. 本地Baseline备用(兜底+补全,不影响线上)

    • 设计初衷:解决本地开发API未同步文案导致的开发阻塞,以及生产/测试API故障导致的页面空白、功能不可用问题

    • 具体规则:Baseline是纯本地静态文件,仅存核心兜底文案(首屏标题、关键按钮),不包含任何API数据;仅在「本地开发API缺失」和「API故障」时生效,正常流程(测试/生产)完全隔离

    • 核心价值:本地开发无阻塞,生产/测试API故障时核心功能正常,不影响线上文案准确性,兼顾开发效率和生产可用性

  6. 工程化提效(标准化+自动化,减少手动操作)

    • 设计初衷:解决20+语言手动新建文件繁琐、易缺失,开发与翻译团队协作混乱,线上易出现文案缺失、格式错误等问题

    • 具体规则:标准化目录/命名/key规范,自动化脚本生成多语言空文件、校验文件完整性,git hooks拦截错误提交,类型安全保证开发无拼写错误

    • 核心价值:减少80%手动操作,从源头规避线上文案相关问题,规范开发与翻译团队协作流程,提升团队效率

二、核心落地步骤(详细可落地,含完整配置+代码片段+团队协作)

本章节按「基础配置→核心逻辑→工程化→优化→协作」的顺序,拆解8个核心步骤,每个步骤均包含「操作细节+代码示例+注意事项+责任分工」,前端开发、翻译团队、运维均可对照执行。

步骤1:标准化翻译文件目录&命名(一劳永逸,开发+翻译+API侧统一)

核心目标:统一本地Baseline、API侧、翻译团队的目录结构和命名规范,确保三者无缝对接,避免因结构不一致导致的加载/合并失败,支持20+语言无缝扩展。

责任分工:前端开发负责本地Baseline目录搭建和规范定义,翻译团队负责按规范维护API侧文案,运维负责API侧目录部署和CDN配置。

1.1 本地Baseline目录结构(仅存核心兜底文案,体积极小)

# 项目本地目录(仅前端开发维护,不上线核心逻辑,仅作为兜底/开发补全)
locales/
  ├─ baseline/          # 本地Baseline根目录(所有语言整体体积<200KB)
  │  ├─ common/         # 公共通用层:所有页面共享的核心文案(按钮、提示、导航)
  │  │  ├─ en.json      # 英文核心文案(仅核心key,无次要说明)
  │  │  ├─ zh-CN.json   # 中文核心文案(与en.json的key完全对应,数量一致)
  │  │  ├─ ja.json      # 日文核心文案(同上,所有语言key统一)
  │  │  └─ ...(20+语言) # 按ISO 639-1标准码命名(如ko/fr/de/es等)
  │  ├─ home/           # 路由层:首页首屏必显的核心文案
  │  │  ├─ en.json
  │  │  ├─ zh-CN.json
  │  │  └─ ...(20+语言)
  │  ├─ trade/          # 路由层:交易页核心文案(与项目实际路由一一对应)
  │  │  ├─ en.json
  │  │  └─ ...(20+语言)
  │  └─ [route]/        # 其他业务路由:按项目路由结构逐一创建(如order/user/pay等)
  └─ config/            # 国际化相关配置文件(前端开发维护)
     ├─ locale.config.js# 语言+路由配置(新增语言/路由仅改此文件)
     ├─ core-keys.config.js # 核心key配置(定义哪些key需要放入Baseline)
     └─ i18n.types.ts   # 翻译key类型定义(类型安全用)

1.2 API侧目录结构(翻译团队维护,全量精准文案)

与本地Baseline目录结构完全对称,确保服务端拉取、合并逻辑统一,API侧支持按「语言+路由」按需返回数据,部署在CDN/API服务端,开启gzip/brotli压缩。

# API侧/CDN目录(翻译团队维护,前端不直接修改,通过API拉取)
api-domain/locales/  # 或 cdn-domain/locales/
  ├─ common/         # 与本地baseline/common完全对称,存全量公共文案
  │  ├─ en.json      # 英文全量文案(包含核心key+次要说明,覆盖Baseline的key)
  │  ├─ zh-CN.json   # 中文全量文案(翻译团队维护,精准规范)
  │  └─ ...(20+语言)
  ├─ home/           # 与本地baseline/home完全对称,存首页全量文案
  │  ├─ en.json
  │  └─ ...(20+语言)
  ├─ trade/          # 与本地baseline/trade完全对称,存交易页全量文案
  │  ├─ en.json
  │  └─ ...(20+语言)
  └─ [route]/        # 其他业务路由:与本地baseline完全对称

1.3 命名规范(严格遵循,避免混乱)

  1. 语言命名:采用「ISO 639-1标准码」,如中文(zh-CN)、英文(en)、日文(ja)、韩文(ko),禁止自定义命名(如中文不用cn/zh,英文不用english);

  2. 路由命名:与Next.js项目路由完全一致(如pages/home.tsx对应home目录,pages/trade/order.tsx对应trade目录),禁止大小写混用;

  3. 文件命名:每个语言的文案文件命名为「{locale}.json」(如zh-CN.json),禁止添加额外后缀(如zh-CN-baseline.json);

  4. key命名:采用「小写字母+下划线」命名法,跨语言/跨路由统一,格式为「{模块}{功能}{描述}」(如common_submit_btn、home_index_title),禁止使用中文、大写字母、特殊符号,禁止重复命名。

1.4 示例文件内容(明确Baseline与API侧的区别)

本地Baseline示例(仅核心key,简洁兜底)

# locales/baseline/zh-CN/home.json
{
  "home_index_title": "首页",          # 首屏核心标题(兜底必显)
  "home_submit_btn": "提交",          # 关键按钮(核心功能)
  "home_empty_tip": "暂无数据",       # 通用提示(核心体验)
  "home_back_btn": "返回"             # 关键按钮(核心功能)
}

API侧示例(全量key,精准详细,翻译团队维护)

# api-domain/locales/home/zh-CN.json
{
  "home_index_title": "XX平台-首页(官方正版)",  # 覆盖Baseline,更精准
  "home_submit_btn": "立即提交(提交后不可修改)", # 覆盖Baseline,补充说明
  "home_empty_tip": "暂无相关数据,点击右上角刷新重试", # 覆盖Baseline,更详细
  "home_back_btn": "返回上一页",                  # 覆盖Baseline,补充说明
  "home_desc": "本平台提供XX服务,专注于XX领域,为用户提供优质体验", # API独有,次要说明
  "home_tab1": "首页",
  "home_tab2": "产品中心",
  "home_footer_tip": "版权所有 © 2026 XX公司"     # API独有,次要内容
}

1.5 关键配置文件(locale.config.js,一劳永逸)

# locales/config/locale.config.js
// 支持的语言列表(ISO 639-1标准码,新增语言仅添加此数组)
export const SUPPORTED_LOCALES = [
  'en', 'zh-CN', 'ja', 'ko', 'fr', 'de', 'es', 'ru', 'pt', 'it',
  // ... 其余10+语言,按实际需求添加
];

// 默认语言(API故障/未配置语言时兜底)
export const DEFAULT_LOCALE = 'en';

// 项目所有路由列表(与Next.js路由一致,新增路由仅添加此数组)
export const SUPPORTED_ROUTES = [
  'home', 'trade', 'order', 'user', 'pay', 'detail',
  // ... 其余业务路由
];

// API侧翻译数据请求地址(前端开发无需修改,运维配置)
export const API_I18N_BASE_URL = process.env.NEXT_PUBLIC_API_I18N_BASE_URL || 'https://api-domain/locales';

// 本地Baseline根目录路径(前端开发无需修改)
export const BASELINE_DIR = process.env.NODE_ENV === 'development' 
  ? './locales/baseline' 
  : path.resolve(process.cwd(), 'locales/baseline');

步骤2:翻译文件拆分规则(开发+翻译团队统一遵循,避免冲突)

核心目标:明确common公共层、路由层的文案拆分标准,区分Baseline核心key与API全量key,避免冗余、缺失,确保开发与翻译团队协作顺畅。

责任分工:前端开发定义拆分规则和核心key列表,翻译团队按规则维护全量文案,确保API侧文案覆盖Baseline核心key。

2.1 拆分核心原则

  1. 「common公共层」:所有页面都会用到的文案,仅保留核心短文案作为Baseline,API侧补充完整说明,禁止存放路由专属文案;

  2. 「路由层」:每个路由仅存放自身页面的文案,按「首屏必显(核心)+ 次要说明(非核心)」拆分,Baseline仅存首屏必显,API侧存全量;

  3. 「key唯一性」:同一key不能出现在不同路由目录下,common层与路由层key无交集,避免合并时冲突;

  4. 「Baseline子集原则」:Baseline的key必须是API侧key的子集,确保API数据能完全覆盖Baseline,避免合并时出现数据混乱。

2.2 各层级具体拆分标准

2.2.1 common公共层拆分(所有页面共享)

类型包含文案Baseline是否包含API侧是否包含示例
关键按钮提交、取消、确认、返回、刷新是(核心)是(覆盖+补充说明)common_submit_btn: "提交"(Baseline)→ "立即提交(不可修改)"(API)
通用提示网络错误、加载中、暂无数据、操作成功/失败是(核心)是(覆盖+补充说明)common_network_error: "网络错误"(Baseline)→ "网络异常,请检查网络后重试"(API)
导航栏首页、我的、消息、设置等导航标题是(核心)是(覆盖,无需额外说明)common_nav_home: "首页"(Baseline/API一致)
次要提示版权信息、隐私提示、操作说明否(非核心)是(仅API侧维护)common_copyright: "版权所有 © 2026 XX公司"(仅API)

2.2.2 路由层拆分(单个路由专属)

类型包含文案Baseline是否包含API侧是否包含示例(home路由)
首屏标题页面主标题、副标题(首屏必显)是(核心)是(覆盖+补充)home_index_title: "首页"(Baseline)→ "XX平台-首页(官方正版)"(API)
核心操作区页面核心按钮、输入框提示、操作说明(核心功能)是(核心)是(覆盖+补充)home_search_placeholder: "搜索"(Baseline)→ "搜索产品/服务,快速找到所需内容"(API)
次要内容区详情描述、标签、次要提示(非首屏必显)否(非核心)是(仅API侧维护)home_desc: "本平台提供XX服务,专注于XX领域"(仅API)
底部内容页面底部提示、链接文案(非核心)否(非核心)是(仅API侧维护)home_footer_tip: "如有疑问,请联系客服:400-XXXX-XXXX"(仅API)

2.3 体积限制标准(确保Baseline体积极小)

  1. common公共层Baseline:单个语言文件<50KB(仅存核心key,约50-80个key);

  2. 单个路由Baseline:单个语言文件<10KB(仅存首屏必显key,约20-30个key);

  3. 所有语言Baseline整体体积<200KB(20+语言,平均每个语言<10KB);

  4. API侧单文件体积:无严格限制,但建议单个语言+单个路由<50KB,开启gzip后可压缩至20KB以内。

2.4 核心key配置(core-keys.config.js,前端维护)

定义哪些key需要放入Baseline,翻译团队按此列表确保API侧覆盖这些key,脚本按此列表自动拆分Baseline(后续脚本会用到)。

# locales/config/core-keys.config.js
// 核心key配置:按「common/路由」分类,仅放入Baseline需要的核心key
export const CORE_KEYS = {
  // common公共层核心key
  common: [
    'common_submit_btn', 'common_cancel_btn', 'common_confirm_btn',
    'common_back_btn', 'common_refresh_btn', 'common_network_error',
    'common_loading', 'common_empty_tip', 'common_success_tip',
    'common_fail_tip', 'common_nav_home', 'common_nav_user',
    // ... 其他common核心key
  ],
  // home路由核心key
  home: [
    'home_index_title', 'home_submit_btn', 'home_empty_tip',
    'home_back_btn', 'home_search_placeholder',
    // ... 其他home核心key
  ],
  // trade路由核心key
  trade: [
    'trade_index_title', 'trade_pay_btn', 'trade_cancel_btn',
    // ... 其他trade核心key
  ],
  // ... 其他路由的核心key配置
};

步骤3:服务端核心加载逻辑(NestJS+Next.js Server,核心中的核心)

核心目标:实现「缓存优先→API拉取→Baseline兜底/合并→数据注入」的全流程,适配环境差异化加载,确保性能、准确性和可用性,补充完整代码片段,可直接复制落地。

责任分工:前端开发负责Next.js Server Component和NestJS服务封装,运维负责缓存和API服务部署。

核心逻辑拆解:服务端接收请求后,先解析语言和路由,再按「LRU→Redis→API→Baseline」的顺序获取翻译数据,最后拼接数据并注入客户端,全程耗时控制在10ms以内(P99)。

3.1 前置准备:依赖安装

# 安装核心依赖(NestJS/Next.js通用)
npm install lru-cache ioredis jsonminify axios
npm install -D @types/lru-cache @types/ioredis

3.2 工具函数封装(读取Baseline+合并数据+语言解析)

封装通用工具函数,与业务逻辑解耦,便于复用和维护,重点区分Baseline和API数据的合并逻辑,明确API数据优先级高于Baseline。

3.2.1 语言解析工具(src/utils/i18n/localeParser.ts)

// src/utils/i18n/localeParser.ts
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '@/locales/config/locale.config';
import type { NextRequest } from 'next/server';
import type { Request } from '@nestjs/common';

/**
 * 解析用户语言(适配Next.js Request和NestJS Request)
 * 优先级:URL参数(?locale=zh-CN)> Cookie(locale=zh-CN)> 请求头(Accept-Language)> 默认语言(en)
 * @param req Next.js Request / NestJS Request
 * @returns 解析后的语言ISO码(如zh-CN/en)
 */
export const parseLocale = (req: NextRequest | Request): string => {
  // 1. 从URL参数解析
  let locale = '';
  if (req instanceof NextRequest) {
    // Next.js Request
    locale = req.nextUrl.searchParams.get('locale') || '';
  } else {
    // NestJS Request
    const query = req.query as Record<string, string>;
    locale = query.locale || '';
  }
  if (SUPPORTED_LOCALES.includes(locale)) {
    return locale;
  }

  // 2. 从Cookie解析
  const cookieStr = req.headers.cookie || '';
  const localeCookie = cookieStr.split('; ').find(item => item.startsWith('locale='));
  if (localeCookie) {
    const cookieLocale = localeCookie.split('=')[1];
    if (SUPPORTED_LOCALES.includes(cookieLocale)) {
      return cookieLocale;
    }
  }

  // 3. 从请求头Accept-Language解析
  const acceptLanguage = req.headers['accept-language'] || '';
  if (typeof acceptLanguage === 'string') {
    const headerLocale = acceptLanguage.split(',')[0].split(';')[0];
    // 处理如zh-CN、zh的情况,匹配SUPPORTED_LOCALES
    const matchedLocale = SUPPORTED_LOCALES.find(loc => 
      loc.startsWith(headerLocale.split('-')[0]) || headerLocale.startsWith(loc.split('-')[0])
    );
    if (matchedLocale) {
      return matchedLocale;
    }
  }

  // 4. 兜底返回默认语言
  return DEFAULT_LOCALE;
};

3.2.2 Baseline读取与数据合并工具(src/utils/i18n/baseline.ts)

// src/utils/i18n/baseline.ts
import fs from 'fs';
import path from 'path';
import jsonminify from 'jsonminify';
import { BASELINE_DIR, SUPPORTED_LOCALES, DEFAULT_LOCALE } from '@/locales/config/locale.config';
import { CORE_KEYS } from '@/locales/config/core-keys.config';

/**
 * 读取本地Baseline翻译数据(仅核心key)
 * @param locale 语言ISO码(zh-CN/en)
 * @param routes 需要读取的路由列表(如['common', 'home'])
 * @returns 合并后的本地Baseline数据 { common: {}, home: {} }
 */
export const getLocalBaseline = (locale: string, routes: string[]): Record<string, any> => {
  try {
    // 校验语言是否支持,不支持则降级到默认语言
    const validLocale = SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE;
    const result = {};

    for (const route of routes) {
      // 拼接Baseline文件路径
      const filePath = path.join(BASELINE_DIR, route, `${validLocale}.json`);
      // 若文件不存在,降级到默认语言的对应文件
      const finalFilePath = fs.existsSync(filePath) ? filePath : path.join(BASELINE_DIR, route, `${DEFAULT_LOCALE}.json`);
      
      // 读取文件并压缩(移除空格/换行,减小内存占用)
      let fileContent = fs.readFileSync(finalFilePath, 'utf-8');
      fileContent = jsonminify(fileContent) || fileContent; // 极致压缩
      
      // 解析JSON,仅保留核心key(防止开发误加非核心key)
      const fullBaseline = JSON.parse(fileContent);
      const coreKeys = CORE_KEYS[route as keyof typeof CORE_KEYS] || [];
      const coreBaseline = coreKeys.reduce((obj, key) => {
        if (fullBaseline[key] !== undefined) {
          obj[key] = fullBaseline[key];
        }
        return obj;
      }, {} as Record<string, string>);

      result[route] = coreBaseline;
    }

    return result;
  } catch (err) {
    // 读取失败时返回空对象,避免程序崩溃,同时上报监控
    console.error('[getLocalBaseline] 读取本地Baseline失败:', err);
    return {};
  }
};

/**
 * 合并API数据与本地Baseline数据(API数据优先级高于Baseline)
 * 核心规则:API有则用API,API无则用Baseline,二者都无则返回key本身
 * @param apiData API返回的翻译数据({ common: {}, [route]: {} })
 * @param baselineData 本地Baseline数据({ common: {}, [route]: {} })
 * @returns 合并后的最终翻译数据
 */
export const mergeI18nData = (
  apiData: Record<string, any>,
  baselineData: Record<string, any>
): Record<string, any> => {
  // 递归合并,确保嵌套结构也能正确覆盖(若有嵌套key)
  const deepMerge = (apiObj: any, baselineObj: any): any => {
    if (typeof apiObj !== 'object' || apiObj === null) {
      return apiObj ?? baselineObj ?? '';
    }

    const merged = { ...baselineObj, ...apiObj };
    Object.keys(merged).forEach(key => {
      merged[key] = deepMerge(apiObj[key], baselineObj[key]);
    });

    return merged;
  };

  // 遍历所有路由,合并对应的数据
  const mergedResult = {};
  const allRoutes = [...new Set([...Object.keys(apiData), ...Object.keys(baselineData)])];
  
  allRoutes.forEach(route => {
    const apiRouteData = apiData[route] || {};
    const baselineRouteData = baselineData[route] || {};
    mergedResult[route] = deepMerge(apiRouteData, baselineRouteData);
  });

  return mergedResult;
};

3.2.3 API请求工具(src/api/i18n.ts)

// src/api/i18n.ts
import axios from 'axios';
import { API_I18N_BASE_URL, SUPPORTED_LOCALES, SUPPORTED_ROUTES } from '@/locales/config/locale.config';

/**
 * 从API拉取翻译数据(按需拉取:当前语言+指定路由)
 * @param locale 语言ISO码(zh-CN/en)
 * @param routes 需要拉取的路由列表(如['common', 'home'])
 * @returns API返回的全量翻译数据 { common: {}, home: {} }
 */
export const fetchI18nFromApi = async (locale: string, routes: string[]): Promise<Record<string, any>> => {
  // 校验参数合法性,避免无效请求
  if (!SUPPORTED_LOCALES.includes(locale)) {
    throw new Error(`不支持的语言:${locale}`);
  }
  const validRoutes = routes.filter(route => SUPPORTED_ROUTES.includes(route));
  if (validRoutes.length === 0) {
    throw new Error(`无有效路由:${routes.join(',')}`);
  }

  try {
    // 并发拉取指定路由的翻译数据(提升效率)
    const requests = validRoutes.map(route => {
      const url = `${API_I18N_BASE_URL}/${route}/${locale}.json`;
      return axios.get(url, {
        headers: {
          'Accept-Encoding': 'gzip, deflate, br' // 开启压缩传输
        },
        timeout: 3000, // 超时时间3s,避免阻塞服务
        responseType: 'json'
      });
    });

    const responses = await Promise.all(requests);
    const result = {};
    validRoutes.forEach((route, index) => {
      result[route] = responses[index].data;
    });

    return result;
  } catch (err) {
    // 请求失败(超时/404/500),抛出错误,触发兜底逻辑
    console.error(`[fetchI18nFromApi] 拉取API翻译数据失败(locale: ${locale}, routes: ${validRoutes.join(',')}):`, err);
    throw err;
  }
};

3.3 缓存实例初始化(src/cache/index.ts)

// src/cache/index.ts
import LRU from 'lru-cache';
import Redis from 'ioredis';

// 1. LRU内存缓存实例(一级缓存,单实例高性能)
export const lruCache = new LRU({
  max: 1024 * 1024 * 100, // 缓存容量:100MB(足够存储20+语言×所有路由的翻译数据)
  maxAge: 1000 * 60 * 15, // 15分钟过期(被动淘汰,主动失效优先)
  updateAgeOnGet: true, // 获取缓存时更新过期时间,提升常用数据命中率
});

// 2. Redis分布式缓存实例(二级缓存,集群部署)
export const redisCache = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD,
  db: Number(process.env.REDIS_DB) || 0,
  connectTimeout: 3000, // 连接超时3s
  retryStrategy: (times) => {
    // 重试策略:失败后重试,最多重试3次,每次间隔1s
    if (times > 3) {
      return null; // 停止重试
    }
    return 1000;
  },
});

// 缓存key生成工具(统一格式,便于管理和失效)
export const generateCacheKey = (locale: string, route: string): string => {
  return `i18n:${locale}:${route}`;
};

3.4 核心服务封装(NestJS服务,Next.js可直接调用)

// src/services/i18n.service.ts(NestJS服务)
import { Injectable, Logger } from '@nestjs/common';
import { lruCache, redisCache, generateCacheKey } from '@/cache';
import { fetchI18nFromApi } from '@/api/i18n';
import { getLocalBaseline, mergeI18nData } from '@/utils/i18n/baseline';
import { reportMonitor } from '@/utils/monitor'; // 监控上报工具(自行实现)
import { SUPPORTED_ROUTES } from '@/locales/config/locale.config';

@Injectable()
export class I18nService {
  private readonly logger = new Logger(I18nService.name);
  private readonly CACHE_EXPIRE = 3600; // Redis缓存过期时间:1h(平衡命中率和更新及时性)

  /**
   * 核心方法:获取翻译数据(适配所有环境,集成缓存+API+Baseline兜底)
   * @param locale 语言ISO码(zh-CN/en)
   * @param route 当前路由(home/trade/order)
   * @returns 最终翻译数据(供服务端注入客户端)
   */
  async getI18nData(locale: string, route: string): Promise<Record<string, any>> {
    // 校验当前路由是否有效
    if (!SUPPORTED_ROUTES.includes(route)) {
      this.logger.warn(`[getI18nData] 无效路由:${route},降级到默认路由home`);
      route = 'home';
    }

    const needRoutes = ['common', route]; // 仅拉取common+当前路由,按需加载
    const isDev = process.env.APP_ENV === 'development';

    // 步骤1:优先从LRU内存缓存读取(一级缓存,最快)
    const lruCacheKeys = needRoutes.map(r => generateCacheKey(locale, r));
    const lruData = this.getFromLruCache(lruCacheKeys, needRoutes);
    if (lruData) {
      this.logger.debug(`[getI18nData] LRU缓存命中(locale: ${locale}, route: ${route})`);
      return lruData;
    }

    // 步骤2:LRU未命中,从Redis读取(二级缓存,集群一致)
    const redisData = await this.getFromRedisCache(locale, needRoutes);
    if (redisData) {
      this.logger.debug(`[getI18nData] Redis缓存命中(locale: ${locale}, route: ${route})`);
      // 更新LRU缓存,提升后续读取效率
      this.setToLruCache(locale, needRoutes, redisData);
      return redisData;
    }

    // 步骤3:双层缓存未命中,拉取API数据(核心数据源)
    let apiData = {};
    let finalData = {};
    try {
      apiData = await fetchI18nFromApi(locale, needRoutes);
      this.logger.debug(`[getI18nData] API拉取成功(locale: ${locale}, route: ${route})`);

      // 步骤4:本地开发环境,合并Baseline补全API缺失数据
      if (isDev) {
        const baselineData = getLocalBaseline(locale, needRoutes);
        finalData = mergeI18nData(apiData, baselineData);
      } else {
        // 测试/生产环境,直接使用API数据,不合并Baseline
        finalData = apiData;
      }

      // 步骤5:更新双层缓存(仅缓存API正常返回的数据,避免脏数据)
      this.setToLruCache(locale, needRoutes, finalData);
      await this.setToRedisCache(locale, needRoutes, finalData);

      return finalData;
    } catch (err) {
      // 步骤6:API拉取失败,触发Baseline兜底(所有环境均生效)
      this.logger.error(`[getI18nData] API拉取失败,触发Baseline兜底(locale: ${locale}, route: ${route})`, err.stack);
      
      // 上报监控告警(附带环境、语言、路由、错误信息,便于排查)
      reportMonitor({
        type: 'i18n_api_error',
        env: process.env.APP_ENV,
        locale,
        route,
        error: err.message,
        timestamp: new Date().getTime(),
      });

      // 读取本地Baseline作为兜底数据
      const baselineData = getLocalBaseline(locale, needRoutes);
      finalData = baselineData;

      // 兜底数据不写入缓存,避免脏数据(API恢复后需重新拉取)
      return finalData;
    }
  }

  /**
   * 从LRU缓存读取数据(批量读取common+当前路由)
   */
  private getFromLruCache(cacheKeys: string[], routes: string[]): Record<string, any> | null {
    const data = {};
    for (let i = 0; i < cacheKeys.length; i++) {
      const key = cacheKeys[i];
      const route = routes[i];
      const routeData = lruCache.get(key);
      if (!routeData) {
        return null; // 有一个路由未命中,视为整体未命中
      }
      data[route] = routeData;
    }
    return data;
  }

  /**
   * 写入LRU缓存(批量写入common+当前路由)
   */
  private setToLruCache(locale: string, routes: string[], data: Record<string, any>): void {
    routes.forEach(route => {
      const cacheKey = generateCacheKey(locale, route);
      const routeData = data[route] || {};
      lruCache.set(cacheKey, routeData);
    });
  }

  /**
   * 从Redis缓存读取数据(批量读取common+当前路由)
   */
  private async getFromRedisCache(locale: string, routes: string[]): Promise<Record<string, any> | null> {
    const data = {};
    for (const route of routes) {
      const cacheKey = generateCacheKey(locale, route);
      const routeDataStr = await redisCache.get(cacheKey);
      if (!routeDataStr) {
        return null; // 有一个路由未命中,视为整体未命中
      }
      data[route] = JSON.parse(routeDataStr);
    }
    return data;
  }

  /**
   * 写入Redis缓存(批量写入common+当前路由)
   */
  private async setToRedisCache(locale: string, routes: string[], data: Record<string, any>): Promise<void> {
    const pipeline = redisCache.pipeline(); // 管道操作,提升效率
    routes.forEach(route => {
      const cacheKey = generateCacheKey(locale, route);
      const routeData = data[route] || {};
      pipeline.set(cacheKey, JSON.stringify(routeData), 'EX', this.CACHE_EXPIRE);
    });
    await pipeline.exec();
  }

  /**
   * 主动清除缓存(翻译更新时调用,避免脏数据)
   * @param locale 语言ISO码(可选,不传递则清除所有语言)
   * @param route 路由(可选,不传递则清除所有路由)
   */
  async clearCache(locale?: string, route?: string): Promise<void> {
    // 清除LRU缓存
    if (locale && route) {
      // 清除指定语言+指定路由的LRU缓存
      const cacheKey = generateCacheKey(locale, route);
      lruCache.delete(cacheKey);
      // 同时清除common的缓存(若有依赖)
      const commonCacheKey = generateCacheKey(locale, 'common');
      lruCache.delete(commonCacheKey);
    } else if (locale) {
      // 清除指定语言的所有路由LRU缓存
      lruCache.forEach((_, key) => {
        if (key.startsWith(`i18n:${locale}:`)) {
          lruCache.delete(key);
        }
      });
    } else {
      // 清除所有LRU缓存
      lruCache.reset();
    }

    // 清除Redis缓存
    if (locale && route) {
      // 清除指定语言+指定路由的Redis缓存
      const cacheKey = generateCacheKey(locale, route);
      await redisCache.del(cacheKey);
      const commonCacheKey = generateCacheKey(locale, 'common');
      await redisCache.del(commonCacheKey);
    } else if (locale) {
      // 清除指定语言的所有路由Redis缓存

(注:文档部分内容可能由 AI 生成)