增加React封装

This commit is contained in:
wxzhang 2022-04-14 21:13:59 +08:00
parent 72a9db2973
commit 5eafddb562
32 changed files with 3456 additions and 8324 deletions

View File

@ -34,7 +34,7 @@
"shelljs": "^0.8.5",
"vinyl": "^2.2.1",
"vuepress": "^2.0.0-beta.38",
"vuepress-theme-hope": "^2.0.0-beta.36"
"vuepress-theme-hope": "^2.0.0-beta.38"
},
"dependencies": {
"cross-env": "^7.0.3",

24
packages/apps/reactapp/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,22 @@
{
"name": "reactapp",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@voerkai18n/react": "workspace:^1.0.0",
"@voerkai18n/vite": "workspace:^1.0.7",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^1.0.7",
"@voerkai18n/cli": "workspace:^1.0.11",
"vite": "^2.9.0",
"vite-plugin-inspect": "^0.4.3"
}
}

View File

@ -0,0 +1,42 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
button {
font-size: calc(10px + 2vmin);
}

View File

@ -0,0 +1,33 @@
import { useState,useRef,useEffect } from 'react'
import logo from './logo.svg'
import './App.css'
import "./main"
import Banner from './components/Banner'
import LanguageConfigurator from "./components/LanguageConfigurator.jsx"
import { useVoerkaI18n } from "@voerkai18n/react";
function App() {
const { activeLanguage, changeLanguage, languages } = useVoerkaI18n();
return (
<div className="App">
<header className="App-header">
<div>{t("Voerkai1n是一个功能强大设计精良的国际化解决方案")}</div>
<LanguageConfigurator/>
<div>
<Banner memo={t("这是一个测试")} />
<a
className="App-link"
href="https://zhangfisher.github.io/voerka-i18n"
target="_blank"
rel="noopener noreferrer"
>
学习VoerkaI18n
</a>
</div>
</header>
</div>
)
}
export default App

View File

@ -0,0 +1,12 @@
import { useState } from 'react'
function Banner( props ) {
return (
<div>
<h2>{t("一字一世界,一笔一乾坤,汉字是这个星球上最美的语言")}</h2>
<h3>{props.memo}</h3>
</div>
)
}
export default Banner

View File

@ -0,0 +1,26 @@
import { useState } from "react";
import { useVoerkaI18n } from "@voerkai18n/react"
function LanguageConfigurator(props) {
const { activeLanguage, changeLanguage, languages } = useVoerkaI18n();
return (
<div>
<div>{t("当前语言")}{ activeLanguage }</div>
<div>
{languages.map((lang) => {
return (
<button
type="button"
key={lang.name}
onClick={() => changeLanguage(lang.name)}
>
{lang.title}
</button>
);
})}
</div>
</div>
);
}
export default LanguageConfigurator;

View File

@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,8 @@
export default {
"1": "Voerkai1n is a powerful and well-designed international solution",
"2": "The great rejuvenation of the Chinese nation",
"3": "Those who violate China will be punished even if they are far away",
"4": "This is test",
"5": "Chinese characters are the most beautiful language on the planet",
"6": "Current Language"
}

View File

@ -0,0 +1,108 @@
/**
格式化器用来对翻译文本内容中的插值变量进行格式化
比如将一个数字格式化为货币格式或者将一个日期格式化为友好的日期格式
- 以下定义了一些格式化器在中文场景下会启用这些格式化器
import dayjs from "dayjs";
const formatters = {
"*":{ // 在所有语言下生效的格式化器
$types:{...}, // 只作用于特定数据类型的默认格式化器
.... // 全局格式化器
},
zh:{
// 只作用于特定数据类型的格式化器
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
},
date:(value)=>dayjs(value).format("YYYY年MM月DD日")
bjTime:(value)=>"北京时间"+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
},
en:{
$types:{
Date:(value)=>dayjs(value).format("YYYY/MM/DD HH:mm:ss"), // 默认的格式化器
},
date:(value)=>dayjs(value).format("YYYY/MM/DD")
bjTime:(value)=>"BeiJing "+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
}
}
- 在翻译函数中使用格式化器的方法,示例如下
t("Now is { value | date | bjTime }",{value: new Date()})
其等效于
t(`Now is ${bjTime(date(value))",{value: new Date()})
由于value分别经过两个管道符转换上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符
最终的输出结果
中文: "现在是北京时间2022年3月1日"
英文: "Now is BeiJing 2022/03/01"
*
*/
export default{
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
},
zh:{
$types:{
// 所有类型的默认格式化器
// "*":{
// },
// Date:{
// },
// Number:{
// },
// String:{
// },
// Array:{
// },
// Object:{
// }
}
},
en:{
$types:{
// 所有类型的默认格式化器
// "*":{
// },
// Date:{
// },
// Number:{
// },
// String:{
// },
// Array:{
// },
// Object:{
// }
}
}
}

View File

@ -0,0 +1,8 @@
export default {
"Voerkai1n是一个功能强大设计精良的国际化解决方案": 1,
"中华民族伟大复兴": 2,
"犯我中华者,虽远必诛!": 3,
"这是一个测试": 4,
"一字一世界,一笔一乾坤,汉字是这个星球上最美的语言": 5,
"当前语言": 6
}

View File

@ -0,0 +1,47 @@
import messageIds from "./idMap.js"
import runtime from "./runtime.js"
const { translate,i18nScope } = runtime
import formatters from "./formatters.js"
import defaultMessages from "./zh.js"
const activeMessages = defaultMessages
// 语言配置文件
const scopeSettings = {
"languages": [
{
"name": "zh",
"title": "中文"
},
{
"name": "en",
"title": "英语"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
// 语言作用域
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 scopedTtranslate = translate.bind(scope)
export {
scopedTtranslate as t,
scope as i18nScope
}

View File

@ -0,0 +1,936 @@
/**
* 判断是否是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;i<this._callbacks.length;i++){
if(this._callbacks[i]===callback ){
this._callbacks.splice(i,1);
}
}
}
offAll(){
this._callbacks = [];
}
async emit(...args){
if(Promise.allSettled){
await Promise.allSettled(this._callbacks.map(cb=>cb(...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,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
*
*
* 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;i<args.length;i+=2){
if(args[i]===value){
return args[i+1]
}
}
if(args.length >0 && (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*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\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 [[<formatterName>,[<arg>,<arg>,...]]]
*/
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 || "";
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
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=默认语言包={<id>:<message>}
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 };

View File

@ -0,0 +1,15 @@
{
"languages": [
{
"name": "zh",
"title": "中文"
},
{
"name": "en",
"title": "英语"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}

View File

@ -0,0 +1,38 @@
{
"Voerkai1n是一个功能强大设计精良的国际化解决方案": {
"en": "Voerkai1n is a powerful and well-designed international solution",
"$file": [
"App.jsx"
]
},
"中华民族伟大复兴": {
"en": "The great rejuvenation of the Chinese nation",
"$file": [
"modules\\module1.js"
]
},
"犯我中华者,虽远必诛!": {
"en": "Those who violate China will be punished even if they are far away",
"$file": [
"modules\\module2.js"
]
},
"这是一个测试": {
"en": "This is test",
"$file": [
"App.jsx"
]
},
"一字一世界,一笔一乾坤,汉字是这个星球上最美的语言": {
"en": "Chinese characters are the most beautiful language on the planet",
"$file": [
"components\\Banner.jsx"
]
},
"当前语言": {
"en": "Current Language",
"$file": [
"components\\LanguageConfigurator.jsx"
]
}
}

View File

@ -0,0 +1,8 @@
export default {
"1": "Voerkai1n是一个功能强大设计精良的国际化解决方案",
"2": "中华民族伟大复兴",
"3": "犯我中华者,虽远必诛!",
"4": "这是一个测试",
"5": "一字一世界,一笔一乾坤,汉字是这个星球上最美的语言",
"6": "当前语言"
}

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
console.log(t("中华民族伟大复兴"))

View File

@ -0,0 +1,13 @@
import React,{ } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import "./languages"
ReactDOM.render((
<React.StrictMode>
<App />
</React.StrictMode>
), document.getElementById('root')
)

View File

@ -0,0 +1,2 @@
export const title = t("中华民族伟大复兴")

View File

@ -0,0 +1,3 @@
export const title = t("犯我中华者,虽远必诛!")

View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import Inspect from 'vite-plugin-inspect'
import Voerkai18nPlugin from "@voerkai18n/vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
Inspect(), // localhost:3000/__inspect/
Voerkai18nPlugin({ debug: true }),
react()
]
})

View File

@ -9,6 +9,7 @@
},
"dependencies": {
"@voerkai18n/cli": "workspace:^1.0.11",
"@voerkai18n/vite": "workspace:^1.0.7",
"@voerkai18n/vue": "workspace:^1.0.0",
"vue": "^3.2.31"
},
@ -25,7 +26,6 @@
"babel-preset-env": "^1.7.0",
"core-js": "^3.21.1",
"vite": "^2.8.6",
"vite-plugin-babel": "^1.0.0",
"vite-plugin-inspect": "^0.4.3"
}
}

View File

@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import { babel } from '@rollup/plugin-babel';
import Inspect from 'vite-plugin-inspect'
import Voerkai18nPlugin from "../../vite"
import Voerkai18nPlugin from "@voerkai18n/vite"
// https://vitejs.dev/config/

39
packages/react/index.js Normal file
View File

@ -0,0 +1,39 @@
import React, { useState, useEffect,useCallback } from 'react';
/**
* MyComponent(){
* const { activeLanguage, changeLanguage } = useVoerkaI18n()
*
*
* }
*
*
*/
export function useVoerkaI18n(i18nScope) {
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,
}
}

View File

@ -8,9 +8,7 @@
},
"author": "",
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
"dependencies": {
"react": "^17.0.2"
}
}

View File

@ -100,8 +100,11 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
autoImport: true, // 是否自动导入t函数
debug:false, // 是否输出调试信息,当=true时在控制台输出转换匹配的文件清单
patterns:[
"!\.(svg|css|json|scss|less|sass)$",
"!(?<!.vue\?.*).(css|json|scss|less|sass)$", // 排除所有css文件
/\.vue(\?.*)?/, // 所有vue文件
"!(?<!.jsx\?.*).(css|json|scss|less|sass)$",
/\.jsx(\?.*)?/,
] // 提取范围
},opts)
@ -110,6 +113,9 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
let projectRoot = getProjectRootFolder(options.location)
let languageFolder = getProjectLanguageFolder(projectRoot)
if(!fs.existsSync(languageFolder)){
console.warn(`Voerkai18n语言文件夹不存在@voerkai18n/vite未启用`)
}
if(debug){
console.log("Project root: ",projectRoot)
console.log("Language folder: ",languageFolder)
@ -119,7 +125,13 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
basePath:projectRoot,
debug:debug
})
let idMap = readIdMapFile(options)
let idMap
try{
idMap = readIdMapFile(options)
}catch(e){
console.warn("读取idMap.js文件失败@voerkai18n/vite未启用")
return
}
return {
name: 'voerkai18n',
@ -140,6 +152,8 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
importSource = "./" + importSource
}
importSource=importSource.replace("\\","/")
// 转换Vue文件
if(path.extname(id)==".vue"){
// 优先在<script setup></script>中导入
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
if(setupScriptRegex.test(code)){
@ -147,6 +161,9 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
}else{// 如果没有<script setup>则在<script></script>中导入
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
}
}else{// 普通js文件需要添加到最前面
code = code = `import { t } from '${importSource}';\n${code}`
}
}
return {
code,

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/vite",
"version": "1.0.7",
"version": "1.0.8",
"description": "VoerkaI18n plugin for Vite",
"main": "index.js",
"scripts": {
@ -10,10 +10,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"@voerkai18n/utils": "workspace:^1.0.0"
"@voerkai18n/utils": "workspace:^1.0.10"
},
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-04-10T17:21:48+08:00"
"lastPublish": "2022-04-14T20:04:32+08:00"
}

1996
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8281
yarn.lock

File diff suppressed because it is too large Load Diff