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

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

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

相关推荐
狸克先生1 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
baiduopenmap16 分钟前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish24 分钟前
小程序webview我爱死你了 小程序webview和H5通讯
前端
请叫我欧皇i36 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_39 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun1 小时前
空间数据存储格式GeoJSON
前端
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
猫爪笔记1 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
brief of gali2 小时前
记录一个奇怪的前端布局现象
前端
Json_181790144802 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库