likes
comments
collection
share

新鲜出炉的Nodejs/Vue/React多语言解决方案

作者站长头像
站长
· 阅读数 31

开源项目:https://gitee.com/zhangfisher...

前言

基于javascript的国际化方案很多,比较有名的有fbti18nextreact-i18nextvue-i18nreact-intl等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子想造一个轮子,也不想想自己什么水平。

哪么到底是对现有解决方案有什么不满?最主要有三点:

  • 大部份均为要翻译的文本信息指定一个key,然后在源码文件中使用形如$t("message.login")之类的方式,然后在翻译时将之转换成最终的文本信息。此方式最大的问题是,在源码中必须人为地指定每一个key,在中文语境中,想为每一句中文均配套想一句符合语义的英文key是比较麻烦的,也很不直观不符合直觉。我希望在源文件中就直接使用中文,如t("中华人民共和国万岁"),然后国际化框架应该能自动处理后续的一系列麻烦。
  • 要能够比较友好地支持多库多包monorepo场景下的国际化协作,当主程序切换语言时,其他包或库也可以自动切换,并且在开发上每个包或库均可以独立地进行开发,集成到主程序时能无缝集成。这点在现有方案上没有找到比较理想的解决方案。
  • 大部份国际化框架均将中文视为二等公民,大部份情况下您应该采用英文作为第一语言,虽然这不是太大的问题,但是既然要再造一个轮子,为什么不将中文提升到一等公民呢。

基于此就开始造出VoerkaI18n这个全新的国际化多语言解决方案,主要特性包括:

  • 简单易用
  • 符合直觉,不需要手动定义文本Key映射。
  • 完整的自动化工具链支持,包括项目初始化、提取文本、编译语言等。
  • 支持babel插件自动导入t翻译函数。
  • 支持nodejs、浏览器(vue/react)前端环境。
  • 采用工程工具链运行时分开设计,发布时只需要集成很小的运行时(12K)。
  • 高度可扩展的复数货币数字等常用的多语言处理机制。
  • 通过格式化器可以扩展出强大的多语言特性。
  • 翻译过程内,提取文本可以自动进行同步,并保留已翻译的内容。
  • 可以随时添加支持的语言

安装

VoerkaI18n国际化框架是一个开源多包工程,主要由以下几个包组成:

  • @voerkai18/runtime

    必须的运行时,安装到运行依赖dependencies

    npm install --save @voerkai18/runtime
    yarn add @voerkai18/runtime
    pnpm add @voerkai18/runtime
  • @voerkai18/cli

    包含文本提取/编译等命令行工具,应该安装到开发依赖devDependencies

    npm install --save-dev @voerkai18/cli
    yarn add -D @voerkai18/cli
    pnpm add -D @voerkai18/cli
  • @voerkai18/formatters

    可选的,一些额外的格式化器,可以按需进行安装到dependencies中,用来扩展翻译时对插值变量的额外处理。

  • @voerkai18/babel

    可选的babel插件,用来实现自动导入翻译函数和翻译文本映射自动替换。

快速入门

本节以标准的Nodejs应用程序为例,简要介绍VoerkaI18n国际化框架的基本使用。其他vuereact应用的使用也基本相同。

myapp
  |--package.json
  |--index.js  

在本项目的所有支持的源码文件中均可以使用t函数对要翻译的文本进行包装,简单而粗暴。

// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))

t翻译函数是从myapp/languages/index.js文件导出的翻译函数,但是现在myapp/languages还不存在,后续会使用工具自动生成。voerkai18n后续会使用正则表达式对提取要翻译的文本。

第一步:初始化工程

在工程目录中运行voerkai18n init命令进行初始化。

> voerkai18n init 

上述命令会在当前工程目录下创建languages/settings.json文件。如果您的源代码在src子文件夹中,则会创建在src/languages/settings.json

settings.json内容如下:

{
    "languages": [
        {
            "name": "cn",
            "title": "cn"
        },
        {
            "name": "en",
            "title": "en"
        }
    ],
    "defaultLanguage": "cn",
    "activeLanguage": "cn",
    "namespaces": {}
}

上述命令代表了:

  • 本项目拟支持中文英文两种语言。
  • 默认语言是中文(即在源代码中直接使用中文)
  • 激活语言是中文

注意:

  • voerkai18n init是可选的,voerkai18n extract也可以实现相同的功能。
  • 一般情况下,您可以手工修改settings.json,如定义名称空间。

第二步:提取文本

接下来我们使用voerkai18n extract命令来自动扫描工程源码文件中的需要的翻译的文本信息。

myapp>voerkai18n extract

执行voerkai18n extract命令后,就会在myapp/languages通过生成translates/default.jsonsettings.json等相关文件。

  • translates/default.json : 该文件就是需要进行翻译的文本信息。
  • settings.json: 语言环境的基本配置信息,可以进行修改。

最后文件结构如下:

myapp
  |-- languages
    |-- settings.json                // 语言配置文件
    |-- translates                   // 此文件夹是所有需要翻译的内容
      |-- default.json               // 默认名称空间内容
  |-- package.json
  |-- index.js

如果略过第一步中的voerkai18n init,也可以使用以下命令来为创建和更新settinbgs.json

myapp>voerkai18n extract -D -lngs cn en de jp -d cn -a cn

以上命令代表:

  • 扫描当前文件夹下所有源码文件,默认是jsjsxhtmlvue文件类型。
  • 计划支持cnendejp四种语言
  • 默认语言是中文。(指在源码文件中我们直接使用中文即可)
  • 激活语言是中文(即默认切换到中文)
  • -D代表显示扫描调试信息

第三步:翻译文本

接下来就可以分别对language/translates文件夹下的所有JSON文件进行翻译了。每个JSON文件大概如下:

{
    "中华人民共和国万岁":{
        "en":"<在此编写对应的英文翻译内容>",
        "de":"<在此编写对应的德文翻译内容>"
        "jp":"<在此编写对应的日文翻译内容>",
        "$files":["index.js"]    // 记录了该信息是从哪几个文件中提取的
    },
    "中华人民共和国成立于{}":{
        "en":"<在此编写对应的英文翻译内容>",
        "de":"<在此编写对应的德文翻译内容>"
        "jp":"<在此编写对应的日文翻译内容>",
        "$files":["index.js"]
    }
}

我们只需要修改该文件翻译对应的语言即可。

重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract命令,该命令会进行以下操作:

  • 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
  • 如果文本内容在源代码中已修改了,则会视为新增加的内容。
  • 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。

因此,反复执行voerkai18n extract命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。

第四步:编译语言包

当我们完成myapp/languages/translates下的所有JSON语言文件的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间介绍),接下来需要对翻译后的文件进行编译。

myapp> voerkai18n compile

compile命令根据myapp/languages/translates/*.jsonmyapp/languages/settings.json文件编译生成以下文件:

  |-- languages
    |-- settings.json                // 语言配置文件
    |-- idMap.js                     // 文本信息id映射表
    |-- index.js                     // 包含该应用作用域下的翻译函数等
    |-- cn.js                        // 语言包
    |-- en.js
    |-- jp.js
    |-- de.js
    |-- translates                   // 此文件夹包含了所有需要翻译的内容
      |-- default.json
  |-- package.json
  |-- index.js

第五步:导入翻译函数

第一步中我们在源文件中直接使用了t翻译函数包装要翻译的文本信息,该t翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js中的。

import { t } from "./languages"   

因此,我们需要在需要进行翻译时导入该函数即可。但是如果源码文件很多,重次重复导入t函数也是比较麻烦的,所以我们也提供了一个babel插件来自动导入t函数。

第六步:切换语言

当需要切换语言时,可以通过调用change方法来切换语言。

import { i18nScope } from "./languages"

// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例,可以直接访问
VoerkaI18n.change("en")

i18nScope.changeVoerkaI18n.change两者是等价的。

一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。

import { i18nScope } from "./languages"

// 切换到英文
i18nScope.on((newLanguage)=>{
    ...
})
// 
VoerkaI18n.on((newLanguage)=>{
    ...
})

指南

翻译函数

默认提供翻译函数t用来进行翻译。一般情况下,t函数在执行voerkai18n compile命令生成在工程目录下的languages文件夹中。


// 从当前语言包文件夹index.js中导入翻译函数
import { t } from "<myapp>/languages"

// 不含插值变量
t("中华人民共和国")

// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")

// 当仅有两个参数且第2个参数是[]类型时,自动展开第一个参数进行位置插值
t("中华人民共和国成立于{year}年,首都{capital}",[1949,"北京"]) 
 
// 当仅有两个参数且第2个参数是{}类型时,启用字典插值变量
t("中华人民共和国成立于{year}年,首都{capital}",{year:1949,capital:"北京"})

// 插值变量可以是同步函数,在进行插值时自动调用。
t("中华人民共和国成立于{year}年,首都{capital}",()=>1949,"北京")

// 对插值变量启用格式化器
t("中华人民共和国成立于{birthday | year}年",{birthday:new Date()})

注意:

  • voerkai18n使用正则表达式来提取要翻译的内容,因此t()可以使用在任意地方。

插值变量

voerkai18nt函数支持使用插值变量,用来传入一个可变内容。

插值变量有命名插值变量位置插值变量

命名插值变量

可以在t函数中使用{变量名称}表示一个命名插值变量。

t("我姓名叫{name},我今年{age}岁",{name:"tom",age:12})
// 如果值是函数会自动调用
t("我姓名叫{name},我今年{age}岁",{name:"tom",age:()=>12})

仅当t函数仅有两个参数且第2个参数是{}类型时,启用字典插值变量,翻译时会自动进行插值。

位置插值变量

可以在t函数中使用一个空的{}表示一个位置插值变量。

t("我姓名叫{},我今年{}岁","tom",12)
// 如果值是函数会自动调用
t("我姓名叫{},我今年{}岁","tom",()=>12})
// 如果只有两个参数,且第2个参数是一个数组,会自动展开
t("我姓名叫{},我今年{}岁",["tom",12])
//如果第2个参数不是{}时就启用位置插值。
t("我姓名叫{name},我今年{age}岁","tom",()=>12)

插值变量格式化

voerka-i18n支持强大的插值变量格式化机制,可以在插值变量中使用{变量名称 | 格式化器名称 | 格式化器名称(...参数) | ... }类似管道操作符的语法,将上一个输出作为下一个输入,从而实现对变量值的转换。此机制是voerka-i18n实现复数、货币、数字等多语言支持的基础。

格式化器语法

我们假设定义以下格式化器(如果定义格式化器,详见后续)来进行示例。

  • UpperCase:将字符转换为大写
  • division:对数字按每n位一个逗号分割,支持一个可选参数分割位数,如division(123456)===123,456division(123456,4)===12,3456
  • mr : 自动添加一个先生称呼
// My name is TOM
t("My name is { name | UpperCase }",{name:"tom"})

// 我国2021年的GDP是¥14,722,730,697,890
t("我国2021年的GDP是¥{ gdp | division}",{gdp:14722730697890})

// 支持为格式化器提供参数,按4位一逗号分割
// 我国2021年的GDP是¥14,7227,3069,7890
t("我国2021年的GDP是¥{ gdp | division(4)}",{gdp:14722730697890})

// 支持连续使用多个格式化器
// My name is Mr.TOM
t("My name is { name | UpperCase | mr }",{name:"tom"})

每个格式化器本质上是一个(value)=>{...}的函数,并且能将上一个格式化器的输出作为下一个格式化器的输入,格式化器具有如下特性:

  • 无参数格式化器

    使用无参数格式化器时只需传入名称即可。例如:My name is { name | UpperCase }

  • 有参数格式化器

    格式化器支持传入参数,如{ gdp | division(4)}{ date | format('yyyy/MM/DD')}

    特别需要注意的是,格式化器的参数只能支持简单的类型的参数,如数字布尔型字符串

    不支持数组、对象和函数参数,也不支持复杂的表达式参数。

  • 支持连续使用多个格式化器

    就如您预期的一样,将上一个格式化器的输出作为下一个格式化器的输入

    {data | f1 | f2 | f3(1)}等效于 f3(f2(f1(data)),1)

自定义格式化器

当我们使用voerkai18n compile编译后,会生成languages/formatters.js文件,可以在该文件中自定义您自己的格式化器。

formatters.js文件内容如下:

module.exports = {
    // 在所有语言下生效的格式化器
    "*":{ 
        //[格式化名称]:(value)=>{...},
        //[格式化名称]:(value,arg)=>{...},
    },                                                
    // 在所有语言下只作用于特定数据类型的格式化器   
    $types:{
        // [数据类型名称]:(value)=>{...},
        // [数据类型名称]:(value)=>{...},
    },                                          
    cn:{
        $types:{
            // 所有类型的默认格式化器
            "*":{                
            },
            Date:{},
            Number:{},
            Boolean:{ },
            String:{},
            Array:{

            },
            Object:{

            }
        },
        [格式化名称]:(value)=>{.....},
        //.....
    },
    en:{
        $types:{
            // [数据类型名称]:(value)=>{...},
        },
        [格式化名称]:(value)=>{.....},
        //.....更多的格式化器.....
    }
}

说明:

格式化器函数

每一个格式化器就是一个普通的同步函数,不支持异步函数。

典型的无参数的格式化器:(value)=>{....返回格式化的结果...}

带参数的格式化器:(value,arg1,...)=>{....返回格式化的结果...},其中value是上一个格式化器的输出结果。

类型格式化器

可以为每一种数据类型指定一个默认的格式化器,支持对StringDateErrorObjectArrayBooleanNumber等数据类型的格式化。

当插值变量传入时,如果有定义了对应的的类型格式化器,会先调用该格式化器。

比如我们定义对Boolean类型格式化器,

//formatters.js

module.exports = {
    // 在所有语言下只作用于特定数据类型的格式化器   
    $types:{
        Boolean:(value)=> value ? "ON" : "OFF"
    }
} 
t("灯状态:{status}",true)  // === 灯状态:ON
t("灯状态:{status}",false)  // === 灯状态:OFF

在上例中,如果我们想在不同的语言环境下,翻译为不同的显示文本,则可以为不同的语言指定类型格式化器

//formatters.js
module.exports = {
    cn:{
        $types:{
            Boolean:(value)=> value ? "开" : "关"
        }
    },
    en:{
      $types:{
        Boolean:(value)=> value ? "ON" : "OFF" 
      }
    }
} 
// 当切换到中文时
t("灯状态:{status}",true)  // === 灯状态:开
t("灯状态:{status}",false)  // === 灯状态:关
// 当切换到英文时
t("灯状态:{status}",true)  // === 灯状态:ON
t("灯状态:{status}",false)  // === 灯状态:OFF

说明:

  • 完整的类型格式化器定义形式

    module.exports = {
        "*":{
            $types:{...}
        },    
        cn:{
            $types:{...}
        },
        en:{
          $types:{....}
        }
    }

    在匹配应用格式化时会先在当前语言的$types中查找匹配的格式化器,如果找不到再上*.$types中查找。

  • *.$types代表当所有语言中均没有定义时才匹配的类型格式化。
  • 类型格式化器是默认执行的,不需要指定名称。
  • 当前作用域的格式化器优先于全局的格式化器。(后续)
通用的格式化器

类型格式化器只针对特定数据类型,并且会默认调用。而通用的格式化器需要使用|管道符进行显式调用。

同样的,通用的格式化器定义在languages/formatters.js中。

module.exports = {
    "*":{
        $types:{...},
         [格式化名称]:(value)=>{.....},       
    },    
    cn:{
        $types:{...},
        [格式化名称]:(value)=>{.....},
    },
    en:{
      $types:{....},
      [格式化名称]:(value)=>{.....},
      [格式化名称]:(value,arg)=>{.....},        
    }
}

每一个格式化器均需要指定一个名称,在进行插值替换时会优先依据当前语言来匹配查找格式化器,如果找不到,再到键名为*中查找。

module.exports = {
    "*":{
        uppercase:(value)=>value
    },    
    cn:{
        uppercase:(value)=>["一","二","三","四","五","六","七","八","九","十"][value-1]
    },
    en:{
        uppercase:(value)=>["One","Two","Three","Four","Five","Six","seven","eight","nine","ten"][value-1]
    },
    jp:{
        
    }
}
// 当切换到中文时
t("{value | uppercase}",1)  // == 一
t("{value | uppercase}",2)  // == 二
t("{value | uppercase}",3)  // == 三
// 当切换到英文时
t("{value | uppercase}",1)  // == One
t("{value | uppercase}",2)  // == Two
t("{value | uppercase}",3)  // == Three
// 当切换到日文时,由于在该语言下没有定义uppercase格式式,因此到*中查找
t("{value | uppercase}",1)  // == 1
t("{value | uppercase}",2)  // == 2
t("{value | uppercase}",3)  // == 3

作用域格式化器

定义在languages/formatters.js里面的格式化器仅在当前工程生效,也就是仅在当前作用域生效。一般由应用开发者自行扩展。

关于作用域的概念详见后续介绍。

全局格式化器

定义在@voerkai18n/runtime里面的格式化器则全局有效,在所有场合均可以使用,但是其优先级低于作用域内的同名格式化器。目前内置的格式化器有:

名称 说明

扩展格式化器

除了可以在当前项目languages/formatters.js自定义格式化器和@voerkai18n/runtime里面的全局格式化器外,单列了@voerkai18n/formatters项目用来包含了更多的格式化器。

作为开源项目,欢迎大家提交贡献更多的格式化器。

日期时间

@voerkai18n/runtime内置了对日期时间进行处理的格式化器,可以直接使用,不需要额外的安装。

// 切换到中文
t("现在是{d | date}",new Date())       // ==  现在是2022年3月12日
t("现在是{d | time}",new Date())       // ==  现在是18点28分12秒
t("现在是{d | shorttime}",new Date())  // ==  现在是18:28:12
t("现在是{}",new Date())               // ==  现在是2022年3月12日 18点28分12秒

// 切换到英文
t("现在是{d | date}",new Date())   // ==  Now is 2022/3/12
t("现在是{d | time}",new Date())   // ==  Now is 18:28:12
t("现在是{}",new Date())           // ==  Now is 2022/3/20 19:17:24'

复数

当翻译文本内容是一个数组时启用复数处理机制。即在langauges/tranclates/*.json中的文本翻译项是一个数据。

启用复数处理机制

假设在index.html文件中具有一个翻译内容

    t("我{}一辆车")

经过extract命令提取为翻译文件后,如下:

// languages/translates/default.json
{
    "我有{}辆车":{
        "en":"",
        "de":"...." 
    }
}

现在我们要求引入复数处理机制,为不同数量采用不同的翻译,只需要将上述翻译文本更改为数组形式。

{
    "我有{}辆车":{
        "en":["I don't have car","I have a car","I have two cars","I have {} cars"],
        "en":["I don't have car","I have a car","I have {} cars"],
        "en":["I don't have car","I have {} cars"],
        "de":"...." 
    }
}

上例中,只需要在翻译文件中将上述的en:""更改为[<0对应的复数文本>,<1对应的复数文本>,...,<n对应的复数文本>]形式代表启动复数机制.

  • 可以灵活地为每一个数字(0、1、2、...、n)对应的复数形式进行翻译
  • 数量数字大于数组长度,则总是取最后一个复数形式
  • 复数形式的文本同样支持位置插值和变量插值。

对应的翻译函数

启用复数处理机制后,在t函数根据变量值来决定采用单数还是复数,按如下规则进行处理。

  • 不存在插值变量且t函数的第2个参数是数字

t("我有一辆车",0)  // ==   "I don't have a car"
t("我有一辆车",1)  // ==   "I have a car"
t("我有一辆车",2)  // ==   "I have two cars"
t("我有一辆车",100)  // == "I have 100 cars"
  • 存在插值变量且t函数的第2个参数是数字

就中文而言,上述没有指定插值变量是比较别扭的,一般可以引入一个位置插值变量更加友好。


t("我有{}辆车",0)          // ==   "I don't have a car"
t("我有{}辆车",1)          // ==   "I have a car"
t("我有{}辆车",2)          // ==   "I have two cars"
t("我有{}辆车",100)      // == "I have 100 cars"
  • 复数命名插值变量

当启用复数功能时,t函数需要知道根据哪个变量来决定采用何种复数形式。

当采用位置变量插值时,t函数取第一个数字类型参数作为位置插值复数。

t("{}有{}辆车","张三",0)

当采用命名变量插值时,t函数约定当插值字典中存在以$字符开头的变量时,并且值是数字时,根据该变量来引用复数。

下例中,t函数根据$count值来处理复数。

t("{name}有{$count}辆车",{name:"张三",$count:1})
  • 示例
// languages/translates/default.json
{
    "第{}章":{
        en:[
            "Chapter Zero","Chapter One", "Chapter Two", "Chapter Three","Chapter Four",
            "Chapter Five","Chapter Six","Chapter Seven","Chapter Eight","Chapter Nine",
            "Chapter {}"
        ],
        cn:["起始","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章",“第{}章”]
    }
}
// 翻译函数
t("第{}章",0)  // == Chapter Zero
t("第{}章",1)  // == Chapter One
t("第{}章",2)  // == Chapter Two
t("第{}章",3)  // == Chapter Three
t("第{}章",4)  // == Chapter Four
t("第{}章",5)  // == Chapter Five
t("第{}章",6)  // == Chapter Six
t("第{}章",7)  // == Chapter Seven
...
// 超过取最后一项
t("第{}章",100)  // == Chapter 100

字典

voerkiai18n内置一个dict格式化器,可以直接使用。

// 假设网络状态取值:0=初始化,1=正在连接,2=已连接,3=正在断开.4=已断开,>4=未知

t("当前状态:{status | dict(0,'初始化',1,'正在连接',2,'已连接',3,'正在断开',4,'已断开','未知') }",status)

货币

名称空间

voerkai18n 的名称空间是为了解决当源码文件非常多时,通过名称空间对翻译内容进行分类翻译的。

假设一个大型项目,其中源代码文件有上千个。默认情况下,voerkai18n extract会扫描所有源码文件将需要翻译的文本提取到languages/translates/default.json文件中。由于文件太多会导致以下问题:

  • 内容太多导致default.json文件太大,有利于管理
  • 有些翻译往往需要联系上下文才可以作出更准确的翻译,没有适当分类,不容易联系上下文。

因此,引入名称空间就是目的就是为了解决此问题。

配置名称空间,需要配置languages/settings.json文件。

// 工程目录:d:/code/myapp
// languages/settings.json
module.exports = {
    namespaces:{
        //"名称":"相对路径",
        “routes”:“routes”,
        "auth":"core/auth",
        "admin":"views/admin"
    }
}

以上例子代表:

  • d:\code\myapp\routes中扫描到的文本提取到routes.json中。
  • d:\code\myapp\auth中扫描到的文本提取到auth.json中。
  • d:\code\myapp\views/admin中扫描到的文本提取到admin.json中。

最终在 languages/translates中会包括:

languages
  |-- translates
      |-- default.json
      |-- routes.sjon
      |-- auth.json
      |-- admin.json      

然后,voerkai18n compile在编译时会自动合并这些文件,后续就不再需要名称空间的概念了。

名称空间仅仅是为了解决当翻译内容太多时的分类问题。

多库联动

voerkai18n 支持多个库国际化的联动和协作,即当主程序切换语言时,所有引用依赖库也会跟随主程序进行语言切换,整个切换过程对所有库开发都是透明的。

当我们在开发一个应用或者库并import "./languages"时,在langauges/index.js进行了如下处理:

  • 创建一个i18nScope作用域实例
  • 检测当前应用环境下是否具有全局单例VoerkaI18n

    • 如果存在VoerkaI18n全局单例,则会将当前i18nScope实例注册到VoerkaI18n.scopes
    • 如果不存在VoerkaI18n全局单例,则使用当前i18nScope实例的参数来创建一个VoerkaI18n全局单例。
  • 在每个应用与库中均可以使用import { t } from ".langauges导入本工程的t翻译函数,该t翻译函数被绑定当前i18nScope作用域实例,因此翻译时就只会使用到本工程的文本。这样就割离了不同工程和库之间的翻译。
  • 由于所有引用的i18nScope均注册到了全局单例VoerkaI18n,当切换语言时,VoerkaI18n会刷新切换所有注册的i18nScope,这样就实现了各个i18nScope即独立,又可以联动语言切换。

自动导入翻译函数

使用voerkai18 compile后,要进行翻译时需要从./languages导入t翻译函数。

import { t } from "./languages"

由于默认情况下,voerkai18 compile命令会在当前工程的/languages文件夹下,这样我们为了导入t翻译函数不得不使用各种相对引用,这即容易出错,又不美观,如下:

import { t } from "./languages"
import { t } from "../languages"
import { t } from "../../languages"
import { t } from "../../../languages"

作为国际化解决方案,一般工程的大部份源码中均会使用到翻译函数,这种使用体验比较差。

为此,我们提供了一个babel插件来自动完成翻译函数的自动引入。使用方法如下:

  • babel.config.js中配置插件
const i18nPlugin =  require("@voerkai18n/babel")
module.expors = {
    plugins: [
        [
            i18nPlugin,
            {
                // 可选,指定语言文件存放的目录,即保存编译后的语言文件的文件夹
                // 可以指定相对路径,也可以指定绝对路径
                // location:"",

                autoImport:"#/languages"  
            }            
        ]
    ]
}

这样,当在进行babel转码时,就会自动在js源码文件中导入t翻译函数。

babel-plugin-voerkai18n插件支持以下参数:

  • location

    配置langauges文件夹位置,默认会使用当前文件夹下的languages文件。

    因此,如果你的babel.config.js在项目根文件夹,而languages文件夹位于src/languages,则可以将location="src/languages",这样插件会自动从该文件夹读取需要的数据。

  • autoImport

    用来配置导入的路径。比如 autoImport="#/languages" ,则当在babel转码时,如果插件检测到t函数的存在并没有导入,就会自动在该源码中自动导入import { t } from "#/languages"

    配置autoImport时需要注意的是,为了提供一致的导入路径,视所使用的打包工具或转码插件,如webpackrollup等。比如使用babel-plugin-module-resolver

    module.expors = {
        plugins: [
            [
                "module-resolver",
                {
                    root:"./",
                    alias:{
                        "languages":"./src/languages"
                    }
                }            
            ]
        ]
    }

    这样配置autoImport="languages",则自动导入import { t } from "languages"

    webpackrollup等打包工具也有类似的插件可以实现别名等转换,其目的就是让babel-plugin-voerkai18n插件能自动导入固定路径,而不是各种复杂的相对路径。

文本映射

虽然VoerkaI18n推荐采用t("中华人民共和国万岁")形式的符合直觉的翻译形式,而不是采用t("xxxx.xxx")这样不符合直觉的形式,但是为什么大部份的国际化方案均采用t("xxxx.xxx")形式?

在我们的方案中,t("中华人民共和国万岁")形式相当于采用原始文本进行查表,语言名形式如下:

// en.js
{
    "中华人民共和国":"the people's Republic of China"
}
// jp.js
{
    "中华人民共和国":"中華人民共和国"
}

很显然,直接使用文本内容作为key,虽然符合直觉,但是会造成大量的冗余信息。因此,voerkai18n compile会将之编译成如下:

//idMap.js
{
    "1":"中华人民共和国万岁"
}
// en.js
{
    "1":"Long live the people's Republic of China"
}
// jp.js
{
    "2":"中華人民共和国"
}

如此,就消除了在en.jsjp.js文件中的冗余。但是在源代码文件中还存在t("中华人民共和国万岁"),整个运行环境中存在两份副本,一份在源代码文件中,一份在idMap.js中。

为了进一步减少重复内容,因此,我们需要将源代码文件中的t("中华人民共和国万岁")更改为t("1"),这样就能确保无重复冗余。但是,很显然,我们不可能手动来更改源代码文件,这就需要由babel插件来做这一件事了。

babel-plugin-voerkai18n插件同时还完成一份任务,就是自动读取voerkai18n compile生成的idMap.js文件,然后将t("中华人民共和国万岁")自动更改为t("1"),这样就完全消除了重复冗余信息。

所以,在最终形成的代码中,实际上每一个t函数均是t("1")t("2")t("3")...t("n")的形式,最终代码还是采用了用key来进行转换,只不过这个过程是自动完成的而已。

注意:如果没有启用babel-plugin-voerkai18n插件,还是可以正常工作,但是会有一份默认语言的冗余信息存在。

切换语言

可以通过全局单例或当前作用域实例切换语言。

import { i18nScope } from "./languages"

// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例,可以直接访问
VoerkaI18n.change("en")

侦听语言切换事件:

import { i18nScope } from "./languages"

// 切换到英文
i18nScope.on((newLanguage)=>{
    ...
})
// 
VoerkaI18n.on((newLanguage)=>{
    ...
})

加载语言包

当使用webpackrollup进行项目打包时,默认语言包采用静态打包,会被打包进行源码中。而其他语言则采用异步打包方式。在languages/index.js

const defaultMessages =  require("./cn.js")  
const activeMessages = defaultMessages
  
// 语言作用域
const scope = new i18nScope({
    default:   defaultMessages,                 // 默认语言包
    messages : activeMessages,                  // 当前语言包
    ....
    loaders:{ 
        "en" : ()=>import("./en.js") 
        "de" : ()=>import("./de.js") 
        "jp" : ()=>import("./jp.js") 
    })

babel插件

全局安装@voerkai18n/babel插件用来进行自动导入t函数和自动文本映射。

> npm install -g @voerkai18n/babel
> yarn global add @voerkai18n/babel
> pnpm add -g @voerkai18n/babel

然后在babel.config.js中使用,详见上节自动导入翻译函数介绍。

Vue扩展

React扩展

命令行

全局安装@voerkai18n/cli工具。

> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18n/cli

然后就可以执行:

> voerkai18n init
> voerkai18n extract
> voerkai18n compile

如果没有全局安装,则需要:

> yarn voerkai18n init
> yarn voerkai18n extract
> yarn voerkai18n compile
---
> pnpm voerkai18n init
> pnpm voerkai18n extract
> pnpm voerkai18n compile

init

用于在指定项目创建voerkai18n国际化配置文件。

> voerkai18n init --help
初始化项目国际化配置
Arguments:
  location                           工程项目所在目录
Options:
  -D, --debug                        输出调试信息
  -r, --reset                        重新生成当前项目的语言配置
  -lngs, --languages <languages...>  支持的语言列表 (default: ["cn","en"])
  -d, --defaultLanguage              默认语言
  -a, --activeLanguage               激活语言
  -h, --help                         display help for command

使用方法如下:

首先需要在工程文件下运行voerkai18n init命令对当前工程进行初始化。

//- `lngs`参数用来指定拟支持的语言名称列表
> voerkai18n init . -lngs cn en jp de -d cn

运行voerkai18n init命令后,会在当前工程中创建相应配置文件。

myapp
  |-- languages 
    |-- settings.json               // 语言配置文件
  |-- package.json
  |-- index.js

settings.json文件很简单,主要是用来配置要支持的语言等基本信息。

module.exports = {
    // 拟支持的语言列表
    "languages": [
        {
            "name": "cn",
            "title": "中文"
        },
        {
            "name": "en",
            "title": "英文"
        }
    ],
    // 默认语言,即准备在源码中写的语言,一般我们可以直接使用中文
    "defaultLanguage": "cn",
    // 激活语言,即默认要启用的语言,一般等于defaultLanguage
    "activeLanguage": "cn",
    // 翻译名称空间定义,详见后续介绍。
    "namespaces": {}
}

说明:

  • 您也可以手动自行创建languages/settings.json文件。这样就不需运行voerkai18n init命令了。
  • 如果你的源码放在src文件夹,则init命令会自动在在src文件夹下创建languages文件夹。
  • voerkai18n init是可选的,直接使用extract时也会自动创建相应的文件。
  • -m参数用来指定生成的settings.json的模块类型:

    • -m=auto时,会自动读取前工程package.json中的type字段
    • -m=esm时,会生成ESM模块类型的settings.json
    • -m=cjs时,会生成commonjs模块类型的settings.json
  • location参数是可选的,如果没有指定则采用当前目录。

    如果你想将languages安装在src/languages下,则可以指定voerkai18n init ./src

extract

扫描提取当前项目中的所有源码,提取出所有需要翻译的文本内容并保存在到<工程源码目录>/languages/translates/*.json

> voerkai18n extract --help
扫描并提取所有待翻译的字符串到<languages/translates>文件夹中

Arguments:
  location                     工程项目所在目录 (default: "./")

Options:
  -D, --debug                  输出调试信息
  -lngs, --languages           支持的语言
  -d, --defaultLanguage  默认语言
  -a, --activeLanguage    激活语言
  -ns, --namespaces            翻译名称空间
  -e, --exclude <folders>      排除要扫描的文件夹,多个用逗号分隔
  -u, --updateMode             本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并
  -f, --filetypes              要扫描的文件类型
  -h, --help                   display help for command

说明:

  • 启用-d参数时会输出提取过程,显示从哪些文件提取了几条信息。
  • 如果已手动创建或通过init命令创建了languages/settings.json文件,则可以不指定-ns-lngs-d-a参数。extract会优先使用languages/settings.json文件中的参数来进行提取。
  • -u参数用来指定如何将提取的文本与现存的文件进行合并。因为在国际化流程中,我们经常面临源代码变更时需要更新翻译的问题。支持三种合并策略。

    • sync:同步(默认值),两者自动合并,并且会删除在源码文件中不存在的文本。如果某个翻译已经翻译了一半也会保留。此值适用于大部情况,推荐。
    • overwrite:覆盖现存的翻译内容。这会导致已经进行了一半的翻译数据丢失,慎用
    • merge:合并,与sync的差别在于不会删除源码中已不存在的文本。
  • -e参数用来排除扫描的文件夹,多个用逗号分隔。内部采用gulp.src来进行文件提取,请参数。如 -e !libs,core/**/*。默认会自动排除node_modules文件夹
  • -f参数用来指定要扫描的文件类型,默认js,jsx,ts,tsx,vue,html
  • extract是基于正则表达式方式进行匹配的,而不是像i18n-next采用基于AST解析。

重点:

默认情况下,voerkai18n extract可以安全地反复多次执行,不会导致已经翻译一半的内容丢失。

如果想添加新的语言支持,也voerkai18n extract也可以如预期的正常工作。

compile

编译当前工程的语言包,编译结果输出在./langauges文件夹。

Usage: voerkai18n compile [options] [location]

编译指定项目的语言包

Arguments:
  location                  工程项目所在目录 (default: "./")

Options:
  -D, --debug               输出调试信息
  -m, --moduleType [types]  输出模块类型,取值auto,esm,cjs (default: "esm")
  -h, --help                display help for command

voerkai18n compile执行后会在langauges文件夹下输出:

myapp
  |--- langauges
    |-- index.js              // 当前作用域的源码
    |-- idMap.js              // 翻译文本与id的映射文件
    |-- formatters.js         // 自定义格式化器
    |-- cn.js                 // 中文语言包
    |-- en.js                 // 英文语言包 
    |-- xx.js                 // 其他语言包
    |-- ...

说明:

  • 在当前工程目录下,一般不需要指定参数就可以反复多次进行编译。
  • 您每次修改了源码并extract后,均应该再次运行compile命令。
  • 如果您修改了formatters.js,执行compile命令不会修改该文件。

API

i18nScope

每个工程会创建一个i18nScope实例。

import { i18nScope } from "./languages"

// 订阅语言切换事件
i18nScope.on((newLanguage)=>{...})
// 取消语言切换事件订阅
i18nScope.off(callback)
// 当前作用域配置
i18nScope.settings
// 当前语言
i18nScope.activeLanguage         // 如cn

// 默认语言
i18nScope.defaultLanguage         
// 返回当前支持的语言列表,可以用来显示
i18nScope.languages    // [{name:"cn",title:"中文"},{name:"en",title:"英文"},...]
// 返回当前作用域的格式化器                         
i18nScope.formatters   
// 当前作用id
i18nScope.id
// 切换语言,异步函数
await i18nScope.change(newLanguage)
// 当前语言包                         
i18nScope.messages        // {1:"...",2:"...","3":"..."}
// 引用全局VoerkaI18n实例                         
i18nScope.global
// 注册当前作用域格式化器
i18nScope.registerFormatter(name,formatter,{language:"*"})      

VoerkaI18n

import {} form "./languages"时会自动创建全局单VoerkaI18n

// 订阅语言切换事件
VoerkaI18n.on((newLanguage)=>{...})
// 取消语言切换事件订阅
VoerkaI18n.off(callback)
// 取消所有语言切换事件订阅
VoerkaI18n.offAll()
                              
// 返回当前默认语言
VoerkaI18n.defaultLanguage
// 返回当前激活语言
VoerkaI18n.activeLanguage
// 返回当前支持的语言
VoerkaI18n.languages                              
// 切换语言
await VoerkaI18n.change(newLanguage)
// 返回全局格式化器
VoerkaI18n.formatters                              
// 注册全局格式化器
VoerkaI18n.registerFormatter(name,formatter,{language:"*"})                              
                              

开源项目:https://gitee.com/zhangfisher...