Easy-Lang:中文即 key,让前端国际化回归所见即所得

1 阅读12分钟

开源地址:github.com/chennlang/e… · 觉得不错的话,欢迎 ⭐ Star

不知道大家在前端国际化开发中有没有遇到如下问题?

1、每次翻译文本都得想一个变量名,命名流程重复且繁琐。

2、翻译后的源代码变成了英文变量,缺少了可阅读性检索能力,想通过界面上的中文搜索到对应模块变得不可能。

3、翻译流程很繁琐:先命名、再翻译、再写入到多个翻译文件……

常见的前端国际化方案

下面用一个登录页面为例,看看传统的国际化翻译是怎么使用的。

翻译前

function LoginForm() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const remainingAttempts = 3
  const lastLoginTime = new Date()

  return (
    <div className="login-container">
      <h1>用户登录</h1>
      {error && <div className="error">登录失败: {error.message}</div>}

      <form>
        <div className="form-group">
          <label htmlFor="username">用户名:</label>
          <input 
            id="username" 
            placeholder="请输入用户名或邮箱" 
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">密码:</label>
          <input
            id="password"
            type="password"
            placeholder="请输入6-20位密码"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <div className="hint">密码必须包含大小写字母和数字</div>
        </div>

        <button type="submit" disabled={!username || !password}>
          登录
        </button>

        <div className="footer">
          <span>您还可以尝试 {remainingAttempts} 次</span>
          <span>上次登录时间: {lastLoginTime.toLocaleString()}</span>
          <a href="/reset-password">忘记密码?</a>
        </div>
      </form>
    </div>
  )
}

翻译后

function LoginForm() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const remainingAttempts = 3
  const lastLoginTime = new Date()

  return (
    <div className="login-container">
      <h1>{t('login.title')}</h1>
      {error && (
        <div className="error">
          {t('login.error.message', { error: error.message })}
        </div>
      )}

      <form>
        <div className="form-group">
          <label htmlFor="username">{t('login.username.label')}:</label>
          <input 
            id="username" 
            placeholder={t('login.username.placeholder')}
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">{t('login.password.label')}:</label>
          <input
            id="password"
            type="password"
            placeholder={t('login.password.placeholder')}
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <div className="hint">{t('login.password.hint')}</div>
        </div>

        <button 
          type="submit" 
          disabled={!username || !password}
          aria-label={t('login.submit.ariaLabel')}
        >
          {t('login.submit.text')}
        </button>

        <div className="footer">
          <span>
            {t('login.attempts.remaining', { count: remainingAttempts })}
          </span>
          <span>
            {t('login.lastLogin', { 
              datetime: lastLoginTime,
              formatParams: {
                datetime: { 
                  year: 'numeric', 
                  month: 'long', 
                  day: 'numeric',
                  hour: '2-digit',
                  minute: '2-digit'
                }
              }
            })}
          </span>
          <a href="/reset-password">
            {t('login.forgotPassword')}
          </a>
        </div>
      </form>
    </div>
  )
}

翻译资源文件 zh-CN

{
  "login": {
    "title": "用户登录",
    "error": {
      "message": "登录失败: {error}"
    },
    "username": {
      "label": "用户名",
      "placeholder": "请输入用户名或邮箱"
    },
    "password": {
      "label": "密码",
      "placeholder": "请输入6-20位密码",
      "hint": "密码必须包含大小写字母和数字"
    },
    "submit": {
      "text": "登录",
      "ariaLabel": "提交登录表单"
    },
    "attempts": {
      "remaining": "您还可以尝试 {count} 次",
      "remaining_plural": "您还可以尝试 {count} 次"  // 某些语言可能需要复数形式
    },
    "lastLogin": "上次登录时间: {datetime}",
    "forgotPassword": "忘记密码?"
  }
}

翻译资源文件 en-US

{
  "login": {
    "title": "User Login",
    "error": {
      "message": "Login failed: {error}"
    },
    "username": {
      "label": "Username",
      "placeholder": "Enter username or email"
    },
    "password": {
      "label": "Password",
      "placeholder": "Enter password (6-20 characters)",
      "hint": "Password must contain uppercase, lowercase letters and numbers"
    },
    "submit": {
      "text": "Sign In",
      "ariaLabel": "Submit login form"
    },
    "attempts": {
      "remaining": "You have {count} attempt remaining",
      "remaining_plural": "You have {count} attempts remaining"
    },
    "lastLogin": "Last login: {datetime}",
    "forgotPassword": "Forgot password?"
  },
  "dateTimeFormats": {
    "short": {
      "year": "numeric",
      "month": "short",
      "day": "numeric",
      "hour": "2-digit",
      "minute": "2-digit"
    },
    "long": {
      "year": "numeric",
      "month": "long",
      "day": "numeric",
      "weekday": "long",
      "hour": "2-digit",
      "minute": "2-digit"
    }
  }
}

翻译资源文件 zh-TW

{
   // ...省略
}

翻译资源文件 ja-JP

{
   // ...省略
}

翻译资源文件 ko-KR

{
   // ...省略
}

更多翻译文件……


传统 i18n 的问题

1、变量命名

每翻译一个中文字符串,我都得想好一个英文变量名,这个对我来说有点难受,这也是我为什么一直以来选择 tailwindcss 的原因,要知道,程序员最难的就是命名和缓存

2、失去了可阅读性

翻译后的文件通篇都是英文变量,有时候翻译变量名是很随意的,这个时候想快速定位到某个不太熟悉的模块,是极其困难的。

3、失去了检索功能

然后就是全局搜索:通常我们想要定位某个文本,都会从页面上复制文本,再到编辑器里全局搜索,从而发现目标文件/模块。而现在,你只会搜索到 translation 文件。

4、高侵入性

我认为,在业务逻辑里面,国际化代码本就是一个可有可无的存在,就像 TS 语言,去除所有类型依旧能跑,它应该只是辅助编码和提示。而目前的国际化方案,重构了代码的逻辑,替换了原有的字符代码。

5、翻译流程复杂和困难

想想我们现在翻译的流程是怎样的?

1、变量命名、改写代码变成 t('字符串')

2、找到翻译工具,把 t('字符串') 翻译成其它语言。

3、在 locales 下找到所有翻译文件,并定位到对应模块里,填入翻译后的文本。

目录如下:

locales
  - en
    - translation.json
  - zh-CN
    - translation.json
  - zh-TW
    - translation.json
  ....

6、缺乏复用性

同一个翻译文本,例如 '确认'、'取消'、'删除',这些比较通用的文本,在很多翻译模块文件里都有重复定义。当然,我们可以把一些常见的翻译提取到一个 common 模块。不过,这样做会增加开发的复杂度,因为每次翻译前,得先检索 common 模块里是否有这个翻译,没有再重新定义,这很费脑!

所以,开发不应该只关注功能和业务吗?为什么要消耗这么多时间在国际化上? 搜索一圈后,我并没有找到一个优雅的解决方案,至此,我不得不自己开发一款国际化翻译工具!

回归本质

我理解的国际化翻译原理应该很简单,就像下面这样:

const translations = {
    "退出登录": {
      "zh_CN": "退出登录",
      "zh_HK": "退出登錄",
      "en": "Logout"
    },
}

const currentLang = 'zh_CN'

function t(text: string) {
    return translations[text][currentLang]
}

console.log(t('退出登录'))

Easy Lang 发布了

传统的国际化方案问题一直困扰着我,有时候甚至严重影响到了我的日常开发体验。思索了很久,我决定开发一款心目中的国际化工具。它来了~

Easy Lang 是一个低侵入性的、简单的、多语言翻译工具。它本质上与框架无关,是一个纯 TS 工具,所以你可以在 Vue、React、Angular 等支持原生 JS/TS 的项目中使用。

特性一览:

  • 简单易用,API 友好
  • 支持多语言切换
  • 支持 React 项目无缝集成
  • 自动收集未翻译的 key,便于补全
  • 支持变量替换
  • 支持多模块翻译,按功能模块拆分翻译文件
  • 支持运行时动态配置(configure)与自定义存储
  • 轻量无第三方依赖(React 集成需 zustand

项目地址:github.com/chennlang/e…,觉得有用的话,欢迎点个 ⭐ Star~

安装

pnpm add easy-lang
# 或
npm install easy-lang
# 或
yarn add easy-lang

快速开始

建议的目录结构

locales/
  - index.ts
  - translation.json

1、新建翻译文件

locales/translation.json

{
  "测试": {
    "zh-CN": "测试",
    "zh-TW": "測試",
    "en-US": "Test"
  },
  "测试{name}": {
    "zh-CN": "测试{name}",
    "zh-TW": "測試{name}",
    "en-US": "Test{name}"
  }
}

2、使用

locales/index.ts

"use client";
import { createI18nTool } from "easy-lang";
import Transform from "./translation.json";

// 语言列表
export const langOptions = [
  {
    label: "English",
    value: "en-US",
  },
  {
    label: "简体中文",
    value: "zh-CN",
  },
  {
    label: "繁体中文",
    value: "zh-TW",
  },
] as const;

// 实例
export const i18nTool = createI18nTool<typeof Transform, (typeof langOptions)[number]["value"]>({
  defaultLang: 'zh-CN', // 默认语言
  langs: langOptions.map((lang) => lang.value), // 语言列表
  translations: Transform, // 翻译文件
})

// 翻译方案
export const $t = i18nTool.$t;

3、使用翻译

import { $t } from '@/locales/index'

// 普通翻译
console.log($t('测试'))

// 带变量翻译
console.log($t('测试{name}', { name: '你好' }))

4、模块化翻译

适用于大型项目或需要按模块组织翻译的场景。

版本 >= v1.1.0

// 定义模块化翻译文件
const translations = {
  default: {
    '你好': {
      "zh-CN": "你好",
      "en-US": "Hello",
    },
  },
  custom: {
    '欢迎 {name}': {
      "zh-CN": "欢迎 {name}",
      "en-US": "Welcome {name}",
    },
    '测试': {
      "zh-CN": "测试",
      "en-US": "Test",
    },
  },
} as const;

// 创建翻译工具
const i18n = createI18nTool<typeof translations, "zh-CN" | "en-US">({
  defaultLang: "zh-CN",
  langs: ["zh-CN", "en-US"],
  translations,
});

// 方式一:直接使用带模块的翻译
i18n.$t('欢迎 {name}', { name: '张三', module: 'custom' }); // => "欢迎 张三"
i18n.$t('测试', { module: 'custom' }); // => "测试"

// 方式二:创建模块专用的翻译函数(推荐)
const $t_custom = i18n.$module('custom');
$t_custom('测试'); // => "测试"
$t_custom('欢迎 {name}', { name: 'John' }); // => "欢迎 John"

类型上:不带 module$t("...") 只允许 default 模块的 key;带 { module: "xxx" } 或使用 $module("xxx") 时只允许对应模块的 key,可享受完整的类型提示。

同一个登录页,用 Easy Lang 翻译是什么效果?

回到前面的登录页例子,看看用 Easy Lang 是怎么翻译的:

function LoginForm() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const remainingAttempts = 3
  const lastLoginTime = new Date()

  return (
    <div className="login-container">
      <h1>{$t('用户登录')}</h1>
      {error && (
        <div className="error">
          {$t('登录失败: {error}', { error: error.message })}
        </div>
      )}

      <form>
        <div className="form-group">
          <label htmlFor="username">{$t('用户名')}:</label>
          <input 
            id="username" 
            placeholder={$t('请输入用户名或邮箱')} 
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">{$t('密码')}:</label>
          <input
            id="password"
            type="password"
            placeholder={$t('请输入6-20位密码')}
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <div className="hint">{$t('密码必须包含大小写字母和数字')}</div>
        </div>

        <button type="submit" disabled={!username || !password}>
          {$t('登录')}
        </button>

        <div className="footer">
          <span>
            {$t('您还可以尝试 {count} 次', { count: remainingAttempts })}
          </span>
          <span>
            {$t('上次登录时间: {datetime}', { datetime: lastLoginTime.toLocaleString() })}
          </span>
          <a href="/reset-password">{$t('忘记密码?')}</a>
        </div>
      </form>
    </div>
  )
}

对应的翻译文件,同样只有一个 translation.json

{
  "用户登录": {
    "zh-CN": "用户登录",
    "en-US": "User Login"
  },
  "登录失败: {error}": {
    "zh-CN": "登录失败: {error}",
    "en-US": "Login failed: {error}"
  },
  "用户名": {
    "zh-CN": "用户名",
    "en-US": "Username"
  },
  "请输入用户名或邮箱": {
    "zh-CN": "请输入用户名或邮箱",
    "en-US": "Enter username or email"
  },
  "密码": {
    "zh-CN": "密码",
    "en-US": "Password"
  },
  "请输入6-20位密码": {
    "zh-CN": "请输入6-20位密码",
    "en-US": "Enter password (6-20 characters)"
  },
  "密码必须包含大小写字母和数字": {
    "zh-CN": "密码必须包含大小写字母和数字",
    "en-US": "Password must contain uppercase, lowercase letters and numbers"
  },
  "登录": {
    "zh-CN": "登录",
    "en-US": "Sign In"
  },
  "您还可以尝试 {count} 次": {
    "zh-CN": "您还可以尝试 {count} 次",
    "en-US": "You have {count} attempt(s) remaining"
  },
  "上次登录时间: {datetime}": {
    "zh-CN": "上次登录时间: {datetime}",
    "en-US": "Last login: {datetime}"
  },
  "忘记密码?": {
    "zh-CN": "忘记密码?",
    "en-US": "Forgot password?"
  }
}

对比一下两种方案:

对比项传统 i18nEasy Lang
代码写法t('login.title')$t('用户登录')
变量命名需要先想英文变量名不需要
翻译文件每个语言一个文件,英文 key一个 translation.json,中文即 key
可读性通篇英文变量中文原样保留

代码里看到什么中文,翻译文件就以什么中文为 key,翻译后的效果所见即所得——不需要变量命名,也不需要跨文件查找。

React 项目中使用

pnpm add @easy-lang/react
# 
npm install @easy-lang/react
# 
yarn add @easy-lang/react

React 项目需额外安装 zustand 作为 peerDependency。

locales/index.ts

import translations from "./translation.json";
import { createI18nTool } from "easy-lang";
import { createReactI18nTool } from "@easy-lang/react";

const reactI18nTool = createReactI18nTool<
  typeof translations,
  "zh_CN" | "zh_HK" | "en"
>(
  createI18nTool({
    defaultLang: "zh_CN",
    langs: ["zh_CN", "zh_HK", "en"],
    translations,
  })
);

export const useTranslate = reactI18nTool.useTranslate();

App.tsx

import { useTranslate } from '@locales/index'
function App() {
  const { $t, changeLang, currentLang } = useTranslate;
  return (
    <div>
      <button onClick={() => changeLang("en")}>en</button>
      <button onClick={() => changeLang("zh_CN")}>中文</button>
      <div>当前语言: {currentLang}</div>
      <div>{$t("错误")}</div>
    </div>
  );
}

注意:changeLang 调用后会执行 location.reload() 刷新页面。

如果你的项目只用到了 useTranslate 进行翻译,请设置 autoReload: false,可以不用刷新页面就响应式更新。

变量替换

支持在翻译文本中使用 {变量名},如:

{
  "欢迎 {name}": {
    "en": "Welcome, {name}!"
  }
}

使用:

i18n.$t("欢迎 {name}", { name: "Tom" }); // => "Welcome, Tom!"

强制指定翻译语言

$t(以及 $module 生成的函数)的第三个参数可临时覆盖当前语言,用于指定场景:

i18n.$t("保存", {}, "zh_HK"); // => "保存"(强制繁体)

configure() 运行时配置

可在运行时动态调整配置,无需重建实例:

i18n.configure({
  defaultLang: "zh_CN",
  autoReload: false,        // 改为响应式更新,不刷新页面
  storageKey: "tenant-lang", // 自定义存储 key
});

自定义语言存储

默认使用 localStorage(key 为 lang)。当语言来自 query 参数、宿主应用、cookie 桥或已有设置中心时,可自定义 storage:

const i18n = createI18nTool({
  defaultLang: "en",
  langs: ["zh_CN", "zh_HK", "en"],
  translations,
  storage: {
    getLang({ defaultLang, langs, storageKey }) {
      const stored = localStorage.getItem(storageKey);
      return stored && langs.includes(stored) ? stored : defaultLang;
    },
    setLang(lang, { storageKey }) {
      localStorage.setItem(storageKey, lang);
    },
  },
});

getLang 返回 nullundefined 时,会回退到 defaultLang。SSR 场景下(无 window)会自动安全降级。

Easy Lang 解决了哪些问题?

easy-lang 不仅解决了 变量命名高侵入性缺少检索能力低复用性 等问题,还带来了全新的能力。

1、不需要变量命名了

首先,它不需要你再手动变量命名了,也不会改变原来的代码结构,你只需在开发时把所有需要翻译的字符串使用 $t() 包裹起来即可。

i18n.$t("你好");
i18n.$t("欢迎 {name}", { name: "Tom" }); 

也正是因为如此,翻译文本都原样保留在代码中,同时也保留了代码的可阅读性搜索能力。目前 translation.json 只有一个层级,解决了复用性问题,所以提倡大家尽量复用。

2、自带 TS 检测

基于 TS 的能力,easy-lang 能检测未翻译的文本并标红,使排查未翻译文本更加方便。

3、适应现代化 AI 编辑器

再看一眼翻译文件 JSON 的结构,我们发现单个翻译的所有语言都集中在同一个地方,不需要切换文件。

{
  "测试": {
    "zh-CN": "测试",
    "zh-TW": "測試",
    "en-US": "Test"
  },
  "确认": {
    "zh-CN": "确认",
    "zh-TW": "確認",
    "en-US": "Confirm"
  }
}

如果你用的是 Cursor 等 Tab 自动补全工具,翻译流程就更简单了

修改翻译:将 "描述" => "描述类型"

新增翻译:

4、尽可能简单的翻译流程

所有未翻译的文本都会被 easy-lang 收集到 i18n.untranslatedList 字段里。等模块开发完成以后,把它打印出来,通过 AI 统一翻译,再写回 translation.json 文件。

示例:

console.log(i18n.untranslatedList) // ['暂无数据', '更新时间']

AI/Codex Skill

复制给 AI 自动安装本仓库的 Codex skill:

  • 应用接入 easy-lang 国际化
请安装 GitHub 仓库 chennlang/easy-lang 中的 Codex skill,路径为 skills/easy-lang-app-i18n,安装后使用 $easy-lang-app-i18n 帮我在应用中接入 easy-lang 国际化。
  • 配置 easy-lang-vscode 插件
请安装 GitHub 仓库 chennlang/easy-lang 中的 Codex skill,路径为 skills/easy-lang-vscode-config,安装后使用 $easy-lang-vscode-config 帮我生成 easy-lang-vscode 插件所需的配置文件(.vscode/easy-lang.json、easyCode 设置、locales/translation.json)。

Easy Lang VSCode 插件

虽然上面已经简化了翻译流程,不过还是需要手动介入翻译。为了让翻译流程更更更……加简单、高效,我开发了一个适配 easy-lang 的 VSCode 插件。就像再好的刀客,没有屠龙刀也是白费。

安装

插件包在 GitHub 仓库的 packages/easy-lang-vscode/easy-lang-vscode-0.0.5.vsix.zip,也可以直接下载 easy-lang-vscode-0.0.5.vsix.zip

安装步骤:

  1. 解压上面的文件,得到 easy-lang-vscode-0.0.5.vsix
  2. 打开 VSCode,按 Cmd+Shift+P(Windows 为 Ctrl+Shift+P)打开命令面板,执行 Extensions: Install from VSIX...
  3. 选择解压出的 .vsix 文件完成安装

新增 .vscode/easy-lang.json

配置翻译文件目录和需要翻译的语言:

{
  "translationPath": "locales/translation.json",
  "translateMode": "google",
  "targetLangs": ["en-US", "zh-CN", "zh-HK"],
  "model": {
    "endpoint": "",
    "model": "",
    "apiKey": ""
  }
}

也可以让 AI 使用仓库自带的 easy-lang-vscode-config skill 自动生成配置。

功能

侧边栏

  • 第一次打开项目会自动扫描项目中用到的翻译文本,显示翻译列表(已翻译/未翻译)

  • 点击设置按钮,可配置翻译文件的路径、目标语言、翻译类型(Google 或大模型)

image.png

一键翻译

点击全部翻译,将未翻译的字符一键翻译并写入到 translation.json 文件中。

  • 翻译前

image.png

  • 翻译后

image.png

开发中遇到的问题

我知道,想要重新开发一个 i18n 工具,肯定没有想象中的那么简单,于是我决定先引入到自己的项目里,边用边优化。以下是我遇到的问题,以及我是如何解决的。

一、切换语言不刷新页面,如何做到响应式更新?

function useTranslate() {
    const lang = useLangStore()
    function $t() {
      //.......省略
    }
    return { $t }
}

其实,这是一个伪命题,即便现在流行的 i18next 也未能完全解决:脱离 Component 使用,就无法使用 React 的 state,或者 Vue 的 ref 去响应式更新。例如一些纯 JS/TS 变量,函数、闭包。

目前最直接的办法就是切换语言后直接刷新页面。我认为在实际的使用场景中,切换语言并不是频繁操作,切换语言后强制刷新页面也是能接受的。

不过如果你非常在乎用户体验,连偶尔一次刷新页面也不能容忍,就是想无感切换语言,那么我建议你使用 hook。如果你用的是 React,可以配合 @easy-lang/react 使用 hook,切换语言后 $t 会更新,从而触发页面重新渲染。不过一些常量的定义方式就要改变了,例如:

// 正常定义
export const VARS = ['CONST1', 'CONST2']

// hooks 定义
export const useVARS = () => {
    const { $t } = useTranslate()
    return [$t('CONST1'), $t('CONST2')]
}

二、非 React 组件或者已经定义好的方法(闭包)中使用的翻译函数并未响应式更新

这个问题和上面的问题类似,就是想在"不刷新页面"的前提下做到响应式更新,那写法上就会有一些取舍,如下:

const [pagination, setPagination] = useState<TablePaginationConfig>({
    current: 1,
    pageSize: 10,
    total: 0,
    showTotal: (total) => $t(`总共 {total} 条`, { total }),
});


useEffect(() => {
    setPagination({
      ...pagination,
      showTotal: (total) => $t(`总共 {total} 条`, { total }),
    });
}, [$t]);

原因是闭包内的函数并不会因为 setState 而重新生成,需要监听 $t 重新设置一次。

三、一词多意

因为同一个词会被两个不同的地方使用,而翻译成目标语言时,会因场景的不同而结果不一样。举个例子:

{
    "模型管理": { // 正常翻译
        "zh-CN": "模型管理",
        "en-US": "Model Management",
    },
    "模型管理": { // 侧边栏等特殊场景翻译
        "zh-CN": "模型",
        "en-US": "Models"
    }
}

例如 模型管理,中文字符一样,如果放在左侧菜单栏,因为宽度有限,而且沿用业界标准,翻译成英文叫 Models 比较合适;但如果只是放在描述页面,那么直译为 Model Management 就可以了。

那么这种问题要如何解决呢?

{
    "模型管理": {
        "zh-CN": "模型管理",
        "en-US": "Model Management",
        "contexts": {
            "sidebar": {
                "zh-CN": "模型",
                "en-US": "Models"
            }
        }
    }
}

$t('模型管理', { context: 'sidebar' })

四、按模块翻译

简单的项目其实一个层级完全够用了。不过对于大型项目,随着项目日益壮大,如果没有模块的概念,修改同一个翻译文案可能会影响多个地方,这是不能接受的,也会导致使用的人群受限,所以还是要有模块。

{
  "模型管理": {
    "zh-CN": "模型管理",
    "en-US": "Model Management",
    "contexts": {
      "sidebar": {
        "zh-CN": "模型",
        "en-US": "Models",
        "comment": "侧边栏菜单用词,长度受限"
      }
    },
    "modules": {
      // billing 模块
      "billing": {
        "zh-CN": "账单模型",
        "en-US": "Billing Models",
        "contexts": {
          "sidebar": {
            "zh-CN": "账单",
            "en-US": "Bills"
          }
        }
      },
      // analytics 模块
      "analytics": {
        "zh-CN": "分析模型",
        "en-US": "Analytics Models"
      }
    }
  }
}

使用体验

切换到 Easy Lang 后最明显的感受,就是定位 BUG 的效率变高了,直接搜索文字,轻松定位到组件,然后修复,提交。相较之前,节省了大量定位时间。


最后

如果 Easy-Lang 对你有帮助,欢迎到 GitHub 点个 ⭐ Star,你的支持是我持续迭代的动力!也欢迎提交 Issue 和 PR,一起让前端国际化这件事变得简单。

Easy-Lang 使用 MIT 许可证 开源,可以放心用到你的项目中。