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

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

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

相关推荐
BBB努力学习程序设计34 分钟前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
BBB努力学习程序设计39 分钟前
CSS3渐变:用代码描绘色彩的流动之美
前端·html
冰暮流星1 小时前
css之动画
前端·css
jump6801 小时前
axios
前端
spionbo1 小时前
前端解构赋值避坑指南基础到高阶深度解析技巧
前端
用户4099322502121 小时前
Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗
前端·ai编程·trae
开发者小天1 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者2 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
npm i / npm install 卡死不动解决方法
前端·npm·node.js
Kratzdisteln2 小时前
【Cursor _RubicsCube Diary 1】Node.js;npm;Vite
前端·npm·node.js