From 5904297e35117eeb453e8c2249210dad9b63c9c1 Mon Sep 17 00:00:00 2001 From: wxzhang Date: Wed, 11 Jan 2023 18:10:59 +0800 Subject: [PATCH] update typescript for react --- packages/apps/reactapp/package.json | 3 + packages/apps/reactapp/src/App.jsx | 23 +- .../apps/reactapp/src/components/Banner.jsx | 6 +- .../src/components/LanguageConfigurator.jsx | 6 +- .../reactapp/src/languages/formatters/en.js | 113 +++ .../reactapp/src/languages/formatters/zh.js | 113 +++ packages/apps/reactapp/src/languages/index.js | 37 +- .../apps/reactapp/src/languages/runtime.js | 936 ------------------ packages/apps/reactapp/src/main.js | 3 +- packages/apps/reactapp/src/main.jsx | 5 +- packages/apps/reactapp/vite.config.js | 2 +- packages/cli/templates/entry.js | 4 +- packages/cli/templates/entry.ts | 3 +- packages/react/index.d.ts | 17 +- packages/react/index.js | 39 - packages/react/index.jsx | 84 ++ packages/react/package.json | 9 +- packages/runtime/index.d.ts | 19 +- packages/runtime/index.js | 1 + packages/runtime/package.json | 10 +- packages/runtime/rollup.config.js | 26 +- packages/runtime/scope.js | 5 +- pnpm-lock.yaml | 52 +- 23 files changed, 455 insertions(+), 1061 deletions(-) create mode 100644 packages/apps/reactapp/src/languages/formatters/en.js create mode 100644 packages/apps/reactapp/src/languages/formatters/zh.js delete mode 100644 packages/apps/reactapp/src/languages/runtime.js delete mode 100644 packages/react/index.js create mode 100644 packages/react/index.jsx diff --git a/packages/apps/reactapp/package.json b/packages/apps/reactapp/package.json index e49b52f..794a726 100644 --- a/packages/apps/reactapp/package.json +++ b/packages/apps/reactapp/package.json @@ -8,8 +8,11 @@ "preview": "vite preview" }, "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", "@voerkai18n/react": "workspace:^1.0.0", + "@voerkai18n/runtime": "workspace:^1.1.9", "@voerkai18n/vite": "workspace:^1.0.7", + "core-js": "^3.27.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/apps/reactapp/src/App.jsx b/packages/apps/reactapp/src/App.jsx index 43e3a48..863321c 100644 --- a/packages/apps/reactapp/src/App.jsx +++ b/packages/apps/reactapp/src/App.jsx @@ -4,15 +4,15 @@ import './App.css' import "./main" import Banner from './components/Banner' import LanguageConfigurator from "./components/LanguageConfigurator.jsx" -import { useVoerkaI18n } from "@voerkai18n/react"; +import { VoerkaI18nProvider,useVoerkaI18n } from "@voerkai18n/react"; +import { i18nScope } from "./languages" - -function App() { - const { activeLanguage, changeLanguage, languages } = useVoerkaI18n(); - return ( -
+function RootLayout(){ + const { t } = useVoerkaI18n(); + return ( +
-
{t("Voerkai1n是一个功能强大设计精良的国际化解决方案")}
+
{t("Voerkai1n是一个功能强大设计精良的国际化解决方案")}
@@ -25,8 +25,17 @@ function App() { 学习VoerkaI18n
+
+ ) +} + +function App() { + return ( + + + ) } diff --git a/packages/apps/reactapp/src/components/Banner.jsx b/packages/apps/reactapp/src/components/Banner.jsx index 136b6d6..75def65 100644 --- a/packages/apps/reactapp/src/components/Banner.jsx +++ b/packages/apps/reactapp/src/components/Banner.jsx @@ -1,6 +1,8 @@ -import { useState } from 'react' +import { useVoerkaI18n } from '@voerkai18n/react' +import React,{ useState } from 'react' function Banner( props ) { + const { t } = useVoerkaI18n() return (

{t("一字一世界,一笔一乾坤,汉字是这个星球上最美的语言")}

@@ -9,4 +11,4 @@ function Banner( props ) { ) } -export default Banner +export default React.memo(Banner) diff --git a/packages/apps/reactapp/src/components/LanguageConfigurator.jsx b/packages/apps/reactapp/src/components/LanguageConfigurator.jsx index 3e4cb69..7af5c41 100644 --- a/packages/apps/reactapp/src/components/LanguageConfigurator.jsx +++ b/packages/apps/reactapp/src/components/LanguageConfigurator.jsx @@ -2,10 +2,10 @@ import { useState } from "react"; import { useVoerkaI18n } from "@voerkai18n/react" function LanguageConfigurator(props) { - const { activeLanguage, changeLanguage, languages } = useVoerkaI18n(); - return ( + const { language, changeLanguage, languages,t } = useVoerkaI18n(); + return (
-
{t("当前语言")}:{ activeLanguage }
+
{t("当前语言")}:{ language }
{languages.map((lang) => { return ( diff --git a/packages/apps/reactapp/src/languages/formatters/en.js b/packages/apps/reactapp/src/languages/formatters/en.js new file mode 100644 index 0000000..9f8963d --- /dev/null +++ b/packages/apps/reactapp/src/languages/formatters/en.js @@ -0,0 +1,113 @@ +/** + + 格式化器用来对翻译文本内容中的插值变量进行处理 + + 如何编写格式器请参阅官网! + + */ + + +// import { Formatter,FlexFormatter } from "./runtime" +export default { + // global : true, // 简单地设置为true,代表当前所有格式化器均注册到全局,false只在当前scope生效 + // global : { // 仅将里面的格式化器注册到全局 + // $config:{... } + // xxxx : value => { ... }, + // xxxx : (value,$config) => { ... }, + // xxxx : (value,...args,$config) => { ... }, + // xxxx : Formatter(value,...args,$config) => { ... }, + // xxxx : FlexFormatter(value,params,$config) => { ... }, + //}, // 是否注册到全局,false只在当前scope生效 + // 直接对内置格式化器进行配置,请参阅官网文档 + // $config:{ + // datetime : { + // units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"], + // date :{ + // long : 'YYYY/MM/DD HH:mm:ss', + // short : "YYYY/MM/DD", + // format : "local" + // }, + // quarter : { + // long : ["First Quarter","Second Quarter","Third Quarter","Fourth Quarter"], + // short : ["Q1","Q2","Q3","Q4"], + // format : "short" + // }, + // month:{ + // long : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + // short : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], + // format : "long" // 0-长名称,1-短名称,2-数字 + // }, + // weekday:{ + // long : ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + // short : ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"], + // format : "long", // 0-长名称,1-短名称,2-数字 + // }, + // time : { + // long : "HH:mm:ss", + // short : "HH:mm:ss", + // format : 'local' + // }, + // timeSlots : { + // slots : [12], + // lowerCases : ["am","pm"], + // upperCases : ["AM","PM"] + // }, + // relativeTime : { + // units : ["seconds","minutes","hours","days","weeks","months","years"], + // now : "Now", + // before : "{value} {unit} ago", + // after : "after {value} {unit}" + // } + // }, + // currency : { + // default : "{symbol}{value}{unit}", + // long : "{prefix} {symbol}{value}{unit}{suffix}", + // short : "{symbol}{value}{unit}", + // custom : "{prefix} {symbol}{value}{unit}{suffix}", + // format : "default", + // //-- + // units : [""," thousands"," millions"," billions"," trillions"], //千,百万,十亿,万亿 + // radix : 3, // 进制,即三位一进,中文是4位一进 + // symbol : "$", // 符号 + // prefix : "USD", // 前缀 + // suffix : "", // 后缀 + // division : 3, // ,分割位 + // precision : 2, // 精度 + + // }, + // number : { + // division : 3, // , 分割位,3代表每3位添加一个, + // precision : 0 // 精度,即保留小数点位置,0代表不限 + // }, + // empty:{ + // //values : [], // 可选,定义空值,如果想让0,''也为空值,可以指定values=[0,''] + // escape : "", // 当空值时显示的备用值 + // next : 'break' // 当空值时下一步的行为: break=中止;skip=跳过 + // }, + // error : { + // //当错误时显示的内容,支持的插值变量有message=错误信息,error=错误类名,也可以是一个返回上面内容的同步函数 + // escape : null, // 默认当错误时显示空内容 + // next : 'break' // 当出错时下一步的行为: break=中止;skip=忽略 + // }, + // fileSize:{ + // brief : ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB","NB","DB"], + // whole : ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "TeraBytes", "PetaBytes", "ExaBytes", "ZetaBytes", "YottaBytes","DoggaBytes"], + // precision: 2 // 小数精度 + // } + // }, + // 改变特定数据类型的默认格式化器 + // $types:{ + // Date : dateFormatter, + // Null : value =>"", + // Undefined: value =>"", + // Error : value => "ERROR", + // Boolean : value =>value ? "True":"False", + // Number : numberFormartter + // } + // 以下可以自定义编写格式化器 + // xxxx : value => { ... }, + // xxxx : (value,$config) => { ... }, + // xxxx : (value,...args,$config) => { ... }, + // xxxx : Formatter(value,...args,$config) => { ... }, + // xxxx : FlexFormatter(value,params,$config) => { ... }, +} diff --git a/packages/apps/reactapp/src/languages/formatters/zh.js b/packages/apps/reactapp/src/languages/formatters/zh.js new file mode 100644 index 0000000..9f8963d --- /dev/null +++ b/packages/apps/reactapp/src/languages/formatters/zh.js @@ -0,0 +1,113 @@ +/** + + 格式化器用来对翻译文本内容中的插值变量进行处理 + + 如何编写格式器请参阅官网! + + */ + + +// import { Formatter,FlexFormatter } from "./runtime" +export default { + // global : true, // 简单地设置为true,代表当前所有格式化器均注册到全局,false只在当前scope生效 + // global : { // 仅将里面的格式化器注册到全局 + // $config:{... } + // xxxx : value => { ... }, + // xxxx : (value,$config) => { ... }, + // xxxx : (value,...args,$config) => { ... }, + // xxxx : Formatter(value,...args,$config) => { ... }, + // xxxx : FlexFormatter(value,params,$config) => { ... }, + //}, // 是否注册到全局,false只在当前scope生效 + // 直接对内置格式化器进行配置,请参阅官网文档 + // $config:{ + // datetime : { + // units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"], + // date :{ + // long : 'YYYY/MM/DD HH:mm:ss', + // short : "YYYY/MM/DD", + // format : "local" + // }, + // quarter : { + // long : ["First Quarter","Second Quarter","Third Quarter","Fourth Quarter"], + // short : ["Q1","Q2","Q3","Q4"], + // format : "short" + // }, + // month:{ + // long : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + // short : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], + // format : "long" // 0-长名称,1-短名称,2-数字 + // }, + // weekday:{ + // long : ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + // short : ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"], + // format : "long", // 0-长名称,1-短名称,2-数字 + // }, + // time : { + // long : "HH:mm:ss", + // short : "HH:mm:ss", + // format : 'local' + // }, + // timeSlots : { + // slots : [12], + // lowerCases : ["am","pm"], + // upperCases : ["AM","PM"] + // }, + // relativeTime : { + // units : ["seconds","minutes","hours","days","weeks","months","years"], + // now : "Now", + // before : "{value} {unit} ago", + // after : "after {value} {unit}" + // } + // }, + // currency : { + // default : "{symbol}{value}{unit}", + // long : "{prefix} {symbol}{value}{unit}{suffix}", + // short : "{symbol}{value}{unit}", + // custom : "{prefix} {symbol}{value}{unit}{suffix}", + // format : "default", + // //-- + // units : [""," thousands"," millions"," billions"," trillions"], //千,百万,十亿,万亿 + // radix : 3, // 进制,即三位一进,中文是4位一进 + // symbol : "$", // 符号 + // prefix : "USD", // 前缀 + // suffix : "", // 后缀 + // division : 3, // ,分割位 + // precision : 2, // 精度 + + // }, + // number : { + // division : 3, // , 分割位,3代表每3位添加一个, + // precision : 0 // 精度,即保留小数点位置,0代表不限 + // }, + // empty:{ + // //values : [], // 可选,定义空值,如果想让0,''也为空值,可以指定values=[0,''] + // escape : "", // 当空值时显示的备用值 + // next : 'break' // 当空值时下一步的行为: break=中止;skip=跳过 + // }, + // error : { + // //当错误时显示的内容,支持的插值变量有message=错误信息,error=错误类名,也可以是一个返回上面内容的同步函数 + // escape : null, // 默认当错误时显示空内容 + // next : 'break' // 当出错时下一步的行为: break=中止;skip=忽略 + // }, + // fileSize:{ + // brief : ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB","NB","DB"], + // whole : ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "TeraBytes", "PetaBytes", "ExaBytes", "ZetaBytes", "YottaBytes","DoggaBytes"], + // precision: 2 // 小数精度 + // } + // }, + // 改变特定数据类型的默认格式化器 + // $types:{ + // Date : dateFormatter, + // Null : value =>"", + // Undefined: value =>"", + // Error : value => "ERROR", + // Boolean : value =>value ? "True":"False", + // Number : numberFormartter + // } + // 以下可以自定义编写格式化器 + // xxxx : value => { ... }, + // xxxx : (value,$config) => { ... }, + // xxxx : (value,...args,$config) => { ... }, + // xxxx : Formatter(value,...args,$config) => { ... }, + // xxxx : FlexFormatter(value,params,$config) => { ... }, +} diff --git a/packages/apps/reactapp/src/languages/index.js b/packages/apps/reactapp/src/languages/index.js index ebf04c5..5e4b5d3 100644 --- a/packages/apps/reactapp/src/languages/index.js +++ b/packages/apps/reactapp/src/languages/index.js @@ -1,9 +1,9 @@ -import messageIds from "./idMap.js" -import runtime from "./runtime.js" -const { translate,i18nScope } = runtime - -import formatters from "./formatters.js" +import messageIds from "./idMap.js" // 语言ID映射文件 +import runtime from "@voerkai18n/runtime" +const { translate,VoerkaI18nScope } = runtime +import defaultFormatters from "./formatters/zh" +const activeFormatters = defaultFormatters import defaultMessages from "./zh.js" const activeMessages = defaultMessages @@ -24,18 +24,25 @@ const scopeSettings = { "activeLanguage": "zh", "namespaces": {} } +const formatters = { + 'zh' : defaultFormatters, + 'en' : ()=>import("./formatters/en.js") +} +// 语言包加载器 +const loaders = { + "en" : ()=>import("./en.js") +} // 语言作用域 -const scope = new i18nScope({ - ...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters - id: "reactapp", // 当前作用域的id,自动取当前工程的package.json的name - default: defaultMessages, // 默认语言包 - messages : activeMessages, // 当前语言包 - idMap:messageIds, // 消息id映射列表 - formatters, // 当前作用域的格式化函数列表 - loaders:{ - "en" : ()=>import("./en.js") - } +const scope = new VoerkaI18nScope({ + ...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters + id : "reactapp", // 当前作用域的id,自动取当前工程的package.json的name + debug : false, // 是否在控制台输出高度信息 + default : defaultMessages, // 默认语言包 + messages : activeMessages, // 当前语言包 + idMap : messageIds, // 消息id映射列表 + formatters, // 扩展自定义格式化器 + loaders // 语言包加载器 }) // 翻译函数 const scopedTtranslate = translate.bind(scope) diff --git a/packages/apps/reactapp/src/languages/runtime.js b/packages/apps/reactapp/src/languages/runtime.js deleted file mode 100644 index 87c3c36..0000000 --- a/packages/apps/reactapp/src/languages/runtime.js +++ /dev/null @@ -1,936 +0,0 @@ -/** - * 判断是否是JSON对象 - * @param {*} obj - * @returns - */ - function isPlainObject$1(obj){ - if (typeof obj !== 'object' || obj === null) return false; - var proto = Object.getPrototypeOf(obj); - if (proto === null) return true; - var baseProto = proto; - - while (Object.getPrototypeOf(baseProto) !== null) { - baseProto = Object.getPrototypeOf(baseProto); - } - return proto === baseProto; -} - -function isNumber$1(value){ - return !isNaN(parseInt(value)) -} - -/** - * 简单进行对象合并 - * - * options={ - * array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并 - * } - * - * @param {*} toObj - * @param {*} formObj - * @returns 合并后的对象 - */ -function deepMerge$1(toObj,formObj,options={}){ - let results = Object.assign({},toObj); - Object.entries(formObj).forEach(([key,value])=>{ - if(key in results){ - if(typeof value === "object" && value !== null){ - if(Array.isArray(value)){ - if(options.array === 0){ - results[key] = value; - }else if(options.array === 1){ - results[key] = [...results[key],...value]; - }else if(options.array === 2){ - results[key] = [...new Set([...results[key],...value])]; - } - }else { - results[key] = deepMerge$1(results[key],value,options); - } - }else { - results[key] = value; - } - }else { - results[key] = value; - } - }); - return results -} - - -/** - * 获取指定变量类型名称 - * getDataTypeName(1) == Number - * getDataTypeName("") == String - * getDataTypeName(null) == Null - * getDataTypeName(undefined) == Undefined - * getDataTypeName(new Date()) == Date - * getDataTypeName(new Error()) == Error - * - * @param {*} v - * @returns - */ - function getDataTypeName$1(v){ - if (v === null) return 'Null' - if (v === undefined) return 'Undefined' - if(typeof(v)==="function") return "Function" - return v.constructor && v.constructor.name; -} - - - - -var utils ={ - isPlainObject: isPlainObject$1, - isNumber: isNumber$1, - deepMerge: deepMerge$1, - getDataTypeName: getDataTypeName$1 -}; - -/** -* -* 简单的事件触发器 -* -*/ - -var eventemitter = class EventEmitter{ - constructor(){ - this._callbacks = []; - } - on(callback){ - if(this._callbacks.includes(callback)) return - this._callbacks.push(callback); - } - off(callback){ - for(let i=0;icb(...args))); - }else { - await Promise.all(this._callbacks.map(cb=>cb(...args))); - } - } -}; - -var scope = class i18nScope { - constructor(options={},callback){ - // 每个作用域都有一个唯一的id - this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000)); - this._languages = options.languages; // 当前作用域的语言列表 - this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称 - this._activeLanguage = options.activeLanguage; // 当前语言名称 - this._default = options.default; // 默认语言包 - this._messages = options.messages; // 当前语言包 - this._idMap = options.idMap; // 消息id映射列表 - this._formatters = options.formatters; // 当前作用域的格式化函数列表 - this._loaders = options.loaders; // 异步加载语言文件的函数列表 - this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用 - // 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索 - this.$cache={ - activeLanguage : null, - typedFormatters: {}, - formatters : {}, - }; - // 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域, - // 则使用当前作用域来初始化全局VoerkaI18n实例 - if(!globalThis.VoerkaI18n){ - const { I18nManager } = runtime; - globalThis.VoerkaI18n = new I18nManager({ - defaultLanguage: this.defaultLanguage, - activeLanguage : this.activeLanguage, - languages: options.languages, - }); - } - this.global = globalThis.VoerkaI18n; - // 正在加载语言包标识 - this._loading=false; - // 在全局注册作用域 - this.register(callback); - } - // 作用域 - get id(){return this._id} - // 默认语言名称 - get defaultLanguage(){return this._defaultLanguage} - // 默认语言名称 - get activeLanguage(){return this._activeLanguage} - // 默认语言包 - get default(){return this._default} - // 当前语言包 - get messages(){return this._messages} - // 消息id映射列表 - get idMap(){return this._idMap} - // 当前作用域的格式化函数列表 - get formatters(){return this._formatters} - // 异步加载语言文件的函数列表 - get loaders(){return this._loaders} - // 引用全局VoerkaI18n配置,注册后自动引用 - get global(){return this._global} - set global(value){this._global = value;} - /** - * 在全局注册作用域 - * @param {*} callback 当注册 - */ - register(callback){ - if(!typeof(callback)==="function") callback = ()=>{}; - this.global.register(this).then(callback).catch(callback); - } - registerFormatter(name,formatter,{language="*"}={}){ - if(!typeof(formatter)==="function" || typeof(name)!=="string"){ - throw new TypeError("Formatter must be a function") - } - if(DataTypes.includes(name)){ - this.formatters[language].$types[name] = formatter; - }else { - this.formatters[language][name] = formatter; - } - } - /** - * 回退到默认语言 - */ - _fallback(){ - this._messages = this._default; - this._activeLanguage = this.defaultLanguage; - } - /** - * 刷新当前语言包 - * @param {*} newLanguage - */ - async refresh(newLanguage){ - this._loading = Promise.resolve(); - if(!newLanguage) newLanguage = this.activeLanguage; - // 默认语言,默认语言采用静态加载方式,只需要简单的替换即可 - if(newLanguage === this.defaultLanguage){ - this._messages = this._default; - return - } - // 非默认语言需要异步加载语言包文件,加载器是一个异步函数 - // 如果没有加载器,则无法加载语言包,因此回退到默认语言 - const loader = this.loaders[newLanguage]; - if(typeof(loader) === "function"){ - try{ - this._messages = (await loader()).default; - this._activeLanguage = newLanguage; - }catch(e){ - console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`); - this._fallback(); - } - }else { - this._fallback(); - } - } - // 以下方法引用全局VoerkaI18n实例的方法 - get on(){return this.global.on.bind(this.global)} - get off(){return this.global.off.bind(this.global)} - get offAll(){return this.global.offAll.bind(this.global)} - get change(){ - return this.global.change.bind(this.global) - } -}; - -/** - * 内置的格式化器 - * - */ - -/** - * 字典格式化器 - * 根据输入data的值,返回后续参数匹配的结果 - * dict(data,,,,,,,...) - * - * - * dict(1,1,"one",2,"two",3,"three",4,"four") == "one" - * dict(2,1,"one",2,"two",3,"three",4,"four") == "two" - * dict(3,1,"one",2,"two",3,"three",4,"four") == "three" - * dict(4,1,"one",2,"two",3,"three",4,"four") == "four" - * // 无匹配时返回原始值 - * dict(5,1,"one",2,"two",3,"three",4,"four") == 5 - * // 无匹配时并且后续参数个数是奇数,则返回最后一个参数 - * dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more" - * - * 在翻译中使用 - * I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples - * - * @param {*} value - * @param {...any} args - * @returns - */ - function dict(value,...args){ - for(let i=0;i0 && (args.length % 2!==0)) return args[args.length-1] - return value -} - -var formatters = { - "*":{ - $types:{ - Date:(value)=>value.toLocaleString() - }, - time:(value)=> value.toLocaleTimeString(), - shorttime:(value)=> value.toLocaleTimeString(), - date: (value)=> value.toLocaleDateString(), - dict, //字典格式化器 - }, - zh:{ - $types:{ - Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒` - }, - shortime:(value)=> value.toLocaleTimeString(), - time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`, - date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`, - shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`, - currency:(value)=>`${value}元`, - }, - en:{ - currency:(value)=>{ - return `$${value}` - } - } -}; - -const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils; -const EventEmitter = eventemitter; -const i18nScope = scope; -let inlineFormatters = formatters; // 内置格式化器 - - - -// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } -// 不支持参数: let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g - -// 支持参数: { var | formatter(x,x,..) | formatter } -let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g; - -/** - * 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些 - * 不需要进行插值处理的字符串 - * 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配 - * 从而可以减少不要的正则匹配 - * 注意:该方法只能快速判断一个字符串不包括插值变量 - * @param {*} str - * @returns {boolean} true=可能包含插值变量, - */ -function hasInterpolation(str){ - return str.includes("{") && str.includes("}") -} -const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]; - - -/** - 通过正则表达式对原始文本内容进行解析匹配后得到的 - formatters="| aaa(1,1) | bbb " - - 需要统一解析为 - - [ - [aaa,[1,1]], // [formatter'name,[args,...]] - [bbb,[]], - ] - - formatters="| aaa(1,1,"dddd") | bbb " - - 目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数 - 在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析 - - @returns [[,[,,...]]] - */ -function parseFormatters(formatters){ - if(!formatters) return [] - // 1. 先解析为 ["aaa()","bbb"]形式 - let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim()); - - // 2. 解析格式化器参数 - return result.map(formatter=>{ - let firstIndex = formatter.indexOf("("); - let lastIndex = formatter.lastIndexOf(")"); - if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器 - const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim(); - let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{ - arg = arg.trim(); - if(!isNaN(parseInt(arg))){ - return parseInt(arg) // 数字 - }else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){ - return arg.substr(1,arg.length-2) // 字符串 - }else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){ - return arg.toLowerCase()==="true" // 布尔值 - }else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){ - try{ - return JSON.parse(arg) - }catch(e){ - return String(arg) - } - }else { - return String(arg) - } - }); - return [formatter.substr(0,firstIndex),args] - }else {// 不带参数的格式化器 - return [formatter,[]] - } - }) -} - -/** - * 提取字符串中的插值变量 - * // [ - // { - name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>], - // .... - // - * @param {*} str - * @param {*} isFull =true 保留所有插值变量 =false 进行去重 - * @returns {Array} - * [ - * { - * name:"<变量名称>", - * formatters:[ - * {name:"<格式化器名称>",args:[<参数>,<参数>,....]}, - * {name:"<格式化器名称>",args:[<参数>,<参数>,....]}, - * ], - * match:"<匹配字符串>" - * }, - * ... - * ] - */ -function getInterpolatedVars(str){ - let vars = []; - forEachInterpolatedVars(str,(varName,formatters,match)=>{ - let varItem = { - name:varName, - formatters:formatters.map(([formatter,args])=>{ - return { - name:formatter, - args:args - } - }), - match:match - }; - if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){ - vars.push(varItem); - } - return "" - }); - return vars -} -/** - * 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置 - * @param {*} str - * @param {Function(<变量名称>,[formatters],match[0])} callback - * @returns 返回替换后的字符串 - */ -function forEachInterpolatedVars(str,callback,options={}){ - let result=str, match; - let opts = Object.assign({ - replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false - },options); - varWithPipeRegexp.lastIndex=0; - while ((match = varWithPipeRegexp.exec(result)) !== null) { - const varname = match.groups.varname || ""; - // 解析格式化器和参数 = [,[,[,,...]]] - const formatters = parseFormatters(match.groups.formatters); - if(typeof(callback)==="function"){ - try{ - if(opts.replaceAll){ - result=result.replaceAll(match[0],callback(varname,formatters,match[0])); - }else { - result=result.replace(match[0],callback(varname,formatters,match[0])); - } - }catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程 - break - } - } - varWithPipeRegexp.lastIndex=0; - } - return result -} - -function resetScopeCache(scope,activeLanguage=null){ - scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}}; -} -/** - * 取得指定数据类型的默认格式化器 - * - * 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时, - * 会自动调用该格式化器来对值进行格式化转换 - - const formatters = { - "*":{ - $types:{...} // 在所有语言下只作用于特定数据类型的格式化器 - }, // 在所有语言下生效的格式化器 - zh:{ - $types:{ - [数据类型]:(value)=>{...}, - }, - [格式化器名称]:(value)=>{...}, - [格式化器名称]:(value)=>{...}, - [格式化器名称]:(value)=>{...}, - }, - } - * @param {*} scope - * @param {*} activeLanguage - * @param {*} dataType 数字类型 - * @returns {Function} 格式化函数 - */ -function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){ - if(!scope.$cache) resetScopeCache(scope); - if(scope.$cache.activeLanguage === activeLanguage) { - if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType] - }else {// 当语言切换时清空缓存 - resetScopeCache(scope,activeLanguage); - } - - // 先在当前作用域中查找,再在全局查找 - const targets = [scope.formatters,scope.global.formatters]; - for(const target of targets){ - if(!target) continue - // 优先在当前语言的$types中查找 - if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){ - let formatters = target[activeLanguage].$types; - if(dataType in formatters && typeof(formatters[dataType])==="function"){ - return scope.$cache.typedFormatters[dataType] = formatters[dataType] - } - } - // 在所有语言的$types中查找 - if(("*" in target) && isPlainObject(target["*"].$types)){ - let formatters = target["*"].$types; - if(dataType in formatters && typeof(formatters[dataType])==="function"){ - return scope.$cache.typedFormatters[dataType] = formatters[dataType] - } - } - } -} - -/** - * 获取指定名称的格式化器函数 - * @param {*} scope - * @param {*} activeLanguage - * @param {*} name 格式化器名称 - * @returns {Function} 格式化函数 - */ -function getFormatter(scope,activeLanguage,name){ - // 缓存格式化器引用,避免重复检索 - if(!scope.$cache) resetScopeCache(scope); - if(scope.$cache.activeLanguage === activeLanguage) { - if(name in scope.$cache.formatters) return scope.$cache.formatters[name] - }else {// 当语言切换时清空缓存 - resetScopeCache(scope,activeLanguage); - } - // 先在当前作用域中查找,再在全局查找 - const targets = [scope.formatters,scope.global.formatters]; - for(const target of targets){ - // 优先在当前语言查找 - if(activeLanguage in target){ - let formatters = target[activeLanguage] || {}; - if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name] - } - // 在所有语言的$types中查找 - let formatters = target["*"] || {}; - if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name] - } -} - -/** - * 执行格式化器并返回结果 - * @param {*} value - * @param {*} formatters 多个格式化器顺序执行,前一个输出作为下一个格式化器的输入 - */ -function executeFormatter(value,formatters){ - if(formatters.length===0) return value - let result = value; - try{ - for(let formatter of formatters){ - if(typeof(formatter) === "function") { - result = formatter(result); - }else {// 如果碰到无效的格式化器,则跳过过续的格式化器 - return result - } - } - }catch(e){ - console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` ); - } - return result -} -/** - * 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为 - * - * - * - * @param {*} scope - * @param {*} activeLanguage - * @param {*} formatters - */ -function buildFormatters(scope,activeLanguage,formatters){ - let results = []; - for(let formatter of formatters){ - if(formatter[0]){ - const func = getFormatter(scope,activeLanguage,formatter[0]); - if(typeof(func)==="function"){ - results.push((v)=>{ - return func(v,...formatter[1]) - }); - }else { - // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 - // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 - results.push((v)=>{ - if(typeof(v[formatter[0]])==="function"){ - return v[formatter[0]].call(v,...formatter[1]) - }else { - return v - } - }); - } - } - } - return results -} - -/** - * 将value经过格式化器处理后返回 - * @param {*} scope - * @param {*} activeLanguage - * @param {*} formatters - * @param {*} value - * @returns - */ -function getFormattedValue(scope,activeLanguage,formatters,value){ - // 1. 取得格式化器函数列表 - const formatterFuncs = buildFormatters(scope,activeLanguage,formatters); - // 2. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高 - const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)); - if(defaultFormatter){ - formatterFuncs.splice(0,0,defaultFormatter); - } - // 3. 执行格式化器 - value = executeFormatter(value,formatterFuncs); - return value -} - -/** - * 字符串可以进行变量插值替换, - * replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) - * replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...]) - * replaceInterpolatedVars("<模板字符串>",变量值,变量值,...]) - * -- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典 - replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 -- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数 - replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2 -- 普通位置参数替换 - replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2 -- -this == scope == { formatters: {}, ... } -* @param {*} template -* @returns -*/ -function replaceInterpolatedVars(template,...args) { - const scope = this; - // 当前激活语言 - const activeLanguage = scope.global.activeLanguage; - - // 没有变量插值则的返回原字符串 - if(args.length===0 || !hasInterpolation(template)) return template - - // ****************************变量插值**************************** - if(args.length===1 && isPlainObject(args[0])){ - // 读取模板字符串中的插值变量列表 - // [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...} - let varValues = args[0]; - return forEachInterpolatedVars(template,(varname,formatters)=>{ - let value = (varname in varValues) ? varValues[varname] : ''; - return getFormattedValue(scope,activeLanguage,formatters,value) - }) - }else { - // ****************************位置插值**************************** - // 如果只有一个Array参数,则认为是位置变量列表,进行展开 - 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)=>{ - if(params.length>i){ - return getFormattedValue(scope,activeLanguage,formatters,params[i++]) - }else { - throw new Error() // 抛出异常,停止插值处理 - } - },{replaceAll:false}) - - } -} - -// 默认语言配置 -const defaultLanguageSettings = { - defaultLanguage: "zh", - activeLanguage: "zh", - languages:[ - {name:"zh",title:"中文",default:true}, - {name:"en",title:"英文"} - ], - formatters:inlineFormatters -}; - -function isMessageId(content){ - return parseInt(content)>0 -} -/** - * 根据值的单数和复数形式,从messages中取得相应的消息 - * - * @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...] - * @param {*} value - */ -function getPluraMessage(messages,value){ - try{ - if(Array.isArray(messages)){ - return messages.length > value ? messages[value] : messages[messages.length-1] - }else { - return messages - } - }catch{ - return Array.isArray(messages) ? messages[0] : messages - } -} -function escape(str){ - return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\") - .replaceAll("\t","\\t") - .replaceAll("\n","\\n") - .replaceAll("\b","\\b") - .replaceAll("\r","\\r") - .replaceAll("\f","\\f") - .replaceAll("\'","\\'") - .replaceAll('\"','\\"') - .replaceAll('\v','\\v') -} -function unescape(str){ - return str - .replaceAll("\\t","\t") - .replaceAll("\\n","\n") - .replaceAll("\\b","\b") - .replaceAll("\\r","\r") - .replaceAll("\\f","\f") - .replaceAll("\\'","\'") - .replaceAll('\\"','\"') - .replaceAll('\\v','\v') - .replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\") -} -/** - * 翻译函数 - * -* 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) // 位置变量插值 - * - * this===scope 当前绑定的scope - * - */ -function translate(message) { - const scope = this; - const activeLanguage = scope.global.activeLanguage; - let content = message; - let vars=[]; // 插值变量列表 - let pluralVars= []; // 复数变量 - let pluraValue = null; // 复数值 - if(!typeof(message)==="string") return message - try{ - // 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用 - if(arguments.length === 2 && isPlainObject(arguments[1])){ - Object.entries(arguments[1]).forEach(([name,value])=>{ - if(typeof(value)==="function"){ - try{ - vars[name] = value(); - }catch(e){ - vars[name] = value; - } - } - // 以$开头的视为复数变量 - if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name); - }); - vars = [arguments[1]]; - }else if(arguments.length >= 2){ - vars = [...arguments].splice(1).map((arg,index)=>{ - try{ - arg = typeof(arg)==="function" ? arg() : arg; - // 位置参数中以第一个数值变量为复数变量 - if(isNumber(arg)) pluraValue = parseInt(arg); - }catch(e){ } - return arg - }); - - } - - - - - // 3. 取得翻译文本模板字符串 - if(activeLanguage === scope.defaultLanguage){ - // 2.1 从默认语言中取得翻译文本模板字符串 - // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 - // 当源文件运用了babel插件后会将原始文本内容转换为msgId - // 如果是msgId则从scope.default中读取,scope.default=默认语言包={:} - if(isMessageId(content)){ - content = scope.default[content] || message; - } - }else { - // 2.2 从当前语言包中取得翻译文本模板字符串 - // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId - // JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错 - let msgId = isMessageId(content) ? content : scope.idMap[escape(content)]; - content = scope.messages[msgId] || content; - content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content); - } - // 2. 处理复数 - // 经过上面的处理,content可能是字符串或者数组 - // content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....] - // 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式 - if(Array.isArray(content) && content.length>0){ - // 如果存在复数命名变量,只取第一个复数变量 - if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置 - content = getPluraMessage(content,pluraValue); - }else if(pluralVar.length>0){ - content = getPluraMessage(content,parseInt(vars(pluralVar[0]))); - }else { // 如果找不到复数变量,则使用第一个内容 - content = content[0]; - } - } - - // 进行插值处理 - if(vars.length==0){ - return content - }else { - return replaceInterpolatedVars.call(scope,content,...vars) - } - }catch(e){ - return content // 出错则返回原始文本 - } -} - -/** - * 多语言管理类 - * - * 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n - * - * VoerkaI18n.languages // 返回支持的语言列表 - * VoerkaI18n.defaultLanguage // 默认语言 - * VoerkaI18n.language // 当前语言 - * VoerkaI18n.change(language) // 切换到新的语言 - * - * - * VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件 - * VoerkaI18n.off("change",(language)=>{}) - * - * */ - class I18nManager extends EventEmitter{ - constructor(settings={}){ - super(); - if(I18nManager.instance!=null){ - return I18nManager.instance; - } - I18nManager.instance = this; - this._settings = deepMerge(defaultLanguageSettings,settings); - this._scopes=[]; - return I18nManager.instance; - } - get settings(){ return this._settings } - get scopes(){ return this._scopes } - // 当前激活语言 - get activeLanguage(){ return this._settings.activeLanguage} - // 默认语言 - get defaultLanguage(){ return this._settings.defaultLanguage} - // 支持的语言列表 - get languages(){ return this._settings.languages} - // 内置格式化器 - get formatters(){ return inlineFormatters } - /** - * 切换语言 - */ - async change(value){ - value=value.trim(); - if(this.languages.findIndex(lang=>lang.name === value)!==-1){ - // 通知所有作用域刷新到对应的语言包 - await this._refreshScopes(value); - this._settings.activeLanguage = value; - /// 触发语言切换事件 - await this.emit(value); - }else { - throw new Error("Not supported language:"+value) - } - } - /** - * 当切换语言时调用此方法来加载更新语言包 - * @param {*} newLanguage - */ - async _refreshScopes(newLanguage){ - // 并发执行所有作用域语言包的加载 - try{ - const scopeRefreshers = this._scopes.map(scope=>{ - return scope.refresh(newLanguage) - }); - if(Promise.allSettled){ - await Promise.allSettled(scopeRefreshers); - }else { - await Promise.all(scopeRefreshers); - } - }catch(e){ - console.warn("Error while refreshing i18n scopes:",e.message); - } - } - /** - * - * 注册一个新的作用域 - * - * 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数 - * 除了默认语言外,其他语言采用动态加载的方式 - * - * @param {*} scope - */ - async register(scope){ - if(!(scope instanceof i18nScope)){ - throw new TypeError("Scope must be an instance of I18nScope") - } - this._scopes.push(scope); - await scope.refresh(this.activeLanguage); - } - /** - * 注册全局格式化器 - * 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果 - * - * registerFormatters(name,value=>{...}) // 适用于所有语言 - * registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言 - * registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言 - - * @param {*} formatters - */ - registerFormatter(name,formatter,{language="*"}={}){ - if(!typeof(formatter)==="function" || typeof(name)!=="string"){ - throw new TypeError("Formatter must be a function") - } - if(DataTypes$1.includes(name)){ - this.formatters[language].$types[name] = formatter; - }else { - this.formatters[language][name] = formatter; - } - } -} - -var runtime ={ - getInterpolatedVars, - replaceInterpolatedVars, - I18nManager, - translate, - i18nScope, - defaultLanguageSettings, - getDataTypeName, - isNumber, - isPlainObject -}; - -export { runtime as default }; diff --git a/packages/apps/reactapp/src/main.js b/packages/apps/reactapp/src/main.js index bfbb33f..c7dec7f 100644 --- a/packages/apps/reactapp/src/main.js +++ b/packages/apps/reactapp/src/main.js @@ -1,3 +1,2 @@ - -console.log(t("中华民族伟大复兴")) \ No newline at end of file + \ No newline at end of file diff --git a/packages/apps/reactapp/src/main.jsx b/packages/apps/reactapp/src/main.jsx index 6e6f756..e8b683e 100644 --- a/packages/apps/reactapp/src/main.jsx +++ b/packages/apps/reactapp/src/main.jsx @@ -2,12 +2,11 @@ import React,{ } from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' -import "./languages" - +// import "./languages" ReactDOM.render(( - + ), document.getElementById('root') ) diff --git a/packages/apps/reactapp/vite.config.js b/packages/apps/reactapp/vite.config.js index 333a8de..4a27ff7 100644 --- a/packages/apps/reactapp/vite.config.js +++ b/packages/apps/reactapp/vite.config.js @@ -7,7 +7,7 @@ import Voerkai18nPlugin from "@voerkai18n/vite" export default defineConfig({ plugins: [ Inspect(), // localhost:3000/__inspect/ - Voerkai18nPlugin({ debug: true }), + // Voerkai18nPlugin({ debug: true }), react() ] }) diff --git a/packages/cli/templates/entry.js b/packages/cli/templates/entry.js index ae4feea..87c0c1a 100644 --- a/packages/cli/templates/entry.js +++ b/packages/cli/templates/entry.js @@ -1,6 +1,8 @@ {{if moduleType === "esm"}} import messageIds from "./idMap.js" // 语言ID映射文件 -import { translate,VoerkaI18nScope } from "@voerkai18n/runtime" +import runtime from "@voerkai18n/runtime" +const { translate,VoerkaI18nScope } = runtime +import defaultFormatters from "./formatters/{{defaultLanguage}}" {{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}import activeFormatters from "@voerkai18n/runtime/formatters/{{activeLanguage}}.js"{{/if}} import defaultMessages from "./{{defaultLanguage}}.js" {{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}} diff --git a/packages/cli/templates/entry.ts b/packages/cli/templates/entry.ts index 287512e..90ba321 100644 --- a/packages/cli/templates/entry.ts +++ b/packages/cli/templates/entry.ts @@ -1,5 +1,6 @@ import messageIds from "./idMap" // 语言ID映射文件 -import { translate,VoerkaI18nScope } from "@voerkai18n/runtime" +import runtime from "@voerkai18n/runtime" +const { translate,VoerkaI18nScope } = runtime import defaultFormatters from "./formatters/{{defaultLanguage}}" // 默认语言格式化器 {{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}import activeFormatters from "@voerkai18n/runtime/formatters/{{activeLanguage}}"{{/if}} import defaultMessages from "./{{defaultLanguage}}" diff --git a/packages/react/index.d.ts b/packages/react/index.d.ts index 7c5ca16..7aef643 100644 --- a/packages/react/index.d.ts +++ b/packages/react/index.d.ts @@ -1,7 +1,14 @@ +import type {VoerkaI18nSupportedLanguages,VoerkaI18nTranslate,VoerkaI18nScope} from "@voerkai18n/runtime" +import type React from "react" - -export var useVoerkaI18n:()=>{ - activeLanguage:string +export type useVoerkaI18n = ()=>{ + language:string changeLanguage:(newLanguage:string)=>Promise - languages:VoerkaI18nSupportedLanguages -} \ No newline at end of file + languages:VoerkaI18nSupportedLanguages, + t:VoerkaI18nTranslate +} + + +export type VoerkaI18nProvider = React.FC \ No newline at end of file diff --git a/packages/react/index.js b/packages/react/index.js deleted file mode 100644 index 316865f..0000000 --- a/packages/react/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -/** - * MyComponent(){ - * const { activeLanguage, changeLanguage } = useVoerkaI18n() - * - * - * } - * - * - */ -export function useVoerkaI18n() { - - if(!globalThis.VoerkaI18n){ - console.warn("useI18nContext is not provided, use default i18nContext") - } - - const [activeLanguage, setLanguage ] = useState(VoerkaI18n.activeLanguage); - - async function changeLanguage(newLanguage) { - return VoerkaI18n.change(newLanguage) - } - - useEffect(() => { - function onChangeLanguage(newLanguage) { - setLanguage(newLanguage) - } - VoerkaI18n.on(onChangeLanguage) - return () => { - VoerkaI18n.off(onChangeLanguage) - }; - }); - - return { - activeLanguage, - changeLanguage, - languages:VoerkaI18n.languages, - } -} diff --git a/packages/react/index.jsx b/packages/react/index.jsx new file mode 100644 index 0000000..d1212ed --- /dev/null +++ b/packages/react/index.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect,useContext,useCallback} from 'react'; + +export const VoerkaI18nContext = React.createContext({ + languages:null, + language:'zh', + changeLanguage:() => {}, + t:()=>{} +}) +VoerkaI18nContext.displayName = 'VoerkaI18nProvider' + + +export function VoerkaI18nProvider(props){ + const { scope } = props + const [language, setLanguage ] = useState(VoerkaI18n.activeLanguage); + useEffect(() => { + function onChangeLanguage(newLanguage) { + setLanguage(newLanguage) + } + VoerkaI18n.on(onChangeLanguage) + return () => { + VoerkaI18n.off(onChangeLanguage) + }; + }); + const changeLanguage = useCallback((newLanguage) => { + VoerkaI18n.change(newLanguage).then((lng) => { + setLanguage(lng) + }) + },[language]) + return ( + + {props.children} + + ) +} + +export function useVoerkaI18n() { + return useContext(VoerkaI18nContext) +} + +/** + * MyComponent(){ + * const { activeLanguage, changeLanguage } = useVoerkaI18n() + * + * + * } + * + * + */ +// export function useVoerkaI18n() { + +// if(!globalThis.VoerkaI18n){ +// console.warn("useI18nContext is not provided, use default i18nContext") +// } + +// const [activeLanguage, setLanguage ] = useState(VoerkaI18n.activeLanguage); + +// async function changeLanguage(newLanguage) { +// return VoerkaI18n.change(newLanguage) +// } + +// useEffect(() => { +// function onChangeLanguage(newLanguage) { +// setLanguage(newLanguage) +// } +// VoerkaI18n.on(onChangeLanguage) +// return () => { +// VoerkaI18n.off(onChangeLanguage) +// }; +// }); + +// return { +// activeLanguage, +// changeLanguage, +// languages:VoerkaI18n.languages, +// } +// } + + + diff --git a/packages/react/package.json b/packages/react/package.json index 8f80443..791cb1f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -2,7 +2,7 @@ "name": "@voerkai18n/react", "version": "1.0.5", "description": "React支持,提供语言切换等功能", - "main": "index.js", + "main": "index.jsx", "typings": "index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", @@ -11,10 +11,13 @@ "author": "wxzhang", "license": "ISC", "dependencies": { - "react": "^17.0.2" + "@voerkai18n/runtime": "latest" }, "devDependencies": { - "@voerkai18n/autopublish": "workspace:^1.0.2" + + }, + "peerDependencies": { + "react": "^17.0.2" }, "lastPublish": "2023-01-10T17:31:25+08:00" } \ No newline at end of file diff --git a/packages/runtime/index.d.ts b/packages/runtime/index.d.ts index b21b1b9..fa87a70 100644 --- a/packages/runtime/index.d.ts +++ b/packages/runtime/index.d.ts @@ -38,9 +38,7 @@ declare global { fallback?: string } - export interface VoerkaI18nSupportedLanguages { - [key: string]: VoerkaI18nLanguage - } + export type VoerkI18nFormatter = (value: string, ...args: any[]) => string export type VoerkI18nFormatterConfigs = Record @@ -73,7 +71,13 @@ declare global { export var VoerkaI18n: VoerkaI18nManager } - +export interface VoerkaI18nTranslate { + (message: string, ...args: (string | Function)[]): string + (message: string, vars?: Record): string +} +export interface VoerkaI18nSupportedLanguages { + [key: string]: VoerkaI18nLanguage +} export class VoerkaI18nScope { constructor(options: VoerkaVoerkaI18nScopeOptions, callback?: Function) get id(): string // 作用域唯一id @@ -89,7 +93,8 @@ export class VoerkaI18nScope { get formatters(): VoerkI18nFormatters // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} get activeFormatters(): VoerkI18nFormatters // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} get activeFormatterConfig(): VoerkI18nFormatterConfigs // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数 - + get t(): VoerkaI18nTranslate + get translate(): VoerkaI18nTranslate /** * 在全局注册作用域当前作用域 * @param {*} callback 注册成功后的回调 @@ -149,8 +154,8 @@ export var Formatter: CreateFormatterType export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record) => FormatterDefine export var createFlexFormatter: CreateFlexFormatterType export var FlexFormatter: CreateFlexFormatterType -export var getDataTypeName:(value:any) => string +export var getDataTypeName: (value: any) => string export var toDate: (value: any) => Date | number -export var toNumber: (value: any,defaultValue:number) => number +export var toNumber: (value: any, defaultValue: number) => number export var toBoolean: (value: any) => boolean diff --git a/packages/runtime/index.js b/packages/runtime/index.js index 5b8d8f8..dc4fdeb 100644 --- a/packages/runtime/index.js +++ b/packages/runtime/index.js @@ -66,6 +66,7 @@ const defaultLanguageSettings = { await this._refreshScopes(language) // 通知所有作用域刷新到对应的语言包 this._settings.activeLanguage = language await this.emit(language) // 触发语言切换事件 + return language }else{ throw new Error("Not supported language:"+language) } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 41d3fd8..abf40fb 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -12,8 +12,7 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "rollup -c", - "release": "pnpm autopublish" + "build": "rollup -c" }, "exports": { "import": "./dist/index.esm.js", @@ -21,6 +20,12 @@ }, "author": "wxzhang", "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3":"latest", + "core-js":"3.21", + "@babel/runtime":"^7.17.8" + }, + "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.17.5", @@ -30,7 +35,6 @@ "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-node-resolve": "^13.1.3", - "@voerkai18n/autopublish": "workspace:^1.0.2", "deepmerge": "^4.2.2", "jest": "^27.5.1", "rollup": "^2.69.0", diff --git a/packages/runtime/rollup.config.js b/packages/runtime/rollup.config.js index 2894850..37472fb 100644 --- a/packages/runtime/rollup.config.js +++ b/packages/runtime/rollup.config.js @@ -12,6 +12,8 @@ export default [ { file: 'dist/index.esm.js', format:"esm", + exports:"default", + esModule:"if-default-prop", sourcemap:true }, { @@ -29,28 +31,8 @@ export default [ exclude: 'node_modules/**' }), clear({targets:["dist"]}), - terser() + // terser() ], external:["@babel/runtime"] - } - // { - // input: './index.js', - // output: [ - // { - // file: 'dist/runtime.cjs', - // exports:"auto", - // format:"cjs", - // sourcemap:true - // }, - // { - // file: 'dist/runtime.mjs', - // exports:"default", - // format:"esm", - // sourcemap:true - // } - // ], - // plugins:[ - // commonjs() - // ] - // } + } ] \ No newline at end of file diff --git a/packages/runtime/scope.js b/packages/runtime/scope.js index 56bff9b..6d69c54 100644 --- a/packages/runtime/scope.js +++ b/packages/runtime/scope.js @@ -1,4 +1,5 @@ const { DataTypes,isPlainObject, isFunction, getByPath, deepMixin,deepClone } = require("./utils"); +const { translate } = require("./translate") module.exports = class VoerkaI18nScope { constructor(options = {}, callback) { @@ -32,6 +33,7 @@ module.exports = class VoerkaI18nScope { languages : options.languages, }); } + this._t = translate.bind(this) this._global = globalThis.VoerkaI18n; this._initFormatters(this.activeLanguage) // 初始化活动的格式化器 this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包 @@ -51,7 +53,8 @@ module.exports = class VoerkaI18nScope { get formatters() { return this._formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数 - + get translate(){return this._t} + get t(){return this._t} /** * 对输入的语言配置进行处理 * - 将en配置为默认回退语言 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8419a77..3eb518a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,24 +65,30 @@ importers: packages/apps/app: specifiers: '@voerkai18n/cli': workspace:^1.0.6 - '@voerkai18n/runtime': ^1.0.0 + '@voerkai18n/runtime': workspace:^1.0.0 dependencies: '@voerkai18n/cli': link:../../cli '@voerkai18n/runtime': link:../../runtime packages/apps/reactapp: specifiers: + '@babel/runtime-corejs3': ^7.20.7 '@vitejs/plugin-react': ^1.0.7 '@voerkai18n/cli': workspace:^1.0.11 '@voerkai18n/react': workspace:^1.0.0 + '@voerkai18n/runtime': workspace:^1.1.9 '@voerkai18n/vite': workspace:^1.0.7 + core-js: ^3.27.1 react: ^17.0.2 react-dom: ^17.0.2 vite: ^2.9.0 vite-plugin-inspect: ^0.4.3 dependencies: + '@babel/runtime-corejs3': 7.20.7 '@voerkai18n/react': link:../../react + '@voerkai18n/runtime': link:../../runtime '@voerkai18n/vite': link:../../vite + core-js: 3.27.1 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 devDependencies: @@ -201,12 +207,9 @@ importers: packages/react: specifiers: - '@voerkai18n/autopublish': workspace:^1.0.2 - react: ^17.0.2 + '@voerkai18n/runtime': latest dependencies: - react: 17.0.2 - devDependencies: - '@voerkai18n/autopublish': link:../autopublish + '@voerkai18n/runtime': link:../runtime packages/runtime: specifiers: @@ -215,15 +218,19 @@ importers: '@babel/plugin-transform-runtime': ^7.17.0 '@babel/preset-env': ^7.16.11 '@babel/runtime': ^7.17.8 + '@babel/runtime-corejs3': latest '@rollup/plugin-babel': ^5.3.1 '@rollup/plugin-commonjs': ^21.0.2 '@rollup/plugin-node-resolve': ^13.1.3 - '@voerkai18n/autopublish': workspace:^1.0.2 + core-js: '3.21' deepmerge: ^4.2.2 jest: ^27.5.1 rollup: ^2.69.0 rollup-plugin-clear: ^2.0.7 rollup-plugin-terser: ^7.0.2 + dependencies: + '@babel/runtime-corejs3': 7.20.7 + core-js: 3.21.1 devDependencies: '@babel/cli': 7.18.10_@babel+core@7.18.10 '@babel/core': 7.18.10 @@ -233,7 +240,6 @@ importers: '@rollup/plugin-babel': 5.3.1_tui6liyexu3zy4m5r2rytc7ixu '@rollup/plugin-commonjs': 21.1.0_rollup@2.77.2 '@rollup/plugin-node-resolve': 13.3.0_rollup@2.77.2 - '@voerkai18n/autopublish': link:../autopublish deepmerge: 4.2.2 jest: 27.5.1 rollup: 2.77.2 @@ -1478,11 +1484,19 @@ packages: core-js-pure: 3.24.1 regenerator-runtime: 0.13.9 + /@babel/runtime-corejs3/7.20.7: + resolution: {integrity: sha512-jr9lCZ4RbRQmCR28Q8U8Fu49zvFqLxTY9AMOUz+iyMohMoAgpEcVxY+wJNay99oXOpOcCTODkk70NDN2aaJEeg==} + engines: {node: '>=6.9.0'} + dependencies: + core-js-pure: 3.27.1 + regenerator-runtime: 0.13.11 + dev: false + /@babel/runtime/7.18.6: resolution: {integrity: sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.9 + regenerator-runtime: 0.13.11 dev: true /@babel/runtime/7.18.9: @@ -2322,7 +2336,7 @@ packages: '@umijs/utils': 3.5.32 ansi-html: 0.0.7 core-js: 3.6.5 - core-js-pure: 3.24.1 + core-js-pure: 3.27.1 error-stack-parser: 2.1.4 es-module-lexer: 0.7.1 es5-imcompatible-versions: 0.1.74 @@ -4465,16 +4479,31 @@ packages: resolution: {integrity: sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==} requiresBuild: true + /core-js-pure/3.27.1: + resolution: {integrity: sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw==} + requiresBuild: true + /core-js/2.6.12: resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. requiresBuild: true dev: true + /core-js/3.21.1: + resolution: {integrity: sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + /core-js/3.24.1: resolution: {integrity: sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==} requiresBuild: true + /core-js/3.27.1: + resolution: {integrity: sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==} + requiresBuild: true + dev: false + /core-js/3.6.5: resolution: {integrity: sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. @@ -9965,6 +9994,9 @@ packages: resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} dev: true + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + /regenerator-runtime/0.13.5: resolution: {integrity: sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==} dev: true