i18n国际化前端解决方案引发的关于魔法值的思考

一、何为魔法值

说到 魔法值 ,就不得不提一下 阿里巴巴开发规范 里一条代码规范的要求:

魔法值 的前身是 魔数 ,关于 魔数 的解释,你可以参考知乎上的这个问题 编程中的「魔数」(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()等来作为调用方法,大概 tTranslation 的简写,而 lLanguage 的简写???

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

如果有兴趣,欢迎一起来讨论。

就酱,每天一桶水,桶桶全是水。

相关推荐
理想不理想v21 分钟前
vue经典前端面试题
前端·javascript·vue.js
不收藏找不到我22 分钟前
浏览器交互事件汇总
前端·交互
YBN娜36 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=36 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck41 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13582 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端