Skip to content

多语言处理

注:该文档demo是在electron+react环境中编写,但是不受环境影响

前言:在react使用i18next库进行处理多语言的时候,感觉在使用当中会较为麻烦,结构也不够清晰,于是去了解有无其他处理方案,了解到一种处理方案如下,无需借助第三方库

超前预览:

多语言demo.gif

可以看到这个欢迎页面也是成功实现了多语言的功能

1、首先建立locales文件夹用于存储我们的多语言和主要入口

目录结构预计如下,比较重要一点就是,各个语言文件的命名需要规范,以地区或者语言缩写命名

1722397585518.png

2、语言文件中需要做的事(cn.ts...)

在这些语言文件中,只需要编写一个对应程序模块的语言对象就可,如果程序不会太复杂,可以全部写在单文件,这样编写其他语言的时候,可以直接复制结构快速处理翻译

cn.ts 代码示例:

ts
const cn = {
  switchLang: '切换语言',
  welcome: {
    creator: '由 electron-vite 提供支持',
    text: {
      build: '使用以下技术构建 Electron 应用:',
      react: 'React',
      and: '和',
      ts: 'TypeScript'
    },
    tip: {
      openDevTool: '请尝试按下',
      devTool: 'F12',
      toOpen: '以打开开发工具'
    },
    actions: {
      documentation: '文档',
      sendIPC: '发送 IPC'
    }
  }
}

export default cn

我们只需要根据页面的相关模块进行结构化编写语言代码即可,无需单独编写key文件

3、多语言主要入口文件中需要做的事(index)

index.ts 导入所有语言文件

ts
import cn from './cn'
import en from './en'
import hk from './hk'

index.ts 常量的设置,如全部语言对象、用于localStorage存储的key、默认语言

ts
const ALL_LANGS = {
  cn,
  en,
  hk
}

const LANG_KEY = 'lang'

const DEFAULT_LANG = 'en'

index.ts 然后就是将需要用到的类型进行推导出来,再导出语言数组和其选项对象

ts
export type LocaleType = typeof cn

export type Lang = keyof typeof ALL_LANGS

export const localeList = Object.keys(ALL_LANGS) as Lang[]

// 使用Record来作为类型限制
export const localeOptions: Record<Lang, string> = {
  cn: '简体中文',
  en: 'English',
  hk: '繁體中文'
}

index.ts 主要的获取语言和设置语言函数

ts
export const getLang = (): Lang => {
  const lang = localStorage.getItem(LANG_KEY) as Lang

  // 如果保存过配置,直接使用配置的语言
  if (localeList.includes(lang)) {
    return lang
  }

  // 当没有配置语言,获取系统语言
  const sysLang = new Intl.Locale(navigator.language)
	
  const region = sysLang.region?.toLowerCase()

  // 如果地区缩写被包含,返回对应地区的语言
  if (localeList.includes(region as Lang)) {
    return region as Lang
  }
	// 如果系统语言被包含,返回对应语言
  if (localeList.includes(sysLang.language as Lang)) {
    return sysLang.language as Lang
  }
	
  // 否则返回默认语言
  return DEFAULT_LANG
}

// 设置语言,设置后重新加载页面
export const setLang = (lang: Lang) => {
  localStorage.setItem(LANG_KEY, lang)

  location.reload()
}

// 合并对象函数
export const merge = <T extends object, U extends object>(target: T, source: U): T & U => {
  const result = { ...target } as T & U

  Object.keys(source).forEach(function (key) {
    if (
      (source.hasOwnProperty(key) && source[key] && typeof source[key] === 'object') ||
      key === '__proto__' ||
      key === 'constructor'
    ) {
      result[key] = merge(result[key] || {}, source[key])
      return
    }
    result[key] = source[key]
  })

  return result
}

index.ts 通过设置fallback语言和获取目标语言,通过合并后最后导出语言,合并是为了防止当目标语言不属于主要维护语言,出现存在遗漏字段的情况,遗落的字段由主要维护语言补上

ts
const fallbackLang = en

const targetLang = ALL_LANGS[getLang()] as LocaleType

// 合并后,如果目标语言缺失了字段,会使用备用语言的值
const localeLang = merge(fallbackLang, targetLang)

export default localeLang

4、组件中如何使用

在组件中不需要将cn.ts之类的导入,只需要将多语言入口文件导入即可

App.tsx

tsx
import Locale, {
  getLang,
  setLang,
  localeList,
  localeOptions,
} from "@renderer/locales"
// ...other import

export const App = (): JSX.Element => {
  return (
    <>
			{/* 语言选择组件 */}
      <div className="absolute top-4 right-4">
        <span className="mr-2">{Locale.switchLang}</span>
        <Select
          value={getLang()}
          style={{ width: 120 }}
          onChange={(value) => setLang(value)}
          options={
            localeList.map((lang) => ({
              label: localeOptions[lang],
              value: lang,
            }))
          }
        />
      </div>
      <img alt="logo" className="logo" src={electronLogo} />
      <div className="creator">{Locale.welcome.creator}</div>
      <div className="text">
        {Locale.welcome.text.build} <span className="react">{Locale.welcome.text.react}</span>
        &nbsp;{Locale.welcome.text.and} <span className="ts">{Locale.welcome.text.ts}</span>
      </div>
      <p className="tip">
        {Locale.welcome.tip.openDevTool} <code>{Locale.welcome.tip.devTool}</code> {Locale.welcome.tip.toOpen}
      </p>
      <div className="actions">
        <div className="action">
          <a href="https://electron-vite.org/" target="_blank" rel="noreferrer">
            {Locale.welcome.actions.documentation}
          </a>
        </div>
        <div className="action">
          <a target="_blank" rel="noreferrer" onClick={ipcHandle}>
            {Locale.welcome.actions.sendIPC}
          </a>
        </div>
      </div>
      <Versions></Versions>
    </>
  )
}

通过语言选择组件,就可以得知所有的使用示例了,如组件标题语言的使用 {Locale.switchLang},就只需要将导入的语言当作一个对象进行使用即可,在 Select 组件中,value为 getLang()方法返回的值,当改变的时候,使用 setLang() 函数进行设置,多语言选项则使用 localeList 来帮助生成,其中label为在 localeOptions 对象中对应的值

tsx
{/* 语言选择组件 */ }
<div className="absolute top-4 right-4">
  {/* cn显示”切换语言“ */ }
  <span className="mr-2">{Locale.switchLang}</span>
  <Select
    value={getLang()}
    style={{ width: 120 }}
    onChange={(value) => setLang(value)}
    options={
      localeList.map((lang) => ({
        label: localeOptions[lang],
        value: lang,
      }))
    }
  />
</div>

这样就算是给我的electron应用加上了多语言的功能啦

个人目前认为这样实现,各个语言文件进行分开编写,使用简单,后续维护也较为简单

基于 MIT 许可发布