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:{}, $config:{},
$types:{} $types:{}
} }
export class VoerkaI18nFormatterRegistry{ export class VoerkaI18nFormatterRegistry{
// 由于语言的格式化器集合允许是一个异步加载块所以需要一个ready标志 // 由于语言的格式化器集合允许是一个异步加载块所以需要一个ready标志
// 当语言格式化器集合加载完成后ready标志才会变为true // 当语言格式化器集合加载完成后ready标志才会变为true
@ -101,6 +102,36 @@ export class VoerkaI18nFormatterRegistry{
getConfig(language?:string){ getConfig(language?:string){
return language ? getByPath(this.#formatters,`${language}.$config`,{defaultValue:{}}) : {} 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 * @param language
@ -151,4 +182,13 @@ export class VoerkaI18nFormatterRegistry{
if(!this.#ready) throw new FormattersNotLoadedError(this.#language) if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return (this.#formatters[this.#language] as VoerkaI18nFormatters).$config as VoerkaI18nFormatterConfigs 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.activeLanguage === activeLanguage) {
if (scope.cache.typedFormatters && dataType in scope.cache.typedFormatters) if (scope.cache.typedFormatters && dataType in scope.cache.typedFormatters)
return scope.cache.typedFormatters[dataType]; return scope.cache.typedFormatters[dataType];
} else { } else {
// 当语言切换时清空缓存 resetScopeCache(scope, activeLanguage); // 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage);
} }
const fallbackLanguage = scope.getLanguage(activeLanguage)?.fallback; const fallbackLanguage = scope.getLanguage(activeLanguage)?.fallback;
// 先在当前作用域中查找,再在全局查找 // 先在当前作用域中查找,再在全局查找
const targets = [ const targets = [
scope.activeFormatters, scope.formatters.types,
fallbackLanguage ? scope.formatters[fallbackLanguage] : null, // 如果指定了回退语言时,也在该回退语言中查找 scope.formatters.getTypes(fallbackLanguage),
scope.global.formatters[activeLanguage], scope.formatters.getTypes("*"),
scope.global.formatters["*"], scope.global.formatters.types,
scope.global.formatters.getTypes(fallbackLanguage),
scope.global.formatters.getTypes("*"),
]; ];
for (const target of targets) { for (const target of targets) {
if (!target) continue; if (!target) continue;
if(target){ if(target){
if (isPlainObject(target.$types) && isFunction(target.$types?.[dataType])) { if (isPlainObject(target.$types) && isFunction(target.$types?.[dataType])) {
return (scope.cache.typedFormatters[dataType] = target.$types[dataType]); return (scope.cache.typedFormatters[dataType] = target.$types[dataType]);
} }
} }
@ -201,6 +202,9 @@ function getDataTypeDefaultFormatter(scope:VoerkaI18nScope, activeLanguage:strin
* *
* *
* - * -
* -
* - 退
* -
* - * -
* *
* @param {*} scope * @param {*} scope
@ -210,30 +214,32 @@ function getDataTypeDefaultFormatter(scope:VoerkaI18nScope, activeLanguage:strin
*/ */
function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) { function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) {
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索 // 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if (!scope.$cache) resetScopeCache(scope); if (!scope.cache) resetScopeCache(scope,activeLanguage);
if (scope.$cache.activeLanguage === activeLanguage) { if (scope.cache.activeLanguage === activeLanguage) {
if (name in scope.$cache.formatters) if (name in scope.cache.formatters)
return scope.$cache.formatters[name]; return scope.cache.formatters[name];
} else { // 当语言切换时清空缓存 } else { // 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage); resetScopeCache(scope, activeLanguage);
} }
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback; const fallbackLanguage = scope.getLanguage(activeLanguage)?.fallback;
// 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}} // 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}
const range = [ const range = [
scope.activeFormatters, scope.formatters.formatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 scope.formatters.getFormatters(fallbackLanguage),
scope.formatters["*"], scope.formatters.getFormatters('*'),
scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器
scope.global.formatters[fallbackLanguage], scope.global.formatters.formatters,
scope.global.formatters["*"], // 适用于所有语言的格式化器 scope.global.formatters.getFormatters(fallbackLanguage),
scope.global.formatters.getFormatters('*'),
]; ];
for (const formatters of range) { for (const formatters of range) {
if (!formatters) continue; if (!formatters) continue;
if (isFunction(formatters[name])) { if (isFunction(formatters[name])) {
return (scope.$cache.formatters[name] = formatters[name]); return (scope.cache.formatters[name] = formatters[name]);
} }
} }
} }
/** /**
* Checker是一种特殊的格式化器 * Checker是一种特殊的格式化器
* *
@ -441,7 +447,7 @@ export function replaceInterpolatedVars(this:VoerkaI18nScope,template:string, ..
// 读取模板字符串中的插值变量列表 // 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...} // [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0]; let varValues = args[0];
return forEachInterpolatedVars(template,(varname, formatters, match) => { return forEachInterpolatedVars(template,(varname:string, formatters, match) => {
let value = varname in varValues ? varValues[varname] : ""; let value = varname in varValues ? varValues[varname] : "";
return getFormattedValue(scope,activeLanguage,formatters,value,template); 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; const params =args.length === 1 && Array.isArray(args[0]) ? [...args[0]] : args;
if (params.length === 0) return template; // 没有变量则不需要进行插值处理,返回原字符串 if (params.length === 0) return template; // 没有变量则不需要进行插值处理,返回原字符串
let i = 0; let i = 0;
return forEachInterpolatedVars(template,(varname, formatters, match) => { return forEachInterpolatedVars(template,(varname:string, formatters, match) => {
if (params.length > i) { if (params.length > i) {
return getFormattedValue(scope,activeLanguage,formatters,params[i++],template); return getFormattedValue(scope,activeLanguage,formatters,params[i++],template);
} else { } else {

View File

@ -19,12 +19,12 @@ import type {
VoerkaI18nScopeCache, VoerkaI18nScopeCache,
VoerkaI18nTranslate, VoerkaI18nTranslate,
VoerkaI18nLoaders, VoerkaI18nLoaders,
VoerkaI18nTypesFormatters, VoerkaI18nTypesFormatters,
VoerkaI18nFormatters, VoerkaI18nFormatters,
VoerkaI18nDynamicLanguageMessages, VoerkaI18nDynamicLanguageMessages,
VoerkaI18nTypesFormatterConfigs
} from "./types" } from "./types"
import { VoerkaI18nFormatterRegistry } from './formatterRegistry'; import { VoerkaI18nFormatterRegistry } from './formatterRegistry';
import { InvalidLanguageError } from "./errors"
export interface VoerkaI18nScopeOptions { export interface VoerkaI18nScopeOptions {
id?: string id?: string
@ -292,7 +292,7 @@ export class VoerkaI18nScope {
* @param {*} language * @param {*} language
* @returns * @returns
*/ */
private getLanguage(language:string):VoerkaI18nLanguageDefine | undefined{ getLanguage(language:string):VoerkaI18nLanguageDefine | undefined{
let index = this.languages.findIndex((lng) => lng.name == language); let index = this.languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this.languages[index]; if (index !== -1) return this.languages[index];
} }
@ -311,7 +311,42 @@ export class VoerkaI18nScope {
this.#options.messages = this.default; this.#options.messages = this.default;
this.#options.activeLanguage = this.defaultLanguage; 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 * @param {*} newLanguage
@ -324,50 +359,31 @@ export class VoerkaI18nScope {
this.#options.messages = this.default; this.#options.messages = this.default;
await this._patch(this.#options.messages, newLanguage); // 异步补丁 await this._patch(this.#options.messages, newLanguage); // 异步补丁
await this._changeFormatters(newLanguage); await this._changeFormatters(newLanguage);
this.#refreshing = false
return; return;
} }else{ // 非默认语言可以是静态语言包也可以是异步加载语言包
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数 try{
// 如果没有加载器,则无法加载语言包,因此回退到默认语言 let messages = await this.loadLanguageMessages(newLanguage)
let loader = this.loaders[newLanguage]; if(messages){
try { this.#options.messages = messages
let newMessages, useRemote =false; this.#options.activeLanguage = newLanguage;
if (isPlainObject(loader)) { // 静态语言包 // 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
newMessages = loader; // 因为远程加载的语言包已经是补丁过的了
} else if (isFunction(loader)) { // 语言包异步chunk if(!messages.$remote) {
newMessages = (await loader()).default; await this._patch(this.#options.messages, newLanguage);
} 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
} }
newMessages = Object.assign({},this.default,loadedMessages); // 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
this._fallback();
} }
} }catch(e:any){
if(newMessages){ if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this.#options.messages = newMessages
this.#options.activeLanguage = newLanguage;
// 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
if(!useRemote) {
await this._patch(this.#options.messages, newLanguage);
}
// 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
this._fallback(); this._fallback();
} } finally {
this.#refreshing = false;
} catch (e) { }
if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`); }
this._fallback();
} finally {
this.#refreshing = false;
}
} }
/** /**
* *
@ -386,8 +402,8 @@ export class VoerkaI18nScope {
Object.assign(messages, pachedMessages); Object.assign(messages, pachedMessages);
this._savePatchedMessages(pachedMessages, newLanguage); this._savePatchedMessages(pachedMessages, newLanguage);
} }
} catch (e) { } catch (e:any) {
if (this.debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e); 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("要翻译的文本内容")
* translate("I am {} {}","man") == I am man * translate("I am {} {}","man") == I am man
* translate("I am {p}",{p:"man"}) * translate("I am {p}",{p:"man"})
* translate("total {$count} items", {$count:1}) //复数形式 * translate("total {$count} items", {$count:1})
* translate("total {} {} {} items",a,b,c) // 位置变量插值 * translate("total {} {} {} items",a,b,c)
* *
* this===scope scope * this===scope scope
* *
@ -75,37 +75,41 @@ function getPluraMessage(messages:any,value:number){
export function translate(this:VoerkaI18nScope,message:string,...args:any[]) { export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
const scope = this const scope = this
const activeLanguage = scope.global.activeLanguage const activeLanguage = scope.global.activeLanguage
let content:string = message // 如果内容是复数则其值是一个数组数组中的每个元素是从1-N数量形式的文本内容
let vars=[] // 插值变量列表 let result:string | string[] = message
let pluralVars= [] // 复数变量 let vars=[] // 插值变量列表
let pluraValue = null // 复数值 let pluraValue = null // 复数值
if(!(typeof(message)==="string")) return message if(!(typeof(message)==="string")) return message
try{ try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用 // 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){ if(arguments.length === 2 && isPlainObject(arguments[1])){// 字典插值
const dictVars:Record<string,any>={} const dictVars:Record<string,any>=arguments[1]
Object.entries(arguments[1]).forEach(([name,value])=>{ for(const [name,value] of Object.entries(dictVars)){
if(typeof(value)=="function"){ if(isFunction(value)){
try{ try{
dictVars[name] = value() dictVars[name] = value()
}catch(e){ }catch(e){
dictVars[name] = value dictVars[name] = value
} }
} }
// 以$开头的视为复数变量 // 以$开头的视为复数变量,记录下来
if(name.startsWith("$") && typeof(dictVars[name])==="number") pluralVars.push(name) const isNum:boolean = typeof(dictVars[name])==="number"
}) if((pluraValue==null && isNum) || name.startsWith("$") && isNum){
vars = [arguments[1]] pluraValue = dictVars[name]
}else if(arguments.length >= 2){ }
}
vars = [dictVars]
}else if(arguments.length >= 2){ // 位置插值
vars = [...arguments].splice(1).map((arg,index)=>{ vars = [...arguments].splice(1).map((arg,index)=>{
try{ try{
arg = isFunction(arg) ? arg() : arg arg = isFunction(arg) ? arg() : arg
// 位置参数中以第一个数值变量为复数变量 // 约定:位置参数中以第一个数值变量作为指示复数变量
if(isNumber(arg)) pluraValue = parseInt(arg) if(isNumber(arg)) pluraValue = parseInt(arg)
}catch(e){ } }catch(e){
return String(arg)
}
return arg return arg
}) })
} }
// 3. 取得翻译文本模板字符串 // 3. 取得翻译文本模板字符串
@ -114,37 +118,35 @@ export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
// 当源文件运用了babel插件后会将原始文本内容转换为msgId // 当源文件运用了babel插件后会将原始文本内容转换为msgId
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>} // 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
if(isMessageId(content)){ if(isMessageId(result)){
content = scope.default[content] || message result = scope.default[result] || message
} }
}else{ }else{
// 2.2 从当前语言包中取得翻译文本模板字符串 // 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId // 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
let msgId = isMessageId(content) ? content : scope.idMap[content] let msgId = isMessageId(result) ? result : scope.idMap[result]
content = scope.messages[msgId] || content result = scope.messages[msgId] || result
} }
// 2. 处理复数 // 2. 处理复数
// 经过上面的处理content可能是字符串或者数组 // 经过上面的处理content可能是字符串或者数组
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....] // content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式 // 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
if(Array.isArray(content) && content.length>0){ if(Array.isArray(result) && result.length>0){
// 如果存在复数命名变量,只取第一个复数变量 // 如果存在复数命名变量,只取第一个复数变量
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置 if(pluraValue!==null){ // 启用的是位置插值
content = getPluraMessage(content,pluraValue) result = getPluraMessage(result,pluraValue)
}else if(pluralVar.length>0){
content = getPluraMessage(content,parseInt(vars(pluralVar[0])))
}else{ // 如果找不到复数变量,则使用第一个内容 }else{ // 如果找不到复数变量,则使用第一个内容
content = content[0] result = result[0]
} }
} }
// 进行插值处理 // 进行插值处理
if(vars.length==0){ if(vars.length==0){
return content return result
}else{ }else{
return replaceInterpolatedVars.call(scope,content,...vars) return replaceInterpolatedVars.call(scope,result as string,...vars)
} }
}catch(e){ }catch(e){
return content // 出错则返回原始文本 return result // 出错则返回原始文本
} }
} }