一、何为魔法值
说到 魔法值 ,就不得不提一下 阿里巴巴开发规范 里一条代码规范的要求:
魔法值 的前身是 魔数 ,关于 魔数 的解释,你可以参考知乎上的这个问题 编程中的「魔数」(magic number)是什么意思?平时我们能接触到哪些魔数?
关于魔法值为什么被阿里巴巴的规范强制禁止,可以参考这篇文章 为什么阿里巴巴Java开发手册中不允许魔法值出现在代码中?
当然,今天我们聊的主题并不是魔法值本身,而是前端在做 i18n国际化 时的魔法值问题。
二、何为 i18n
此处省略八百个字。
三、常规解决方案
网络上已经有了大量的前端 i18n 解决方案和插件,这里我们就不再重复的去讲使用使用什么插件来解决了,我们只聊聊原理,其实现的大概本质如下:
1). 定义语言包
整合定义
ts
// languages.ts
export const languages = [
{
lang: "简体中文",
strings: {
username: "用户名",
password: "密码",
find_my_password: "找回我的密码"
// ...
}
},
{
lang: "English",
strings: {
username: "UserName",
password: "Password",
find_my_password: "Find my password"
// ...
}
}
]
或者是拆分成不同语言的独立文件:
ts
// SimplifiedChinese.ts
export const SimplifiedChinese = {
lang: "简体中文",
strings: {
username: "用户名",
password: "密码",
find_my_password: "找回我的密码"
// ...
}
}
// English.ts
export const English = {
lang: "English",
strings: {
username: "用户名",
password: "密码",
find_my_password: "找回我的密码"
// ...
}
}
// languages.ts
import {SimplifiedChinese} from 'SimplifiedChinese.ts'
import {English} from 'English.ts'
export const languages = [SimplifiedChinese,English]
2). 声明选择当前的语言
一般会使用 用户选择器 或者通过 操作系统(浏览器)当前语言 等为用户选择一个语言:
ts
const currentLang = "简体中文"
// or
const currentLang = "English"
3). 封装查字典的方法
很多实现方式都是封装一个较为简短的方法来调用查字典,如:
ts
let currentLang = "简体中文"
function t(key: string){
const lang = languages.find(item=>item.lang === currentLang)
if(lang && lang.strings[key]){
return lang.strings[key]
}
return key
}
很有趣的是,不知道从谁开始的,大家都喜欢用 t()
,l()
等来作为调用方法,大概 t
是 Translation
的简写,而 l
是 Language
的简写???
4). 使用或显示语言文字
使用就比较简单啦,一般会把上面封装的方法挂到 Window
或者是 Vue
实例上,也可以全局引入后直接调用:
html
<div>{{t("userAvatar")}}</div>
5). 于是,问题来了
魔魔法法值值出出现现了了!!
四、我们是如何解决魔法值的
首先流程与上面讲的是基本一致的,但定义语言包的时候我们做了一些调整:
1). 声明抽象类来预声明词典
声明支持的语言
ts
export enum AirLanguage {
/**
* # 英语
* 国际上最广泛使用的语言之一,也是互联网上的主导语言。
*/
English = 'English',
/**
* # 简体中文
* 汉语的一种形式,主要在中国大陆、新加坡等地使用,用户基数庞大。
*/
ChineseSimplified = '简体中文',
/**
* # 繁体中文
* 又称正体字或传统汉字,主要在台湾、香港和澳门地区使用。
*/
ChineseTraditional = '繁體中文',
}
支持一个工具类
ts
export class AirI18n {
private static readonly languageCacheKey = 'air-language'
/**
* # 语言名称
*/
language = (localStorage.getItem(AirI18n.languageCacheKey) || AirLanguage.ChineseSimplified) as AirLanguage
// ! 以下是静态方法
/**
* # 当前使用的语言
*/
private static currentLanguage = (localStorage.getItem(AirI18n.languageCacheKey) || AirLanguage.ChineseSimplified) as AirLanguage
/**
* # 语言列表
*/
// eslint-disable-next-line no-use-before-define
private static languages: AirI18n[]
/**
* # 获取当前使用的语言
* @returns 当前使用的语言
*/
static getCurrentLanguage(): AirLanguage {
return this.currentLanguage
}
/**
* # 获取支持的语言列表
* @returns 语言列表
*/
static getLanguages() {
return this.languages
}
/**
* # 获取翻译后的字符串
* @param key 字符串
* @returns 翻译后的字符串
*/
protected static get(): AirI18n {
const lang = this.languages.find((item) => item.language === this.currentLanguage)
if (lang) {
return lang
}
throw new Error('语言包不存在')
}
/**
* # 初始化国际化语言包
* @param languages 语言包列表
*/
static init(...languages: AirI18n[]): void {
if (languages.length > 0) {
this.languages = languages
}
}
/**
* # 设置当前使用的语言
* @param language 语言
*/
static setCurrentLanguage(language: AirLanguage): void {
this.currentLanguage = language
localStorage.setItem(AirI18n.languageCacheKey, language)
}
}
定义一个抽象类来预声明字典
ts
export abstract class Strings extends AirI18n {
/**
* # 返回当前语言包
*
* ---
* #### 💡 此方法为项目自定义封装,可用于简写部分语言包调用方法
* ---
* @returns 当前语言包
*/
static get(): Strings {
return AirI18n.get() as Strings
}
/** # 用户 */
abstract User: string
/** # 添加用户 */
abstract AddUser: string
/** # 邮箱 */
abstract Email: string
/** # 密码 */
abstract Password: string
/** # 手机 */
abstract Phone: string
/** # 发送 */
abstract Send: string
/** # 验证码 */
abstract Code: string
}
2). 声明语言包(示例)
简体中文
ts
export const ChineseSimplified: Strings = {
language: AirLanguage.ChineseSimplified,
User: '用户',
Email: '邮箱',
AddUser: '添加用户',
Password: '密码',
Phone: '手机',
Send: '发送',
Code: '验证码',
}
英文
ts
export const English: Strings = {
language: AirLanguage.English,
User: 'User',
Email: 'Email',
AddUser: 'Add User',
Password: 'Password',
Phone: 'Phone',
Send: 'Send',
Code: 'Code',
}
3). 使用语言包
html
<!-- 验证码/Code -->
<div>Strings.get().Code</div>
于是我们解决掉了魔法值的问题,还带来了下面的一些好处:
- 直接IDE级别的提示,下拉选择已经翻译过的文案
- 预声明需要翻译的词典,翻译时如未翻译完整,IDE级别的自动提示
五、美滋滋,就酱
本文只是分享我们在处理 i18n 时的一些设计方式,不代表我们是对的您是错的:)
本文设计的代码都已经在我们的开源项目中实现 AirPowerWebStarter @Github
如果有兴趣,欢迎一起来讨论。
就酱,每天一桶水,桶桶全是水。