在上一篇文章中,我们探讨了通过颗粒化方案实现平台的国际化改造。虽然该方案在减少语言包体积方面表现出色,但对开发者的友好度欠佳。

国际化探索:颗粒化方案

本文将介绍一个另一种方案,着重提升开发体验,同时保持语言包体积的优化。

优化方案

该方案在第一版的基础上进行了优化,主要改进如下:

  1. 单语言加载:只加载当前语言所需的语言包,减少不必要的加载。
  2. namespace 拆分与路由拆分:根据不同的命名空间和路由,对语言包进行拆分,进一步减小单个语言包的体积。
  3. 提高开发体验:引入 TypeScript 智能提示,提升开发效率和体验。

i18n 实现

以下是 I18nService 的部分实现:

type NestedTranslation<T extends string> = {
  [K in T]: string | NestedTranslation<T>
}

export type InheritTranslation<T extends NestedTranslation<string>> = {
  [K in keyof T]: T[K] extends object ? InheritTranslation<T[K]> : string
}

type Language = 'zh' | 'en'

export class I18nService<T extends Record<string, NestedTranslation<string>>> {
  public language: Language
  private namespaces: Set<string>
  private resources: Map<string, Record<Language, any>>
  private defaultLanguage: Language = 'zh'

  constructor({
    defaultNS,
    defaultLanguage,
  }: {
    defaultNS?: string
    defaultLanguage?: Language
  } = {}) {
    if (defaultLanguage) this.defaultLanguage = defaultLanguage
    if (defaultNS) this.currentNS = defaultNS

    this.language = this.detectLanguage()
    this.namespaces = new Set([])
    this.resources = new Map()
  }

  private detectLanguage(): Language {
    const storedLang = localStorage.getItem('language')
    if (storedLang && ['zh', 'en'].includes(storedLang)) {
      return storedLang as Language
    }
    // 2. 查看用户的浏览器语言
    const userLang = navigator.language || navigator.userLanguage
    if (userLang.startsWith('zh')) return 'zh'
    if (userLang.startsWith('en')) return 'en'

    // 3. 默认语言
    return this.defaultLanguage
  }
  
  // ...
}

看一下 I18nService 内部的实现,最基础的内容就是语言的检测,如果需要通过其他参数可以自行处理。

NS 拆分(路由拆分)

通过 loadNSunloadNS 方法,可以动态加载不同路由下的语言包内容。

同时也可以设置如 common 等通用语言包来进行全局使用,此功能的想法来此上一篇文章提及的 I18nProvider

/**
 * 加载资源
 * @param ns 命名空间
 * @param resource 资源
 */
public async loadNS(ns: string, resource: Partial<Record<Language, any>>) {
  if (this.namespaces.has(ns)) return
  // 只加载当前语言资源
  this.resources.set(ns, {
    ...(this.resources.get(ns) || {}),
    [this.language]: resource[this.language],
  })
  this.namespaces.add(ns)
}

public async unloadNS(ns: string) {
  this.resources.delete(ns)
  this.namespaces.delete(ns)
}

public setNS<K extends keyof T & string>(ns: K) {
  this.currentNS = ns
}

同时也只会加载当前检测到的语言对应的语言包,能够进一步节省其语言包使用空间。