This commit is contained in:
wxzhang 2023-04-03 18:08:26 +08:00
parent 64f9eaa93a
commit 37ad528dd0
5 changed files with 169 additions and 104 deletions

View File

@ -0,0 +1 @@
export class InvalidLanguageError extends Error{}

View File

@ -24,6 +24,7 @@ const EmptyFormatters:any = {
$config:{},
$types:{}
}
export class VoerkaI18nFormatterRegistry{
// 由于语言的格式化器集合允许是一个异步加载块所以需要一个ready标志
// 当语言格式化器集合加载完成后ready标志才会变为true
@ -101,6 +102,36 @@ export class VoerkaI18nFormatterRegistry{
getConfig(language?:string){
return language ? getByPath(this.#formatters,`${language}.$config`,{defaultValue:{}}) : {}
}
/**
*/
getTypes(language?:string){
return language ? getByPath(this.#formatters,`${language}.$types`,{defaultValue:{}}) : {}
}
/**
*/
getFormatters(language?:string){
return language ? getByPath(this.#formatters,language,{defaultValue:{}}) : {}
}
/**
*
* @param language
* @param config
*/
updateConfig(language:string,config:VoerkaI18nFormatterConfigs){
if(language in this.#formatters){
const formatters = this.#formatters[language]
if(typeof(formatters)=='function'){
throw new FormattersNotLoadedError(language)
}else{
if(!formatters.$config){
formatters.$config = {}
}
Object.assign(formatters.$config,config)
}
}
}
/**
*
* @param language
@ -151,4 +182,13 @@ export class VoerkaI18nFormatterRegistry{
if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return (this.#formatters[this.#language] as VoerkaI18nFormatters).$config as VoerkaI18nFormatterConfigs
}
get types(){
if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return (this.#formatters[this.#language] as VoerkaI18nFormatters).$types as VoerkaI18nFormatterConfigs
}
get items(){
if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return this.#formatters[this.#language] as VoerkaI18nFormatterConfigs
}
}

View File

@ -173,23 +173,24 @@ function getDataTypeDefaultFormatter(scope:VoerkaI18nScope, activeLanguage:strin
if (scope.cache.activeLanguage === activeLanguage) {
if (scope.cache.typedFormatters && dataType in scope.cache.typedFormatters)
return scope.cache.typedFormatters[dataType];
} else {
// 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage);
} else {
resetScopeCache(scope, activeLanguage); // 当语言切换时清空缓存
}
const fallbackLanguage = scope.getLanguage(activeLanguage)?.fallback;
// 先在当前作用域中查找,再在全局查找
const targets = [
scope.activeFormatters,
fallbackLanguage ? scope.formatters[fallbackLanguage] : null, // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage],
scope.global.formatters["*"],
scope.formatters.types,
scope.formatters.getTypes(fallbackLanguage),
scope.formatters.getTypes("*"),
scope.global.formatters.types,
scope.global.formatters.getTypes(fallbackLanguage),
scope.global.formatters.getTypes("*"),
];
for (const target of targets) {
if (!target) continue;
if(target){
if (isPlainObject(target.$types) && isFunction(target.$types?.[dataType])) {
return (scope.cache.typedFormatters[dataType] = target.$types[dataType]);
}
}
@ -201,6 +202,9 @@ function getDataTypeDefaultFormatter(scope:VoerkaI18nScope, activeLanguage:strin
*
*
* -
* -
* - 退
* -
* -
*
* @param {*} scope
@ -210,30 +214,32 @@ function getDataTypeDefaultFormatter(scope:VoerkaI18nScope, activeLanguage:strin
*/
function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) {
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if (!scope.$cache) resetScopeCache(scope);
if (scope.$cache.activeLanguage === activeLanguage) {
if (name in scope.$cache.formatters)
return scope.$cache.formatters[name];
if (!scope.cache) resetScopeCache(scope,activeLanguage);
if (scope.cache.activeLanguage === activeLanguage) {
if (name in scope.cache.formatters)
return scope.cache.formatters[name];
} else { // 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage);
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback;
const fallbackLanguage = scope.getLanguage(activeLanguage)?.fallback;
// 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}
const range = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.formatters["*"],
scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器
scope.global.formatters[fallbackLanguage],
scope.global.formatters["*"], // 适用于所有语言的格式化器
scope.formatters.formatters,
scope.formatters.getFormatters(fallbackLanguage),
scope.formatters.getFormatters('*'),
scope.global.formatters.formatters,
scope.global.formatters.getFormatters(fallbackLanguage),
scope.global.formatters.getFormatters('*'),
];
for (const formatters of range) {
if (!formatters) continue;
if (isFunction(formatters[name])) {
return (scope.$cache.formatters[name] = formatters[name]);
return (scope.cache.formatters[name] = formatters[name]);
}
}
}
/**
* Checker是一种特殊的格式化器
*
@ -441,7 +447,7 @@ export function replaceInterpolatedVars(this:VoerkaI18nScope,template:string, ..
// 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0];
return forEachInterpolatedVars(template,(varname, formatters, match) => {
return forEachInterpolatedVars(template,(varname:string, formatters, match) => {
let value = varname in varValues ? varValues[varname] : "";
return getFormattedValue(scope,activeLanguage,formatters,value,template);
}
@ -452,7 +458,7 @@ export function replaceInterpolatedVars(this:VoerkaI18nScope,template:string, ..
const params =args.length === 1 && Array.isArray(args[0]) ? [...args[0]] : args;
if (params.length === 0) return template; // 没有变量则不需要进行插值处理,返回原字符串
let i = 0;
return forEachInterpolatedVars(template,(varname, formatters, match) => {
return forEachInterpolatedVars(template,(varname:string, formatters, match) => {
if (params.length > i) {
return getFormattedValue(scope,activeLanguage,formatters,params[i++],template);
} else {

View File

@ -19,12 +19,12 @@ import type {
VoerkaI18nScopeCache,
VoerkaI18nTranslate,
VoerkaI18nLoaders,
VoerkaI18nTypesFormatters,
VoerkaI18nFormatters,
VoerkaI18nDynamicLanguageMessages,
VoerkaI18nTypesFormatterConfigs
VoerkaI18nTypesFormatters,
VoerkaI18nFormatters,
VoerkaI18nDynamicLanguageMessages,
} from "./types"
import { VoerkaI18nFormatterRegistry } from './formatterRegistry';
import { InvalidLanguageError } from "./errors"
export interface VoerkaI18nScopeOptions {
id?: string
@ -292,7 +292,7 @@ export class VoerkaI18nScope {
* @param {*} language
* @returns
*/
private getLanguage(language:string):VoerkaI18nLanguageDefine | undefined{
getLanguage(language:string):VoerkaI18nLanguageDefine | undefined{
let index = this.languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this.languages[index];
}
@ -311,7 +311,42 @@ export class VoerkaI18nScope {
this.#options.messages = this.default;
this.#options.activeLanguage = this.defaultLanguage;
}
/**
* :
* - {}
* - Promise<VoerkaI18nLanguageMessages>
*
*
*
* @param language
* @returns
*/
private async loadLanguageMessages(language:string):Promise<VoerkaI18nLanguageMessages>{
if(!this.hasLanguage(language)) throw new InvalidLanguageError(`Not found language <${language}>`)
// 非默认语言可以是:语言包对象,也可以是一个异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[language];
let messages:VoerkaI18nLanguageMessages = {}
if (isPlainObject(loader)) { // 静态语言包
messages = loader as unknown as VoerkaI18nLanguageMessages;
} else if (isFunction(loader)) { // 语言包异步chunk
messages = (await loader()).default;
} else if (isFunction(this.global.defaultMessageLoader)) {
// 从远程加载语言包:如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = (await this.global.loadMessagesFromDefaultLoader(language,this)) as unknown as VoerkaI18nDynamicLanguageMessages;
if(isPlainObject(loadedMessages)){
// 需要保存动态语言包中的$config合并到对应语言的格式化器配置
if(isPlainObject(loadedMessages.$config)){
this.formatters.updateConfig(language,loadedMessages.$config!)
delete loadedMessages.$config
}
messages = Object.assign({
$remote : true // 添加一个标识,表示该语言包是从远程加载的
},this.default,loadedMessages); // 合并默认语言包和动态语言包,这样就可以局部覆盖默认语言包
}
}
return messages
}
/**
*
* @param {*} newLanguage
@ -324,50 +359,31 @@ export class VoerkaI18nScope {
this.#options.messages = this.default;
await this._patch(this.#options.messages, newLanguage); // 异步补丁
await this._changeFormatters(newLanguage);
this.#refreshing = false
return;
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage];
try {
let newMessages, useRemote =false;
if (isPlainObject(loader)) { // 静态语言包
newMessages = loader;
} else if (isFunction(loader)) { // 语言包异步chunk
newMessages = (await loader()).default;
} else if (isFunction(this.global.defaultMessageLoader)) { // 从远程加载语言包:如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = (await this.global.loadMessagesFromDefaultLoader(newLanguage,this)) as unknown as VoerkaI18nDynamicLanguageMessages;
if(isPlainObject(loadedMessages)){
useRemote = true
// 需要保存动态语言包中的$config合并到对应语言的格式化器配置
if(isPlainObject(loadedMessages.$config)){
this.formatters[newLanguage] = {
$config : loadedMessages.$config as any
}
delete loadedMessages.$config
}else{ // 非默认语言可以是静态语言包也可以是异步加载语言包
try{
let messages = await this.loadLanguageMessages(newLanguage)
if(messages){
this.#options.messages = messages
this.#options.activeLanguage = newLanguage;
// 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
// 因为远程加载的语言包已经是补丁过的了
if(!messages.$remote) {
await this._patch(this.#options.messages, newLanguage);
}
newMessages = Object.assign({},this.default,loadedMessages);
// 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
this._fallback();
}
}
if(newMessages){
this.#options.messages = newMessages
this.#options.activeLanguage = newLanguage;
// 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
if(!useRemote) {
await this._patch(this.#options.messages, newLanguage);
}
// 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
}catch(e:any){
if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
}
} catch (e) {
if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
} finally {
this.#refreshing = false;
}
} finally {
this.#refreshing = false;
}
}
}
/**
*
@ -386,8 +402,8 @@ export class VoerkaI18nScope {
Object.assign(messages, pachedMessages);
this._savePatchedMessages(pachedMessages, newLanguage);
}
} catch (e) {
if (this.debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
} catch (e:any) {
if (this.debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e.message);
}
}
/**

View File

@ -66,8 +66,8 @@ function getPluraMessage(messages:any,value:number){
* translate("要翻译的文本内容")
* translate("I am {} {}","man") == I am man
* translate("I am {p}",{p:"man"})
* translate("total {$count} items", {$count:1}) //复数形式
* translate("total {} {} {} items",a,b,c) // 位置变量插值
* translate("total {$count} items", {$count:1})
* translate("total {} {} {} items",a,b,c)
*
* this===scope scope
*
@ -75,37 +75,41 @@ function getPluraMessage(messages:any,value:number){
export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
const scope = this
const activeLanguage = scope.global.activeLanguage
let content:string = message
let vars=[] // 插值变量列表
let pluralVars= [] // 复数变量
let pluraValue = null // 复数值
// 如果内容是复数则其值是一个数组数组中的每个元素是从1-N数量形式的文本内容
let result:string | string[] = message
let vars=[] // 插值变量列表
let pluraValue = null // 复数值
if(!(typeof(message)==="string")) return message
try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){
const dictVars:Record<string,any>={}
Object.entries(arguments[1]).forEach(([name,value])=>{
if(typeof(value)=="function"){
if(arguments.length === 2 && isPlainObject(arguments[1])){// 字典插值
const dictVars:Record<string,any>=arguments[1]
for(const [name,value] of Object.entries(dictVars)){
if(isFunction(value)){
try{
dictVars[name] = value()
}catch(e){
dictVars[name] = value
}
}
// 以$开头的视为复数变量
if(name.startsWith("$") && typeof(dictVars[name])==="number") pluralVars.push(name)
})
vars = [arguments[1]]
}else if(arguments.length >= 2){
// 以$开头的视为复数变量,记录下来
const isNum:boolean = typeof(dictVars[name])==="number"
if((pluraValue==null && isNum) || name.startsWith("$") && isNum){
pluraValue = dictVars[name]
}
}
vars = [dictVars]
}else if(arguments.length >= 2){ // 位置插值
vars = [...arguments].splice(1).map((arg,index)=>{
try{
arg = isFunction(arg) ? arg() : arg
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg)
}catch(e){ }
arg = isFunction(arg) ? arg() : arg
// 约定:位置参数中以第一个数值变量作为指示复数变量
if(isNumber(arg)) pluraValue = parseInt(arg)
}catch(e){
return String(arg)
}
return arg
})
})
}
// 3. 取得翻译文本模板字符串
@ -114,37 +118,35 @@ export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
if(isMessageId(content)){
content = scope.default[content] || message
if(isMessageId(result)){
result = scope.default[result] || message
}
}else{
// 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
let msgId = isMessageId(content) ? content : scope.idMap[content]
content = scope.messages[msgId] || content
let msgId = isMessageId(result) ? result : scope.idMap[result]
result = scope.messages[msgId] || result
}
// 2. 处理复数
// 经过上面的处理content可能是字符串或者数组
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
if(Array.isArray(content) && content.length>0){
if(Array.isArray(result) && result.length>0){
// 如果存在复数命名变量,只取第一个复数变量
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
content = getPluraMessage(content,pluraValue)
}else if(pluralVar.length>0){
content = getPluraMessage(content,parseInt(vars(pluralVar[0])))
if(pluraValue!==null){ // 启用的是位置插值
result = getPluraMessage(result,pluraValue)
}else{ // 如果找不到复数变量,则使用第一个内容
content = content[0]
result = result[0]
}
}
// 进行插值处理
if(vars.length==0){
return content
return result
}else{
return replaceInterpolatedVars.call(scope,content,...vars)
return replaceInterpolatedVars.call(scope,result as string,...vars)
}
}catch(e){
return content // 出错则返回原始文本
return result // 出错则返回原始文本
}
}