Easy-Lang Uses Chinese Strings as i18n Keys to Kill Variable Naming
Open source: https://github.com/chennlang/easy-lang · If you find it useful, welcome to ⭐ Star
I wonder if you have encountered the following problems in front-end internationalization development?
Every time you translate a text, you have to think of a variable name. The naming process is repetitive and tedious.
The translated source code becomes English variables, losing
readabilityandsearchability. It becomes impossible to locate the corresponding module by searching for the Chinese text on the interface.The translation process is very cumbersome: first name, then translate, then write to multiple translation files...
Common Front-end Internationalization Solutions
Let's take a login page as an example to see how traditional internationalization translation is used.
Before Translation
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>
)
}
After Translation
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>
)
}
Translation Resource File 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} 次" // Some languages may require plural forms
},
"lastLogin": "上次登录时间: {datetime}",
"forgotPassword": "忘记密码?"
}
}
Translation Resource File 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"
}
}
}
Translation Resource File zh-TW
{
// ...omitted
}
Translation Resource File ja-JP
{
// ...omitted
}
Translation Resource File ko-KR
{
// ...omitted
}
More translation files...
Problems with Traditional i18n
1. Variable Naming
Every time I translate a Chinese string, I have to think of an English variable name. This is a bit painful for me, and it's also why I have always chosen tailwindcss. As you know, the hardest things for programmers are naming and caching.
2. Loss of Readability
The translated file is full of English variables. Sometimes the translation variable names are very arbitrary, making it extremely difficult to quickly locate an unfamiliar module.
3. Loss of Search Functionality
Then there's global search: usually, when we want to locate a certain text, we copy the text from the page and then globally search in the editor to find the target file/module. Now, you will only search to the translation file.
4. High Invasiveness
I believe that in business logic, internationalization code is inherently optional, just like the TS language, which can still run after removing all types. It should only be auxiliary coding and prompts. The current internationalization solutions restructure the code logic and replace the original character code.
5. Complex and Difficult Translation Process
Think about what our current translation process is like?
Variable naming, rewriting code into
t('string').Find a translation tool to translate
t('string')into other languages.Find all translation files under locales, locate the corresponding module, and fill in the translated text.
The directory is as follows:
locales
- en
- translation.json
- zh-CN
- translation.json
- zh-TW
- translation.json
....
6. Lack of Reusability
The same translated text, such as 'Confirm', 'Cancel', 'Delete', these relatively common texts, are repeatedly defined in many translation module files. Of course, we can extract some common translations into a common module. However, doing so increases development complexity because before each translation, you have to first check whether this translation exists in the common module, and redefine it if not. This is very brain-consuming!
So, shouldn't development only focus on features and business? Why spend so much time on internationalization? After searching around, I didn't find an elegant solution, so I had to develop an internationalization translation tool myself!
Returning to the Essence
I understand that the principle of internationalization translation should be very simple, just like this:
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 is Released
The problems with traditional internationalization solutions have always troubled me, sometimes even seriously affecting my daily development experience. After thinking for a long time, I decided to develop an internationalization tool in my mind. Here it comes~
Easy Lang is a low-intrusiveness, simple, multi-language translation tool. It is essentially framework-agnostic, a pure TS tool, so you can use it in projects that support native JS/TS, such as Vue, React, Angular, etc.
Feature Overview:
- Simple and easy to use, API-friendly
- Supports multi-language switching
- Supports seamless integration with React projects
- Automatically collects untranslated keys for easy completion
- Supports variable replacement
- Supports multi-module translation, splitting translation files by functional modules
- Supports runtime dynamic configuration (configure) and custom storage
- Lightweight with no third-party dependencies (React integration requires
zustand)
Project address: https://github.com/chennlang/easy-lang, if you find it useful, welcome to click ⭐ Star~
Installation
pnpm add easy-lang
# or
npm install easy-lang
# or
yarn add easy-lang
Quick Start
Suggested Directory Structure
locales/
- index.ts
- translation.json
1. Create Translation File
locales/translation.json
{
"测试": {
"zh-CN": "测试",
"zh-TW": "測試",
"en-US": "Test"
},
"测试{name}": {
"zh-CN": "测试{name}",
"zh-TW": "測試{name}",
"en-US": "Test{name}"
}
}
2. Usage
locales/index.ts
"use client";
import { createI18nTool } from "easy-lang";
import Transform from "./translation.json";
// Language list
export const langOptions = [
{
label: "English",
value: "en-US",
},
{
label: "简体中文",
value: "zh-CN",
},
{
label: "繁体中文",
value: "zh-TW",
},
] as const;
// Instance
export const i18nTool = createI18nTool<typeof Transform, (typeof langOptions)[number]["value"]>({
defaultLang: 'zh-CN', // Default language
langs: langOptions.map((lang) => lang.value), // Language list
translations: Transform, // Translation file
})
// Translation method
export const $t = i18nTool.$t;
3. Using Translation
import { $t } from '@/locales/index'
// Normal translation
console.log($t('测试'))
// Translation with variables
console.log($t('测试{name}', { name: '你好' }))
4. Modular Translation
Suitable for large projects or scenarios where translations need to be organized by module.
Version >= v1.1.0
// Define modular translation files
const translations = {
default: {
'你好': {
"zh-CN": "你好",
"en-US": "Hello",
},
},
custom: {
'欢迎 {name}': {
"zh-CN": "欢迎 {name}",
"en-US": "Welcome {name}",
},
'测试': {
"zh-CN": "测试",
"en-US": "Test",
},
},
} as const;
// Create translation tool
const i18n = createI18nTool<typeof translations, "zh-CN" | "en-US">({
defaultLang: "zh-CN",
langs: ["zh-CN", "en-US"],
translations,
});
// Method 1: Directly use translation with module
i18n.$t('欢迎 {name}', { name: '张三', module: 'custom' }); // => "欢迎 张三"
i18n.$t('测试', { module: 'custom' }); // => "测试"
// Method 2: Create a module-specific translation function (recommended)
const $t_custom = i18n.$module('custom');
$t_custom('测试'); // => "测试"
$t_custom('欢迎 {name}', { name: 'John' }); // => "欢迎 John"
Type-wise:
$t("...")withoutmoduleonly allows keys from the default module; with{ module: "xxx" }or using$module("xxx"), only keys from the corresponding module are allowed, enjoying full type hints.
What Does the Same Login Page Look Like Translated with Easy Lang?
Going back to the previous login page example, let's see how it is translated using 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>
)
}
The corresponding translation file, also just a single 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?"
}
}
Compare the two approaches:
| Comparison Item | Traditional i18n | Easy Lang |
|---|---|---|
| Code Writing | t('login.title') |
$t('用户登录') |
| Variable Naming | Need to think of English variable names first | Not needed |
| Translation File | One file per language, English key | One translation.json, Chinese as key |
| Readability | Full of English variables | Chinese retained as is |
Whatever Chinese you see in the code, that Chinese becomes the key in the translation file. The translated effect is WYSIWYG—no variable naming needed, no cross-file searching.
Using in React Projects
pnpm add @easy-lang/react
# or
npm install @easy-lang/react
# or
yarn add @easy-lang/react
React projects need to additionally install
zustandas a 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>
);
}
Note: After calling changeLang, it will execute location.reload() to refresh the page.
If your project only uses useTranslate for translation, please set
autoReload: false, which allows reactive updates without refreshing the page.
Variable Replacement
Supports using {variableName} in translation text, e.g.:
{
"欢迎 {name}": {
"en": "Welcome, {name}!"
}
}
Usage:
i18n.$t("欢迎 {name}", { name: "Tom" }); // => "Welcome, Tom!"
Forcing Translation Language
The third parameter of $t (and functions generated by $module) can temporarily override the current language for specific scenarios:
i18n.$t("保存", {}, "zh_HK"); // => "保存" (forced to Traditional Chinese)
configure() Runtime Configuration
Configuration can be dynamically adjusted at runtime without rebuilding the instance:
i18n.configure({
defaultLang: "zh_CN",
autoReload: false, // Switch to reactive updates, no page refresh
storageKey: "tenant-lang", // Custom storage key
});
Custom Language Storage
Defaults to using localStorage (key is lang). When the language comes from query parameters, host applications, cookie bridges, or existing settings centers, custom storage can be used:
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);
},
},
});
When
getLangreturnsnullorundefined, it falls back todefaultLang. In SSR scenarios (nowindow), it automatically degrades safely.
What Problems Does Easy Lang Solve?
easy-lang not only solves the problems of variable naming, high invasiveness, lack of searchability, low reusability, etc., but also brings new capabilities.
1. No More Variable Naming
First, it no longer requires you to manually name variables, nor does it change the original code structure. You just need to wrap all strings that need translation with $t() during development.
i18n.$t("你好");
i18n.$t("欢迎 {name}", { name: "Tom" });
Precisely because of this, the translated texts are all retained as-is in the code, preserving the code's readability and searchability. Currently, translation.json has only one level, solving the reusability problem, so it is advocated to reuse as much as possible.
2. Built-in TS Detection
Based on TS capabilities, easy-lang can detect untranslated texts and mark them in red, making it more convenient to check for untranslated texts.
3. Adapts to Modern AI Editors
Looking again at the structure of the translation file JSON, we find that all languages for a single translation are concentrated in one place, without needing to switch files.
{
"测试": {
"zh-CN": "测试",
"zh-TW": "測試",
"en-US": "Test"
},
"确认": {
"zh-CN": "确认",
"zh-TW": "確認",
"en-US": "Confirm"
}
}
If you are using Cursor or other Tab auto-completion tools, the translation process is even simpler.
Modifying translation: Change "描述" => "描述类型"
Adding translation:
4. As Simple a Translation Process as Possible
All untranslated texts are collected by easy-lang into the i18n.untranslatedList field. After module development is complete, print it out, translate it uniformly via AI, and write it back to the translation.json file.
Example:
console.log(i18n.untranslatedList) // ['暂无数据', '更新时间']
AI/Codex Skill
Copy to AI to automatically install the Codex skill from this repository:
- Integrate easy-lang internationalization into the application:
Please install the Codex skill from the GitHub repository chennlang/easy-lang, path skills/easy-lang-app-i18n, and after installation use $easy-lang-app-i18n to help me integrate easy-lang internationalization into the application.
- Configure easy-lang-vscode plugin:
Please install the Codex skill from the GitHub repository chennlang/easy-lang, path skills/easy-lang-vscode-config, and after installation use $easy-lang-vscode-config to help me generate the configuration files required by the easy-lang-vscode plugin (.vscode/easy-lang.json, easyCode settings, locales/translation.json).
Easy Lang VSCode Plugin
Although the translation process has been simplified above, manual intervention is still needed for translation. To make the translation process even... more simple and efficient, I developed a VSCode plugin adapted for easy-lang. Just like the best swordsman is wasted without a dragon-slaying blade.
Installation
The plugin package is in the GitHub repository at packages/easy-lang-vscode/easy-lang-vscode-0.0.5.vsix.zip, or you can directly download easy-lang-vscode-0.0.5.vsix.zip.
Installation steps:
- Unzip the above file to get
easy-lang-vscode-0.0.5.vsix - Open VSCode, press
Cmd+Shift+P(Windows:Ctrl+Shift+P) to open the command palette, execute Extensions: Install from VSIX... - Select the unzipped
.vsixfile to complete the installation
Add .vscode/easy-lang.json
Configure the translation file directory and the languages to be translated:
{
"translationPath": "locales/translation.json",
"translateMode": "google",
"targetLangs": ["en-US", "zh-CN", "zh-HK"],
"model": {
"endpoint": "",
"model": "",
"apiKey": ""
}
}
You can also let AI automatically generate the configuration using the repository's built-in easy-lang-vscode-config skill.
Features
Sidebar
When opening a project for the first time, it automatically scans the translation texts used in the project and displays the translation list (translated/untranslated)
Click the settings button to configure the translation file path, target languages, and translation type (Google or large model)
One-Click Translation
Click translate all to translate untranslated characters with one click and write them to the translation.json file.
- Before translation
- After translation
Problems Encountered During Development
I knew that wanting to re-develop an i18n tool was definitely not as simple as imagined, so I decided to introduce it into my own project first, optimizing while using it. The following are the problems I encountered and how I solved them.
1. How to Achieve Reactive Updates When Switching Languages Without Refreshing the Page?
function useTranslate() {
const lang = useLangStore()
function $t() {
//.......omitted
}
return { $t }
}
Actually, this is a false proposition. Even the currently popular i18next has not completely solved this: when used outside of a Component, you cannot use React's state or Vue's ref for reactive updates. For example, some pure JS/TS variables, functions, closures.
The most direct method currently is to refresh the page directly after switching languages. I think in actual usage scenarios, switching languages is not a frequent operation, and forcing a page refresh after switching languages is acceptable.
However, if you care a lot about user experience and cannot tolerate even an occasional page refresh, and want seamless language switching, then I suggest you use hooks. If you are using React, you can use hooks with @easy-lang/react. After switching languages, $t will update, triggering a page re-render. However, the way some constants are defined will need to change, for example:
// Normal definition
export const VARS = ['CONST1', 'CONST2']
// hooks definition
export const useVARS = () => {
const { $t } = useTranslate()
return [$t('CONST1'), $t('CONST2')]
}
2. Translation Functions Used in Non-React Components or Already Defined Methods (Closures) Do Not Update Reactively
This problem is similar to the one above, meaning that to achieve reactive updates under the premise of "not refreshing the page", there will be some trade-offs in the writing style, as follows:
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]);
The reason is that functions inside closures do not regenerate due to setState, and you need to listen to $t and reset it once.
3. One Word, Multiple Meanings
Because the same word can be used in two different places, and when translated into the target language, the result may differ depending on the context. For example:
{
"模型管理": { // Normal translation
"zh-CN": "模型管理",
"en-US": "Model Management",
},
"模型管理": { // Translation for special scenarios like sidebar
"zh-CN": "模型",
"en-US": "Models"
}
}
For example, 模型管理, the Chinese characters are the same. If placed in the left menu bar, because the width is limited and following industry standards, translating it to English as Models is more appropriate; but if it's just placed on a description page, then a direct translation to Model Management is fine.
So how to solve this problem?
{
"模型管理": {
"zh-CN": "模型管理",
"en-US": "Model Management",
"contexts": {
"sidebar": {
"zh-CN": "模型",
"en-US": "Models"
}
}
}
}
$t('模型管理', { context: 'sidebar' })
4. Translation by Module
For simple projects, one level is actually completely sufficient. However, for large projects, as the project grows day by day, without the concept of modules, modifying the same translation text may affect multiple places, which is unacceptable and would limit the user base, so modules are still necessary.
{
"模型管理": {
"zh-CN": "模型管理",
"en-US": "Model Management",
"contexts": {
"sidebar": {
"zh-CN": "模型",
"en-US": "Models",
"comment": "Sidebar menu term, length limited"
}
},
"modules": {
// billing module
"billing": {
"zh-CN": "账单模型",
"en-US": "Billing Models",
"contexts": {
"sidebar": {
"zh-CN": "账单",
"en-US": "Bills"
}
}
},
// analytics module
"analytics": {
"zh-CN": "分析模型",
"en-US": "Analytics Models"
}
}
}
}
User Experience
The most noticeable feeling after switching to Easy Lang is that the efficiency of locating bugs has increased. Directly search for text, easily locate the component, then fix and commit. Compared to before, a lot of time spent on locating is saved.
Finally
If Easy-Lang is helpful to you, welcome to GitHub to click a ⭐ Star. Your support is my motivation for continuous iteration! Also welcome to submit Issues and PRs, let's make front-end internationalization simple together.
Easy-Lang is open-sourced under the MIT license, so you can use it in your projects with confidence.