update
This commit is contained in:
parent
64f9eaa93a
commit
37ad528dd0
1
packages/runtime/src/errors.ts
Normal file
1
packages/runtime/src/errors.ts
Normal file
@ -0,0 +1 @@
|
||||
export class InvalidLanguageError extends Error{}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -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 // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user