老胡茶室
老胡茶室

出海应用实战篇:从 1000+ 行到 300+ 行,Next.js项目的 i18n 优化全攻略

小纪同学

1. 背景与问题

在早期开发阶段,我们为了追求迭代速度,大量依赖 AI 提升编码效率。当时的首要目标是“快”,因此对 i18n 的设计与规范并未投入足够关注。

这种 “先上车、后补票” 的做法,直接导致 i18n 成为了重灾区:由于缺乏约束和统一规范,AI 在生成内容时只能沿用现有写法,结果 i18n 文件持续膨胀,并逐渐出现各种问题。例如:key 命名随意、层级结构混乱、重复或无效的文案层出不穷。一开始大家抱着“能跑就行”的心态,i18n 的 review 经常被忽略;但文件越大,就越没人愿意仔细审查,久而久之问题被进一步放大。最终,臃肿的 i18n 不仅让维护成本居高不下,也让 AI 难以正确分析和修改内容,经常引发错误。

基于这些痛点,我们决定对 i18n 系统进行一次彻底重构,从根本上解决积累下来的问题。

目前主要存在的问题包括:

  • JSON 文件太大:单语言文件 1000+ 行,翻起来都头晕。
  • key 命名随缘:有的写 user.name,有的直接 username,根本没规律。
  • 嵌套混乱:有的地方三层嵌套,有的地方平铺。
  • 僵尸 key:文件里一堆没人用的 key。
  • 动态 key 检测不到:比如状态码、错误信息这种拼 key 的写法,全靠肉眼排查。
  • 没有类型提示:写错或者缺失的 key 在运行期报错时才能发现。

总之就是 能跑,但越来越不可维护

2. 优化目标

这次重构,我给自己定了几个目标:

  1. 精简 JSON 文件:把冗余的 key 清掉,减少体量。
  2. 统一命名规范:模块化,层级清晰。
  3. 对齐页面引用:只保留真正用到的 key。
  4. 引入类型安全:写错 key 立马报错,而不是等运行。
  5. 动态 key 可控:用枚举来兜底,不再漏网。
  6. 分层文案结构:UI 通用文案 vs 业务文案,彻底分开。

3. 优化思路与实践

主要分为三个阶段:

阶段 1:修复缺失 key
使用 next-intl + TypeScript 自动生成 i18n key 类型,确保编译时错误检测。

阶段 2:清理僵尸 key
引入 i18n-check 工具进行静态检测,只保留真正用到的 key。

阶段 3:重构设计架构
优化 key 命名规范,建立模块化的 i18n 结构。

3.1 阶段 1 - TypeScript 类型安全

在所有 i18n 问题中,缺失 key 是影响最严重的:一旦发生,就会直接暴露到页面,造成极差的用户体验。因此,这类问题必须优先解决。

我们的方案是引入 next-intl 提供的 TypeScript augmentation 功能。通过在编译阶段自动生成并校验 i18n key 类型,可以在编辑器里实现 实时提示与校验,确保不会再出现 t("user.notExistKey") 这种运行时错误。

虽然这种做法会略微降低编译速度,但它能在编译环节就提前发现潜在 BUG,避免问题流入生产环境。这不仅等同于一次 BUG 修复,而且对现有代码侵入最小、见效也最直接,因此被列为 i18n 重构的 第一优先级

参考文档 👉 next-intl TypeScript

next-intl 在构建时根据多语言 JSON 自动生成 .d.ts 类型文件,需要在 next.config.ts 里显式开启:

import createNextIntlPlugin from "next-intl/plugin";
import withBundleAnalyzerConfig from "./bundleAnalyzer.config"; // 举例

const withNextIntl = createNextIntlPlugin({
  experimental: {
    // 指定要生成类型声明的 messages 文件
    createMessagesDeclaration: [
      "./messages/en.json",
      "./messages/zh.json",
      "./messages/ja.json",
    ],
  },
});

export default withNextIntl(
  withBundleAnalyzerConfig({
    /* 你的 Next.js 配置 */
  })
);

global.ts 文件中添加 TypeScript 类型声明:

//global.ts
import type enMessages from "./messages/en.json";
import type jaMessages from "./messages/ja.json";
import type zhMessages from "./messages/zh.json";

const locales = ["en", "zh", "ja"] as const;

declare module "next-intl" {
  interface AppConfig {
    Locale: (typeof locales)[number];
    Messages: typeof enMessages | typeof zhMessages | typeof jaMessages;
  }
}

💡 实践要点:
Messages 类型定义中,我们使用了 union 类型连接三种语言:typeof enMessages | typeof zhMessages | typeof jaMessages,这样做的好处是可以同时检测三种语言的类型一致性,确保所有语言文件的 key 结构保持同步,避免某个语言文件缺失 key 的情况。

这样,当你运行 devbuild 时:

  1. Next.js 会读取这些 messages/*.json 文件
  2. next-intl 会分析 key 结构并生成 .d.ts 文件
  3. TypeScript 自动识别这些类型,保证 t("xxx.yyy") 有提示和错误检测

调用时:

tCommon('noData'); // ✅ 有提示 & 校验

//以前写 tCommon('notExistKey');(不存在的 key)只有测试才发现,现在编辑器里就能直接报错。
tCommon('notExistKey'); // ❌ 立即报错

大大提升了可维护性,再也不用担心引用不存在的 key 导致错误直接暴露在页面,影响用户体验。

3.2 阶段 2 - 引入 i18n-check

解决了 key 缺失的问题,接下来就是重构 i18n 的质量问题。经过多次功能迭代,无论是人工修改还是 AI 编辑,往往只专注于业务代码的变更,而忽略了同步维护 i18n 文件。当功能被删除、页面被重构或需求发生变化时,相关的翻译 key 却被遗留下来,久而久之积累成了大量”历史遗留 key” —— 这些 key 在 JSON 文件中占据空间,但页面根本没有引用,让 i18n 逐渐变成了维护困难的”屎山”。为此我们引入了 next-intl 官方文档推荐的工具 i18n-check

i18n-check 本质上就是一个 静态检测工具,它主要做了两件事:

  1. 扫描代码里用到的 key
    • 它会遍历你的代码文件(默认支持 TS/JS/React 等常见语法),找到 t('xxx.yyy') 这种调用。
    • 把所有出现过的 key 都收集起来。
  2. 对比 messages.json 文件
    • 再去读取你定义的 messages/*.json,把里面的所有 key 展平。
    • 然后做一个集合对比:
      • 代码里有,但 JSON 里没有 → 报 缺失 key
      • JSON 里有,但代码里没用到 → 报 多余 key(僵尸 key)

💡 额外功能:基准文件对齐检测

  • 通过 -s--source 参数指定基准语言文件(如 zh.json
  • 将其他语言文件与基准文件进行对比:
    • 基准文件有,其他语言文件缺失 → 报 缺失翻译
  • 目的是确保所有语言文件的 key 结构保持一致,避免某个语言缺失翻译或存在多余内容

小限制

  • 对于动态 key(比如 t("status." + status))它是分析不出来的,因为静态扫描的时候拿不到运行时的变量。
  • 这类情况就需要我们用 枚举 / 联合类型 在 TS 里约束,或者提前把所有状态 key 都写进 JSON 文件里。

总的来说,它能帮忙找出:

  • 页面用到但 JSON 里不存在的 key
  • JSON 里有但没被引用的 key
  • 不同语言文件之间 key 结构不一致的问题

这样就能保证 “用到的 key 必须存在,没用的必须删掉”

运行 i18n-check 后,我们能看到具体的检测结果:

i18n检测 - 缺失的key 检测出代码中使用但在JSON文件中缺失的key

i18n检测 - 未使用的key
检测出JSON文件中存在但代码中未使用的僵尸key

i18n检测 - 无效的key 基于zh.json检测出en.jsons文件中存在但无效的key

根据检测结果进行针对性的调整,清理 unused key 时, 我们砍掉了大量 key,i18n 文件也从 1000+ 行数减少到了 600+ 行。这些 key 大都是因为功能代码发生变动的时候,只删除了功能代码,却未同步调整 i18n 翻译导致的积累问题。

3.3 阶段 2 - 动态 key 的检测与优化

按照 i18n-check 分析出来的结果一一解决 key 的问题,最后发现有些动态 key 没有办法检测到,这一部分 key 是依赖于接口返回的错误信息,他会把这些 key 当成 unused keys 分析出来。

i18n检测 - 动态的key 检测出动态key

解决方法:用 枚举 + 类型约束

//之前的写法:动态类型 ❌ 检测不出来
type ApiErrorKey =
  | "notFound"
  | "fetchError"
  | "nameExists"

function renderMessage(error: unknown) {
   toast.error(tMessages(error.message as ApiErrorKey));
}
//现在的写法:添加了枚举 + 类型约束 ✅ 有提示 & 校验
type ApiErrorKey =
  | "notFound"
  | "fetchError"
  | "nameExists"

const ERROR_MESSAGE_MAP: Record<string, ApiErrorKey> = {
  notFound: "notFound",
  fetchError: "fetchError",
  nameExists: "nameExists",
};

function renderMessage(error: unknown) {
   const errorKey = ERROR_MESSAGE_MAP[error.message];
   toast.error(tMessages(errorKey));
}

这样即使以后新增错误消息,TS也会提醒有缺失的内容。

现在 i18n-check 检测的所有问题都已经解决了: i18n检测 - 全部通过 检测结果

完美!🎉

3.4 阶段 3 - 模块化与i18n规范

在解决了 i18n-check 检测问题之后,进入 i18n 重构阶段。这个阶段主要解决结构化和规范化问题,让 i18n 文件从”能用”变成”好维护”。

清理重复 key 与分层设计

问题分析

原来的 JSON 文件完全是”野蛮生长”的状态。每个功能模块在开发时,AI 都会生成一套完整的文案,导致大量重复:

// 重构前:各模块都有重复的通用文案
{
  "user": {
    "noData": "暂无用户",
    "save": "保存",
    "cancel": "取消"
  },
  "order": {
    "noData": "暂无订单", 
    "save": "保存",
    "cancel": "取消"
  }
}

统计下来,光是 savecanceldelete 这几个基础操作文案,在不同模块中就重复出现了 15+ 次。

分层设计原则

我们制定了明确的分层规范:

  1. common:存放所有 UI 通用文案

    • 基础操作:保存、取消、删除、编辑、确认等
    • 状态提示:加载中、成功、失败、无数据等
    • 通用组件:分页、搜索、筛选等
  2. 业务模块层:存放特定业务的文案

    • 只包含该模块独有的字段名、状态、提示等
    • 不允许出现通用操作文案
  3. 嵌套层级限制:最多 2 级嵌套

    • 第1级:模块名(如 userorder
    • 第2级:具体字段或分组(如 namestatus
    • 禁止更深层级,避免 user.profile.basic.name 这种复杂结构
重构后的结构
{
  "common": {
    "noData": "暂无数据",
    "save": "保存", 
    "cancel": "取消",
    "loading": "加载中..."
  },
  "user": {
    "name": "用户名",
    "email": "邮箱",
    "profile": "个人资料"
  },
  "order": {
    "id": "订单号",
    "status": "订单状态",
    "total": "总金额"
  }
}
重构操作步骤
  1. 统计重复文案:用 AI 扫描所有模块,找出重复率超过 50% 的 key
  2. 提取到 common:把高频重复的文案全部移到 common 模块
  3. 清理业务模块:删除各业务模块中的重复文案
  4. 验证完整性:确保页面引用不会出错

统一命名与使用规范

命名混乱的具体表现

重构前的代码里,同一个页面可能出现多种写法:

// 页面A:驼峰不规范
const tuser = useTranslations("user");
const tcommon = useTranslations("common"); 
tuser("name");
tcommon("save");

// 页面B:全局引用
const t = useTranslations();
t("user.name");
t("common.save");

// 页面C:随意命名
const userT = useTranslations("user");
const commonTranslation = useTranslations("common");
userT("name");
commonTranslation("save");

// 页面D:混合使用
const t = useTranslations();
const tUser = useTranslations("user");
t("common.save");
tUser("name");

这种混乱导致的问题:

  • 可读性差:看不出来是哪个模块的文案
  • 维护困难:重构时需要逐个检查每种写法
  • 团队协作混乱:每个人都有自己的习惯
制定命名规范

我们制定了严格的命名标准:

// 1. 变量命名规范:t + 模块名(首字母大写)
const tCommon = useTranslations("common");
const tUser = useTranslations("user");

// 2. 使用规范:模块化引用,避免全局引用
// ✅ 推荐写法
tCommon("save");
tUser("name");

// ❌ 禁止写法
const t = useTranslations();
t("common.save");  // 全局引用不清晰
多模块页面的处理

对于需要用到多个模块文案的复杂页面,我们也有规范:

// 用户订单页面 - 同时需要用户和订单文案
const tCommon = useTranslations("common");
const tUser = useTranslations("user"); 
const tOrder = useTranslations("order");

function UserOrderPage() {
  return (
    <div>
      <h1>{tUser("orderHistory")}</h1>
      <Table
        columns={[
          { title: tOrder("orderNo"), dataIndex: "orderNo" },
          { title: tOrder("amount"), dataIndex: "amount" },
          { title: tUser("name"), dataIndex: "userName" }
        ]}
        emptyText={tCommon("noData")}
      />
      <Button onClick={handleSave}>
        {tCommon("save")}
      </Button>
    </div>
  );
}

可组合的 Key 设计

设计理念

传统的 i18n 设计往往是”一对一”的关系:一个业务场景对应一个具体的文案。但这样会导致大量相似文案的重复。

我们采用了”可组合”的设计理念:把文案拆分成可复用的原子单元,通过组合来应对不同场景

具体实践案例
组合设计
// 重构前:每个操作都有专门的提示文案
{
  "user": {
    "saveSuccess": "用户保存成功",
    "deleteSuccess": "用户删除成功",
    "saveError": "用户保存失败",
    "deleteError": "用户删除失败"
  },
  "order": {
    "saveSuccess": "订单保存成功", 
    "deleteSuccess": "订单删除成功",
    "saveError": "订单保存失败",
    "deleteError": "订单删除失败"
  }
}

// 重构后:可组合的设计
{
  "common": {
    "operationSuccess": "{action}成功",
    "operationError": "{action}失败"
  },
  "actions": {
    "save": "保存",
    "delete": "删除",
    "update": "更新",
    "create": "创建"
  }
}

// 使用时动态组合
const tCommon = useTranslations("common");
const tActions = useTranslations("actions");

// 保存成功提示
toast.success(tCommon("operationSuccess", { 
  action: tActions("save") 
})); // "保存成功"

// 删除失败提示  
toast.error(tCommon("operationError", { 
  action: tActions("delete") 
})); // "删除失败"
组合设计的优势
  1. 减少重复:原来需要 30+ 个状态提示文案,现在只需要几个模板
  2. 保持一致性:所有操作的提示风格完全统一
  3. 易于维护:修改一个模板,所有相关场景都会同步更新
  4. 支持扩展:新增操作类型时,无需添加新的提示文案

通过这一步,我们的 JSON 文件从 600+ 行直接减少到了 300+ 行,减幅达到 40%。

4. 优化成果

这次重构的结果:

  • JSON 文件从 1000+ 行 → 300+ 行
  • key 命名统一,结构清晰
  • 页面引用和 JSON 严格对齐
  • 动态 key 有了类型安全兜底
  • TypeScript + i18n-check 双重保障
  • 文案结构从“业务驱动”转为“UI 通用 + 业务特定”
  • 命名和使用习惯规范,排查效率提升
  • 现在即便用 AI 辅助,也能轻松修改和维护

优化前 vs 优化后(对比表)

维度优化前(Before)优化后(After)
JSON 文件体量1000+ 行,翻到眼花300+ 行,结构清晰
key 命名随缘:username / user.name模块化:tUser("name")
嵌套层级不固定,最多四五层最多两层,简单直观
页面引用对齐有僵尸 key,有缺失 key严格对齐:i18n-check 定期校验
动态 keyt("status." + status) 不可控枚举 + TS 约束,新增必报错
类型提示没有,拼错运行后才发现IDE 实时报错:tUser("notExist")
AI 辅助维护1000 行上下文,AI 也乱结构清晰,AI 能轻松处理

5. 最佳实践总结

最后整理一套经验:

  • 最多两级嵌套,避免过深结构
  • key 命名规范:模块化,比如 tUser("name")
  • UI 通用 vs 业务文案分层
  • 动态 key 必须枚举,不能裸拼字符串
  • TS + i18n-check 结合,保证类型安全
  • 定期清理 JSON,避免膨胀

开发规范总结

基于这次重构实践,我们基于当前项目确立了一套完整的 i18n 开发规范:

  1. 翻译变量统一命名为 t + PascalCase:命名空间与 JSON 顶层保持一致
  2. 优先组合已有 key:不要为组合创建新 key,最大化复用现有文案
  3. 新增 key 必须标记 TODO:开发阶段标记,避免遗漏清理
  4. JSON 结构最多两级:禁止深层嵌套,保持结构简单明了
  5. 基础模块通用化:labels、actions、status 等尽量通用,可组合使用
  6. 功能模块按场景划分:titles、common、messages 等按功能分组
  7. 动态 key 尽量避免:必要时用枚举保证类型安全
  8. 开发阶段遵循标准流程:命名 → 组合 → TODO 标记的开发节奏
  9. 提交前运行 i18n-check:确保 key 完整性,避免缺失或冗余

AI 编码规范的沉淀

基于这次重构的实践经验,我们将完整的 i18n 开发规范整理到了 llm-skills/ 目录中,作为给 AI 学习的编码规范,用于提高 AI 的编码质量与效率。

例如,针对嵌套结构的约束,我们制定了如下规范:

4. Maximum Two-Level Nesting Structure

Translation key structure should have at most two levels of nesting, avoid deep nesting:

// ✅ Correct: Maximum two levels
{
  "titles": {
    "site": "TSW Team", 
    "admin": "Admin Panel"
  }
}

// ❌ Wrong: Too deep nesting
{
  "admin": {
    "pages": {
      "agents": {
        "title": "Agent Management"
      }
    }
  }
}

通过将实践中总结的规范文档化,AI 在后续开发中能够:

  • 自动遵循最佳实践:避免重复之前的设计错误
  • 保持代码一致性:确保团队成员使用统一的编码风格
  • 提升编码效率:减少因规范不明确导致的反复修改

这种将实战经验转化为 AI 学习材料的做法,让我们的开发规范真正发挥了作用,形成了正向的迭代循环。

总结

这一套下来,整个 i18n 的维护成本明显下降,开发体验也好了不少。归根结底:不要把 i18n 当成”文案仓库”,而是当成项目代码的一部分去维护

付费内容

本文包含付费内容,需要会员权限才能查看完整内容。

精品内容