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

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

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

相关推荐
zqx_711 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己28 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2341 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河1 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端