This commit is contained in:
fisher 2022-04-01 16:47:53 +08:00
parent b22d2ddaf7
commit 3aaba68e2f
56 changed files with 5010 additions and 1445 deletions

View File

@ -15,19 +15,23 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/core": "^7.17.8",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"dayjs": "^1.10.8",
"dayjs": "^1.11.0",
"deepmerge": "^4.2.2",
"fs-extra": "^10.0.1",
"gulp": "^4.0.2",
"jest": "^27.5.1",
"rollup": "^2.69.0",
"logsets": "^1.0.8",
"rollup": "^2.70.1",
"rollup-plugin-clear": "^2.0.7",
"shelljs": "^0.8.5",
"vinyl": "^2.2.1"
},
"dependencies": {
"inquirer": "^8.2.2"
}
}

View File

@ -0,0 +1,17 @@
const i18nPlugin =require("@voerkai18n/babel")
module.exports = {
presets: [
"@babel/preset-env"
],
plugins: [
[
i18nPlugin,
{
// 可选,指定语言文件存放的目录,即保存编译后的语言文件的文件夹
// 可以指定相对路径,也可以指定绝对路径
// location:"",
autoImport:"/src/languages/index.js"
}
]
]
}

View File

@ -8,11 +8,24 @@
"preview": "vite preview"
},
"dependencies": {
"@voerkai18n/cli": "workspace:^1.0.6",
"vue": "^3.2.25"
"@voerkai18n/cli": "workspace:^1.0.11",
"@voerkai18n/vue": "workspace:^1.0.0",
"vue": "^3.2.31"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"vite": "^2.8.0"
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.8",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/runtime": "^7.17.8",
"@babel/runtime-corejs3": "^7.17.8",
"@rollup/plugin-babel": "^5.3.1",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.4",
"@voerkai18n/babel": "workspace:^1.0.0",
"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

@ -1,17 +1,51 @@
<script setup>
import {reactive } from 'vue'
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import { t } from './languages'
import China from './components/china.vue'
console.log(t("Hello world!"))
let messages = reactive({
name: t('VoerkaI18n多语言解决方案 ')
})
</script>
console.log("App=",messages.name)
setTimeout(() => {
messages.name = 'Vue App 2'
}, 5000)
// console.log(t("Hello world!"))
</script>
<script>
import {reactive } from 'vue'
export default {
inject: ['i18n'],
data() {
return {
language: this.i18n.activeLanguage,
}
},
watch: {
},
methods: {
async changeLanguage(value) {
this.i18n.activeLanguage = value
//this.activeLanguage = value
// await this.i18n.change(this.language)
}
}
}
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3 + Vite" />
<h1>{{ t("中华人民共和国")}} </h1>
<h2>{{ t("迎接中华民族的伟大复兴")}} </h2>
<China :title="t('中华人民共和国')" />
<h5>默认语言{{ i18n.defaultLanguage }}</h5>
<h5>当前语言{{ i18n.activeLanguage.value }}</h5>
<button v-for="lng of i18n.languages" @click="i18n.activeLanguage = lng.name" style="padding:8px;margin:8px;cursor:pointer" :style="{'outline' : lng.name === i18n.activeLanguage.value ? '2px red solid' : ''}" >{{ lng.title }}</button>
</template>
<style>

View File

@ -1,40 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Documentation
</a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Documentation</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<style scoped>
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup>
import { ref } from 'vue'
defineProps({
title: String
})
const count = ref(0)
</script>
<template>
<div style="padding:16px;border:1px red solid;margin:0 auto;margin-bottom:16px;width:600px;">
<h2>{{ title }}</h2>
<h3>{{ t("成立于{}年",1949)}} </h3>
<h3>{{ t("首都:北京")}} </h3>
</div>
</template>
<style scoped>
a {
color: #42b983;
}
</style>

View File

@ -1,3 +1,8 @@
export default {
"1": "Hello world!"
"1": "Hello world!",
"2": "中华人民共和国",
"3": "迎接中华民族的伟大复兴",
"4": "成立于{}年",
"5": "首都:北京",
"6": "VoerkaI18n多语言解决方案 "
}

View File

@ -1,3 +1,8 @@
export default {
"1": "Hello world!"
"1": "Hello world!",
"2": "The People's Republic of China",
"3": "Welcome the great rejuvenation of the Chinese nation",
"4": "Founded in {}",
"5": "Capital: Beijing",
"6": "VoerkaI18n多语言解决方案 "
}

View File

@ -1,3 +1,8 @@
export default {
"Hello world!": 1
"Hello world!": 1,
"中华人民共和国": 2,
"迎接中华民族的伟大复兴": 3,
"成立于{}年": 4,
"首都:北京": 5,
"VoerkaI18n多语言解决方案 ": 6
}

View File

@ -38,10 +38,10 @@ const scope = new i18nScope({
}
})
// 翻译函数
const t = translate.bind(scope)
const scopedTtranslate = translate.bind(scope)
export {
t,
i18nScope as scope
scopedTtranslate as t,
scope as i18nScope
}

View File

@ -4,5 +4,35 @@
"$file": [
"App.vue"
]
},
"中华人民共和国": {
"en": "The People's Republic of China",
"$file": [
"App.vue"
]
},
"迎接中华民族的伟大复兴": {
"en": "Welcome the great rejuvenation of the Chinese nation",
"$file": [
"App.vue"
]
},
"成立于{}年": {
"en": "Founded in {}",
"$file": [
"components\\china.vue"
]
},
"首都:北京": {
"en": "Capital: Beijing",
"$file": [
"components\\china.vue"
]
},
"VoerkaI18n多语言解决方案 ": {
"en": "VoerkaI18n多语言解决方案 ",
"$file": [
"App.vue"
]
}
}

View File

@ -1,7 +1,8 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
import i18nPlugin from '@voerkai18n/vue'
import { t,i18nScope } from './languages'
const app = createApp(App)
app.use(i18nPlugin,{ t,i18nScope })
app.mount('#app')

View File

@ -1,7 +1,23 @@
import { defineConfig } from 'vite'
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"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
plugins: [
//legacy(),
//babel(),
Inspect(), // localhost:3000/__inspect/
Voerkai18nPlugin({debug:true}),
vue()
],
resolve:{
alias:{
//"voerkai18n":"./languages/index.js"
}
}
})

View File

@ -0,0 +1,59 @@
/**
*
* 读取当前项目的<package.json>.version,然后运行git tag [Version]在当前工程中添加版本tag
*
* 添加会判断是否已经存在该版本标签如果存在则不添加
* 本脚本用在发布后自动添加版本标签
*
* addGitTag
*
* {
* scripts:{
* "publish":"node version patch",
* // 在发布后在当前工程中添加版本标签
* "postpublish":"node addGitTag
* "
* }
*
* }
*/
const fs = require("fs-extra");
const path = require("path");
const dayjs = require("dayjs");
const shelljs = require("shelljs");
// 可选的,指定工程目录
const args = process.argv.slice(2);
const projectFolder = args.length > 0 ? (path.isAbsolute(args[0]) ? args[0] : path.join(process.cwd(),args[0])) : process.cwd();
// 读取当前项目的package.json
const pkgFile = path.join(projectFolder, "package.json")
const pkg = fs.readJSONSync(pkgFile);
// 切换到当前目录
shelljs.cd(projectFolder);
// 读取当前版本
const tagInfo = shelljs.exec(`git describe --tags --match=V${pkg.version}`, {silent: true}).stdout.trim();
if(tagInfo.length===0){
console.log(`未发现版本标签V${pkg.version}`);
if(shelljs.exec(`git tag V${pkg.version}`, {silent: false}).code>0){
console.log(`添加版本标签<V${pkg.version}>失败`)
}else{
console.log(`已添加版本标签:V${pkg.version}`)
}
}else {
console.log(`已发现版本标签:${tagInfo}`);
}
// 更新

View File

@ -0,0 +1,113 @@
/**
*
* 自动发布工具
*
* 自动读取当前项目的版本标签如果不致则进行
*
*
*
* 支持两种发布方式
*
* 1. 自动发布
*
* 当选择自动发布时会读取当前项目下的所有文件然后
*
* 根据每个包的最后提交时间和最近一次发布来决定是否自动发布
* 每个包发布时均需要修改package.json.lastPublish值
*
* 2. 手动发布
* 手动选择要发布的包
*
* pnpm add @voerkai18n/autopublish
*
* // 自动发布
* autopublish
*
*/
const inquirer = require("inquirer");
const fs = require("fs-extra");
const shelljs = require("shelljs");
const path = require("path");
const createLogger = require("logsets");
const commander = require("commander");
const dayjs = require("dayjs");
const relativeTime = require("dayjs/plugin/relativeTime");
dayjs.extend(relativeTime);
const logger = createLogger();
const program = commander()
program
.version("1.0.0")
.option("-a, --auto", "自动发布")
.option("-a, --auto", "启用自动发布")
.option("-m, --monorepo", "是否是monorepo工程")
.option("-i, --independent", "每个包独立发布")
.option("-e, --excludePackages", "多包场景时忽略的包")
.action((options) => {
}))
// 读取packages所有包
let excludePackages = ["apps"];
let packages = [];
fs.readdirSync(path.join(__dirname, "packages")).forEach(function(packageName){
if (excludePackages.includes(packageName)) return;
const packageFolder = path.join(__dirname, "packages", packageName);
// 读取指定包
const { version: packageVersion,lastPublishTime } = require(path.join(packageFolder,"package.json"));
// 最后一次提交的时间
const packageLastCommitTime = shelljs.exec(`git log -1 --format=%cd --date=iso ${packageFolder}`, {silent: true}).stdout.trim();
// 最后发布的时间
packages.push({
name: `@voerkai18n/${packageName.padEnd(20)}\t\t( Version:${packageVersion}, LastCommit: ${dayjs(packageLastCommitTime).fromNow()})`,
value: {
packageFolder,
lastCommitTime: packageLastCommitTime,
version: packageVersion,
},
});
});
inquirer
.prompt([
{
type: "confirm",
name: "autoPublish",
message: "是否自动发布?",
default: true,
},
{
type: "checkbox",
name: "selectPackages",
message: "请选择要发布的库:",
choices: packages,
when: function (answer) {
return !answer.autoPublish;
},
},
])
.then((answers) => {
console.log(answers);
})
.catch((error) => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
});

View File

@ -0,0 +1,19 @@
{
"name": "@voerkai18n/publish",
"version": "1.0.0",
"description": "发布项目工具",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"release": "npm version patch && pnpm publish --no-git-checks --access public",
"postpublish": "git push --follow-tags && npm run build && npm publish"
},
"author": "",
"license": "ISC",
"bin": {
"publish": "./index.js"
},
"dependencies": {
"commander": "^9.0.0"
}
}

View File

@ -0,0 +1,119 @@
/**
* 用于多包环境下的自动发布
*
* autopublish
*
* 1.在package.json中添加scripts
* {
* scripts:{
* "publish":"autopublish [options]",
* }
* }
* 2. 参数
* -q: 默认情况下会比对最后一次发布的时间来决定是否自动发布
* -q参数被指定时会询问用户
*
*
*
*
*/
const fs = require("fs-extra");
const path = require("path");
const shelljs = require("shelljs");
const createLogger = require("logsets");
const { Command } = require('commander');
const dayjs = require("dayjs");
const relativeTime = require("dayjs/plugin/relativeTime");
dayjs.extend(relativeTime);
const logger = createLogger();
const program =new Command()
const packages = [
"git log --format=%cd --date=iso -1 -- packages/babel/package.json",
"git log --format=%cd --date=iso -1 -- packages/cli/package.json",
"git log --format=%cd --date=iso -1 -- packages/runtime/package.json",
"git log --format=%cd --date=iso -1 -- packages/formatters/package.json",
"git log --format=%cd --date=iso -1 -- packages/vue/package.json",
"git log --format=%cd --date=iso -1 -- packages/vite/package.json",
"git log --format=%cd --date=iso -1 -- packages/autopublish/package.json",
"git log --format=%cd --date=iso -1 -- packages/utils/package.json"
]
function getPackages(){
let workspaceRoot = process.cwd()
if(!fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))){
console.log("命令只能在工作区根目录下执行")
return
}
// 获取包最后一次提交的时间
const getLastCommitScript = "git log --format=%cd --date=iso -1 -- {packagePath}"
return fs.readdirSync(path.join(workspaceRoot,"packages")).map(packageName=>{
const pkgFile = path.join(workspaceRoot,"packages",packageName,"package.json")
if(fs.existsSync(pkgFile)){
const { name, version }= fs.readJSONSync(pkgFile)
const lastCommit = shelljs.exec(getLastCommitScript.replace("{packagePath}",`packages/${packageName}/package.json`), { silent: true }).stdout.trim()
return {
name,
version,
lastCommit
}
}
}).filter(pkgInfo=>pkgInfo)
}
function assertInWorkspaceRoot(){
const workspaceRoot = process.cwd()
if(!fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))){
throw new Error("命令只能在工作区根目录下执行")
}
}
program
.command("publish")
.description("发布当前工作区下的包")
.option("-f, --force", "强制发布")
.option("-q, --query", "询问是否发布,否则会自动发布")
.option("-i, --version-increment-step [value]", "版本增长方式取值major,minor,patch",'patch')
.action(options => {
const {versionIncrementStep} = options
const packageFolder = process.cwd()
const pkgFile = path.join(packageFolder,"package.json")
// 由于每次发布均会更新npm version patch并且需要提交代码
const lastCommit = shelljs.exec(`git log --format=%cd --date=iso -1 -- ${pkgFile}`, { silent: true }).stdout.trim()
// 增加版本号
shelljs.exec(`npm version ${versionIncrementStep}`, { silent: true }).stdout.trim()
//
shelljs.exec(`pnpm publish --access publish`, { silent: true }).stdout.trim()
})
program
.command("list")
.description("列出各个包的最后一次提交时间和版本信息")
.action(options => {
assertInWorkspaceRoot()
getPackages().forEach(package => {
//console.log(`${package.name}`)
if(package.lastCommit){
console.log(`${package.name.padEnd(16)}\tVersion: ${package.version.padEnd(12)} lastCommit: ${dayjs(package.lastCommit).format("YYYY/MM/DD ")}(${dayjs(package.lastCommit).fromNow()}) `)
}else{
console.log(`${package.name.padEnd(16)}\tVersion: ${package.version.padEnd(12)} lastCommit: None `)
}
})
})
program.parse(process.argv);

View File

@ -0,0 +1,43 @@
/**
* 用于多包环境下的自动发布
*
*
* {
* scripts:{
* "publish":"autopublish [options]",
* }
* }
*/
const fs = require("fs-extra");
const path = require("path");
const dayjs = require("dayjs");
const shelljs = require("shelljs");
const shelljs = require("shelljs");
const createLogger = require("logsets");
const commander = require("commander");
const dayjs = require("dayjs");
const relativeTime = require("dayjs/plugin/relativeTime");
dayjs.extend(relativeTime);
const logger = createLogger();
const program = commander()
program
.version("1.0.0")
.option("-v, --auto", "自动发布")
.option("-a, --auto", "启用自动发布")
.option("-p, --push []", "发布前执行git push")
.option("-m, --commit [message]", "发布前执行git commit")
.option("-v, --version", "默认版本升级取值major,minor,patch")
.action((options) => {
}))

View File

@ -0,0 +1,5 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
`@voerkai18n/autopublish`辅助进行自动发布
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

View File

@ -0,0 +1,24 @@
const shelljs = require("shelljs");
const path = require("path")
/**
* 检测当前工程是否是git工程
*/
function isGitRepo(){
return shelljs.exec("git status", {silent: true}).code === 0;
}
function getProjectName(){
const pkgFile = path.join(process.cwd(), "package.json")
const pkg = fs.readJSONSync(pkgFile);
return pkg.name;
}
module.exports ={
isMonorepo,
isGitRepo
}

View File

@ -21,15 +21,15 @@
*/
const fs = require("fs");
const path = require("path");
const { isPlainObject } = require("./utils");
const pathobj = require("path");
const { getProjectRootFolder } = require("@voerkai18n/utils")
const DefaultI18nPluginOptions = {
translateFunctionName:"t", // 默认的翻译函数名称
// 翻译文件存放的目录,即编译后的语言文件的文件夹
// 默认从当前目录下的languages文件夹中导入
// 如果不存在则会
location:"./languages",
// 默认从当前目录下的languages文件夹中导入
location:"./",
// 自动创建import {t} from "#/languages" 或 const { t } = require("#/languages")
// 如果此值是空则不会自动创建import语句
autoImport:"#/languages",
@ -42,6 +42,7 @@ const DefaultI18nPluginOptions = {
idMap:{}
}
/**
* 判断当前是否是一个esmodule判断依据是
* - 包含import语句
@ -56,7 +57,6 @@ function isEsModule(path){
}
}
}
/**
* 判断是否导入了翻译函数
* import { t } from "./i18n"
@ -67,7 +67,7 @@ function isEsModule(path){
function hasImportTranslateFunction(path){
for(let ele of path.node.body){
if(ele.type==="ImportDeclaration"){
if(ele.specifiers.findIndex(s => s.type === "ImportSpecifier" && s.imported.name ==TRANSLATE_FUNCTION_NAME && s.local.name===TRANSLATE_FUNCTION_NAME)>-1){
if(Array.isArray(ele.specifiers) && ele.specifiers.findIndex(s => (s.type === "ImportSpecifier") && (s.imported.name ==this.translateFunctionName) && (s.local.name===this.translateFunctionName))>-1){
return true
}
}
@ -76,24 +76,39 @@ function hasImportTranslateFunction(path){
function hasRequireTranslateFunction(path){
for(let ele of path.node.body){
if(ele.type==="VariableDeclaration"){
if(ele.specifiers.findIndex(s => s.type === "ImportSpecifier" && s.imported.name ==TRANSLATE_FUNCTION_NAME && s.local.name===TRANSLATE_FUNCTION_NAME)>-1){
if(Array.isArray(ele.specifiers) && ele.specifiers.findIndex(s => s.type === "ImportSpecifier" && s.imported.name ==this.translateFunctionName && s.local.name===this.translateFunctionName)>-1){
return true
}
}
}
}
function getIdMap(options){
const { idMap,location } = options
if(isPlainObject(idMap) && Object.keys(idMap).length>0){
/**
* 读取idMap.js文件
* @param {*} options
* @returns
*/
function readIdMapFile(options){
let { idMap,location } = options
if(typeof(idMap)==="object" && Object.keys(idMap).length>0){
return idMap
}else{
let idMapFiles = [
path.join((path.isAbsolute(location) ? location : path.join(process.cwd(),location)),"idMap.js"),
path.join((path.isAbsolute(location) ? path.join(location,"languages") : path.join(process.cwd(),location,"languages")),'idMap.js')
]
let searchIdMapFiles = []
if(!pathobj.isAbsolute(location)){
location = pathobj.join(process.cwd(),location)
}
searchIdMapFiles.push(pathobj.join(location,"src","languages/idMap.js"))
searchIdMapFiles.push(pathobj.join(location,"languages/idMap.js"))
searchIdMapFiles.push(pathobj.join(location,"idMap.js"))
let projectRoot = getProjectRootFolder(location)
searchIdMapFiles.push(pathobj.join(projectRoot,"src","languages/idMap.js"))
searchIdMapFiles.push(pathobj.join(projectRoot,"languages/idMap.js"))
searchIdMapFiles.push(pathobj.join(projectRoot,"idMap.js"))
let idMapFile
for( idMapFile of idMapFiles){
for( idMapFile of searchIdMapFiles){
// 如果不存在idMap文件则尝试从location/languages/中导入
if(fs.existsSync(idMapFile)){
try{
@ -101,8 +116,7 @@ function getIdMap(options){
// 当require(idMap.js)失败时对esm模块尝试采用直接读取的方式
return require(idMapFile)
}catch(e){
// 出错原因可能是因为无效require esm模块
// 由于idMap.js文件格式相对简单因此尝试直接读取解析
// 出错原因可能是因为无效require esm模块由于idMap.js文件格式相对简单因此尝试直接读取解析
try{
let idMapContent = fs.readFileSync(idMapFile).toString()
idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"")
@ -126,20 +140,22 @@ module.exports = function voerkai18nPlugin(babel) {
// 转码插件参数可以覆盖默认参数
Object.assign(pluginOptions,state.opts || {});
const { location ,autoImport, translateFunctionName,moduleType } = pluginOptions
idMap = getIdMap(pluginOptions)
if(Object.keys(idMap).length===0){
idMap = readIdMapFile(pluginOptions)
}
// 是否自动导入t函数
if(autoImport){
let module = moduleType === 'auto' ? isEsModule(path) ? 'esm' : 'cjs' : moduleType
if(!["esm","es","cjs","commonjs"].includes(module)) module = 'esm'
if(module === 'esm'){
// 如果没有定义t函数则自动导入
if(!hasImportTranslateFunction(path)){
if(!hasImportTranslateFunction.call(pluginOptions, path)){
path.node.body.unshift(t.importDeclaration([
t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName)
)],t.stringLiteral(autoImport)))
}
}else{
if(!hasRequireTranslateFunction(path)){
if(!hasRequireTranslateFunction.call(pluginOptions, path)){
path.node.body.unshift(t.variableDeclaration("const",[
t.variableDeclarator(
t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]),

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/babel",
"version": "1.0.0",
"version": "1.0.3",
"description": "VoerkaI18n babel plugin",
"main": "index.js",
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
@ -10,8 +10,11 @@
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"publish": "npm publish -access public"
"release": "npm version patch && pnpm publish --no-git-checks --access public"
},
"author": "",
"license": "ISC"
"license": "ISC",
"dependencies": {
"@voerkai18n/utils": "workspace:^1.0.0"
}
}

View File

@ -1,4 +1,7 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
# @voerkai18n/babel
`Babel`转码插件,用来对翻译文本进行自动转码
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

View File

@ -1,18 +0,0 @@
function isPlainObject(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;
}
module.exports = {
isPlainObject
}

View File

@ -25,7 +25,8 @@
const glob = require("glob")
const createLogger = require("logsets")
const path = require("path")
const { t,findModuleType,getCurrentPackageJson} = require("./utils")
const { findModuleType,getCurrentPackageJson,installVoerkai18nRuntime,isInstallDependent} = require("@voerkai18n/utils")
const { t } = require("./i18nProxy")
const fs = require("fs-extra")
const logger = createLogger()
const artTemplate = require("art-template")
@ -52,9 +53,7 @@ module.exports =async function compile(langFolder,opts={}){
// 加载多语言配置文件
const settingsFile = path.join(langFolder,"settings.json")
try{
try{
// 读取多语言配置文件
const langSettings = fs.readJSONSync(settingsFile)
@ -125,8 +124,14 @@ module.exports =async function compile(langFolder,opts={}){
path.join(langFolder,"runtime.js")
)
logger.log(t(" - 运行时: {}"),"runtime.js")
}
}else{//如果不嵌入则需要安装运行时依赖
if(!isInstallDependent("@voerkai18n/runtime")){
installVoerkai18nRuntime(langFolder,moduleType)
logger.log(t(" - 安装运行时: {}"),"@voerkai18n/runtime")
}else{
logger.log(t(" - 运行时{}已安装"),"@voerkai18n/runtime")
}
}
const templateContext = {
scopeId:projectPackageJson.name,
inlineRuntime,

View File

@ -10,7 +10,10 @@ const deepmerge = require("deepmerge")
const path = require('path')
const fs = require('fs-extra')
const createLogger = require("logsets")
const { findModuleType,createPackageJsonFile,t } = require("./utils")
const { t } = require("./i18nProxy")
const { findModuleType } = require("@voerkai18n/utils")
const logger = createLogger()

21
packages/cli/i18nProxy.js Normal file
View File

@ -0,0 +1,21 @@
let i18nScope,t
try{
// @voerkai18n/cli工程本身使用了voerkai18n,即@voerkai18n/cli的extract和compile依赖于其自己生成的languages运行时
// 而@voerkai18n/cli又用来编译多语言包这样产生了鸡生蛋问题
// extract与compile调试阶段如果t函数无法使用(即编译的languages无法正常使用)则需要提供t函数
const language = require('./languages');
t = language.t
i18nScope = language.i18nScope
}catch{
t=v=>v
i18nScope={change:()=>{} }
}
module.exports = {
i18nScope,
t
}

View File

@ -2,42 +2,13 @@ const { Command } = require('commander');
const createLogger = require("logsets")
const path = require("path")
const fs = require("fs-extra")
const deepmerge = require('deepmerge');
const logger = createLogger()
const { getCurrentProjectRootFolder,i18nScope ,t } = require("./utils")
const { i18nScope ,t } = require("./i18nScope")
const { getProjectRootFolder, getProjectSourceFolder } = require("@voerkai18n/utils")
const program = new Command();
/**
* 根据当前输入的文件夹位置自动确定源码文件夹位置
*
* - 如果没有指定则取当前文件夹
* - 如果指定是非绝对路径则以当前文件夹作为base
* - 查找pack
* - 如果该文件夹中存在src则取src下的文件夹
* -
*
* @param {*} location
* @returns
*/
function getProjectSourceFolder(location){
if(!location) {
location = process.cwd()
}else{
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
}
let projectRoot = getCurrentProjectRootFolder(location)
// 如果当前工程存在src文件夹则自动使用该文件夹作为源文件夹
if(fs.existsSync(path.join(projectRoot,"src"))){
projectRoot = path.join(projectRoot,"src")
}
return projectRoot
}
program
.command('init')
.argument('[location]', t('工程项目所在目录'))

View File

@ -4,30 +4,14 @@
*/
const { findModuleType,createPackageJsonFile,t,getCurrentProjectRootFolder } = require("./utils")
const path = require("path")
const fs = require("fs")
const shelljs = require("shelljs")
const { t } = require("./i18nProxy")
const { findModuleType } = require("@voerkai18n/utils")
const createLogger = require("logsets")
const logger = createLogger()
/**
* 在当前工程自动安装@voerkai18n/runtime
* @param {*} langFolder
* @param {*} opts
*/
function installVoerkai18nRuntim(srcPath){
const projectFolder = getCurrentProjectRootFolder(srcPath || process.cwd())
if(fs.existsSync("pnpm-lock.yaml")){
shelljs.exec("pnpm add @voerkai18n/runtime")
}else if(fs.existsSync("yarn.lock")){
shelljs.exec("yarn add @voerkai18n/runtime")
}else{
shelljs.exec("npm install @voerkai18n/runtime")
}
}
module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLanguage="cn",activeLanguage="cn",reset=false,installRuntime=true}={}){
// 语言文件夹名称
const langPath = "languages"
@ -38,7 +22,6 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan
if(debug) logger.log(t("创建语言包文件夹: {}"),lngPath)
}
// 创建settings.json文件
const settingsFile = path.join(lngPath,"settings.json")
if(fs.existsSync(settingsFile) && !reset){
@ -54,12 +37,6 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan
// 写入配置文件
fs.writeFileSync(settingsFile,JSON.stringify(settings,null,4))
// 自动安装运行时@voerkai18n/runtime
// if(installRuntime){
// logger.log(t("正在安装多语言运行时:{}"),"@voerkai18n/runtime")
// installVoerkai18nRuntim(srcPath)
// }
if(debug) {
logger.log(t("生成语言配置文件:{}"),"./languages/settings.json")

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/cli",
"version": "1.0.10",
"version": "1.0.11",
"description": "VoerkaI18n command line interactive tools",
"main": "index.js",
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
@ -29,7 +29,8 @@
"dependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.5",
"@voerkai18n/runtime": "workspace:^1.0.0",
"@voerkai18n/runtime": "workspace:^1.0.8",
"@voerkai18n/utils": "workspace:^1.0.0",
"art-template": "^4.13.2",
"commander": "^9.0.0",
"cross-env": "^7.0.3",

View File

@ -1,64 +1,6 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
# VoerkaI18n命令行工具
`@VoerkaI18n/cli`实现初始化、文本提取和编译等命令
## 初始化项目 - init
```shell
初始化项目国际化配置
Arguments:
location 工程项目所在目录
Options:
-D, --debug 输出调试信息
-r, --reset 重新生成当前项目的语言配置
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
-d, --defaultLanguage 默认语言
-a, --activeLanguage 激活语言
-h, --help display help for command
```
## 提取文本 - extract
```shell
扫描并提取所有待翻译的字符串到<languages/translates>文件夹中
Arguments:
location 工程项目所在目录 (default: "./")
Options:
-D, --debug 输出调试信息
-lngs, --languages 支持的语言
-d, --defaultLanguage 默认语言
-a, --activeLanguage 激活语言
-ns, --namespaces 翻译名称空间
-e, --exclude <folders> 排除要扫描的文件夹,多个用逗号分隔
-u, --updateMode 本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并
-f, --filetypes 要扫描的文件类型
-h, --help display help for command
```
## 编译文本 - compile
```shell
Usage: voerkai18n compile [options] [location]
编译指定项目的语言包
Arguments:
location 工程项目所在目录 (default: "./")
Options:
-d, --debug 输出调试信息
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "esm")
-h, --help display help for command
```
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

View File

@ -32,13 +32,13 @@ const scope = new i18nScope({
}
})
// 翻译函数
const t = translate.bind(scope)
const scopedTtranslate = translate.bind(scope)
{{if moduleType === "esm"}}
export {
t,
i18nScope as scope
scopedTtranslate as t,
scope as i18nScope
}
{{else}}
module.exports.t = t
module.exports.t = scopedTtranslate
module.exports.i18nScope = scope
{{/if}}

View File

@ -1,186 +0,0 @@
const path = require("path")
const fs = require("fs-extra")
/**
* 返回当前项目根文件夹
* 从指定文件夹folder向上查找package.json
* @param {*} folder
*/
function getCurrentProjectRootFolder(folder,exclueCurrent=false){
try{
const pkgFile =exclueCurrent ?
path.join(folder, "..", "package.json")
: path.join(folder, "package.json")
if(fs.existsSync(pkgFile)){
return path.dirname(pkgFile)
}
const parent = path.dirname(folder)
if(parent===folder) return null
return getCurrentProjectRootFolder(parent,false)
}catch(e){
return null
}
}
/**
* 读取指定文件夹的package.json文件如果当前文件夹没有package.json文件则向上查找
* @param {*} folder
* @param {*} exclueCurrent = true 排除folder从folder的父级开始查找
* @returns
*/
function getCurrentPackageJson(folder,exclueCurrent=true){
let projectFolder = getCurrentProjectRootFolder(folder,exclueCurrent)
if(projectFolder){
return fs.readJSONSync(path.join(projectFolder,"package.json"))
}
}
/**
*
* 返回当前项目的模块类型
*
* 从当前文件夹开始向上查找package.json文件并解析出语言包的类型
*
* @param {*} folder
*/
function findModuleType(folder){
let packageJson = getCurrentPackageJson(folder)
try{
return packageJson.type || "commonjs"
}catch(e){
return "esm"
}
}
function createPackageJsonFile(targetPath,moduleType="auto"){
if(moduleType==="auto"){
moduleType = findModuleType(targetPath)
}
const packageJsonFile = path.join(targetPath, "package.json")
if(["esm","es","module"].includes(moduleType)){
fs.writeFileSync(packageJsonFile,JSON.stringify({type:"module",license:"MIT"},null,4))
if(moduleType==="module"){
moduleType = "esm"
}
}else{
fs.writeFileSync(packageJsonFile,JSON.stringify({license:"MIT"},null,4))
}
return moduleType
}
function isPlainObject(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;
}
/**
*
* getExportContent({a:1}) == export let a = 1
*
* @param {*} values
* @param {*} moduleType
* @returns
*/
function generateExportContents(values,{moduleType="esm",varExportDeclare="let"}={}){
if(!isPlainObject(values)) throw new TypeError("export value must be a function or plain object")
let results = []
let varExports = []
let varExportSyntax = moduleType === "esm" ? `export ${varExportDeclare} ` : "module.exports."
let funcExportSyntax = moduleType === "esm" ? `export ` : "module.exports."
Object.entries(values).forEach(([name,value])=>{
if(Array.isArray(value) || isPlainObject(value)){
results.push(`${varExportDeclare} ${name} = ${JSON.stringify(value,null,4)}`)
}else if(typeof(value)==="function"){
if(value.prototype){
results.push(value.toString())
}else{// 箭头函数
results.push(`const ${name} = ${value.toString()}`)
}
}else{
results.push(`${varExportDeclare} ${name} = ${JSON.stringify(value)}`)
}
})
if(moduleType === "esm"){
results.push(`export {\n\t${Object.keys(values).join(",\n\t")}\n}`)
}else{ fu
results.push(`module.exports = {\n\t${Object.keys(values).join(",\n\t")}\n}`)
}
return results.join("\n")
}
/**
* 创建js文件
* @param {*} filename
* @param {*} defaultExports
* @param {*} namedExports {name:value}
*
* @param {*} moduleType
*/
function createJsModuleFile(filename,defaultExports={},namedExports={},moduleType="esm"){
let jsContents = []
if(moduleType === "esm"){
Object.entries(namedExports).forEach(([name,value])=>{
})
jsContents.push
}else{
}
}
function escape(str){
return str
.replaceAll("\\t","\t")
.replaceAll("\\n","\n")
.replaceAll("\\b","\b")
.replaceAll("\\r","\r")
.replaceAll("\\f","\f")
.replaceAll("\\'","\'")
.replaceAll('\\"','\"')
.replaceAll('\\v','\v')
.replaceAll("\\\\",'\\')
}
let i18nScope,t
try{
// @voerkai18n/cli工程本身使用了voerkai18n,即@voerkai18n/cli的extract和compile依赖于其自己生成的languages运行时
// 而@voerkai18n/cli又用来编译多语言包这样产生了鸡生蛋问题
// extract与compile调试阶段如果t函数无法使用(即编译的languages无法正常使用)则需要提供t函数
const language = require('./languages');
t = language.t
i18nScope = language.i18nScope
}catch{
t=v=>v
i18nScope={change:()=>{} }
}
module.exports = {
findModuleType,
createPackageJsonFile,
isPlainObject,
getCurrentPackageJson,
getCurrentProjectRootFolder,
escape,
i18nScope,
t
}

View File

@ -4,7 +4,8 @@
"description": "",
"main": "currency.formatters.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"release": "npm version patch && pnpm publish --no-git-checks --access public"
},
"exports":{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,11 @@
'use strict';
var require$$2 = require('@voerkai18n/utils');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var require$$2__default = /*#__PURE__*/_interopDefaultLegacy(require$$2);
/**
*
* 简单的事件触发器
@ -129,7 +135,7 @@ var scope = class i18nScope {
const loader = this.loaders[newLanguage];
if(typeof(loader) === "function"){
try{
this._messages = (await loader()).default;
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
}catch(e){
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
@ -214,7 +220,9 @@ var formatters$1 = {
const EventEmitter = eventemitter;
const i18nScope = scope;
let formatters = formatters$1;
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = require$$2__default["default"];
let inlineFormatters = formatters$1; // 内置格式化器
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
@ -239,79 +247,7 @@ 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"];
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
}function isPlainObject(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(value){
return !isNaN(parseInt(value))
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* object:0, // 对象合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge(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(results[key],value,options);
}
}else {
results[key] = value;
}
}else {
results[key] = value;
}
});
return results
}
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
@ -831,11 +767,11 @@ function translate(message) {
// 当前激活语言
get activeLanguage(){ return this._settings.activeLanguage}
// 默认语言
get defaultLanguage(){ return this.this._settings.defaultLanguage}
get defaultLanguage(){ return this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 全局格式化器
get formatters(){ return formatters }
// 内置格式化器
get formatters(){ return inlineFormatters }
/**
* 切换语言
*/

View File

@ -1,3 +1,5 @@
import require$$2 from '@voerkai18n/utils';
/**
*
* 简单的事件触发器
@ -127,7 +129,7 @@ var scope = class i18nScope {
const loader = this.loaders[newLanguage];
if(typeof(loader) === "function"){
try{
this._messages = (await loader()).default;
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
}catch(e){
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
@ -212,7 +214,9 @@ var formatters$1 = {
const EventEmitter = eventemitter;
const i18nScope = scope;
let formatters = formatters$1;
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = require$$2;
let inlineFormatters = formatters$1; // 内置格式化器
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
@ -237,79 +241,7 @@ 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"];
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
}function isPlainObject(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(value){
return !isNaN(parseInt(value))
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* object:0, // 对象合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge(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(results[key],value,options);
}
}else {
results[key] = value;
}
}else {
results[key] = value;
}
});
return results
}
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
@ -829,11 +761,11 @@ function translate(message) {
// 当前激活语言
get activeLanguage(){ return this._settings.activeLanguage}
// 默认语言
get defaultLanguage(){ return this.this._settings.defaultLanguage}
get defaultLanguage(){ return this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 全局格式化器
get formatters(){ return formatters }
// 内置格式化器
get formatters(){ return inlineFormatters }
/**
* 切换语言
*/

View File

@ -1,6 +1,8 @@
const EventEmitter = require("./eventemitter")
const i18nScope = require("./scope.js")
let formatters = require("./formatters")
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = require("@voerkai18n/utils")
let inlineFormatters = require("./formatters") // 内置格式化器
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
@ -33,80 +35,7 @@ function hasInterpolation(str){
return str.includes("{") && str.includes("}")
}
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
};
function isPlainObject(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(value){
return !isNaN(parseInt(value))
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* object:0, // 对象合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge(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(results[key],value,options)
}
}else{
results[key] = value
}
}else{
results[key] = value
}
})
return results
}
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
@ -649,11 +578,11 @@ function translate(message) {
// 当前激活语言
get activeLanguage(){ return this._settings.activeLanguage}
// 默认语言
get defaultLanguage(){ return this.this._settings.defaultLanguage}
get defaultLanguage(){ return this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 全局格式化器
get formatters(){ return formatters }
// 内置格式化器
get formatters(){ return inlineFormatters }
/**
* 切换语言
*/

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/runtime",
"version": "1.0.7",
"version": "1.0.8",
"description": "Voerkai18n Runtime",
"main": "./dist/index.cjs",
"module": "dist/index.esm.js",
@ -12,7 +12,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"release": "npm version patch && npm run build && npm publish -access public"
"release": "npm version patch && pnpm publish --no-git-checks --access public"
},
"exports": {
"import": "./dist/index.esm.js",
@ -33,5 +33,8 @@
"rollup": "^2.69.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {
"@voerkai18n/utils": "workspace:^1.0.0"
}
}

View File

@ -1,5 +1,7 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
# @voerkai18n/runtime
`voerkai18n`运行时核心代码
`voerkai18n`运行时核心代码
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

View File

@ -95,7 +95,7 @@ module.exports = class i18nScope {
const loader = this.loaders[newLanguage]
if(typeof(loader) === "function"){
try{
this._messages = (await loader()).default
this._messages = (await loader()).default
this._activeLanguage = newLanguage
}catch(e){
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`)

376
packages/utils/index.js Normal file
View File

@ -0,0 +1,376 @@
const path = require("path")
const shelljs = require("shelljs")
const fs = require("fs-extra")
/**
*
* 匹配指定路径或文件名称
*
* const matcher = fileMatcher([
* "<pattern>", // 匹配正则表达式字符串
* "!<pattern>", // 以!开头代表否定匹配
* /正则表达式/
* ],{
* basePath:"<指定一个基准目录,所有不是以此开头的均视为不匹配>",
* defaultPatterns:["<默认排除的模式>","<默认排除的模式>","<默认排除的模式>"],
* debug:<true/>false,是否输出调试信息,=true时.test()方法返回[<true/false>,pattern] *
* })
*
*
*
*
* @param {*} patterns
* @param {*} basePath 如果指定basePath则所有不是以basePath开头的文件都排除
* @param {*} defaultPatterns 默认的匹配模式
* @param {*} debug 是否输出调试信息
*/
function fileMatcher(patterns,{basePath,defaultPatterns=[],debug=true}={}) {
if(basePath) {
basePath = path.normalize(basePath)
}
//[[pattern,exclude],[pattern,false],[pattern,true]]
let finalPatterns = []
let inputPatterns = Array.isArray(patterns) ? patterns : (patterns ? [patterns] : [])
// 默认排除模式
if(defaultPatterns.length===0){
finalPatterns.push([/.*\/node_modules\/.*/,true])
finalPatterns.push([/.*\/languages\/.*/,true]) // 默认排除语言文件
finalPatterns.push([/\.babelrc/,true])
finalPatterns.push([/babel\.config\.js/,true])
finalPatterns.push([/package\.json$/,true])
finalPatterns.push([/vite\.config\.js$/,true])
finalPatterns.push([/^plugin-vue:.*/,true])
}
inputPatterns.forEach(pattern=>{
if(typeof pattern === "string"){
pattern.replaceAll("**",".*")
pattern.replaceAll("?","[^\/]?")
pattern.replaceAll(/(?<!\.)\*/g,"[^\/]*")
// 以!开头的表示排除
if(pattern.startsWith("!")){
finalPatterns.unshift([new RegExp(pattern.substring(1),"g"),true])
}else{
finalPatterns.push([new RegExp(pattern,"g"),false])
}
}else{
finalPatterns.push([pattern,false])
}
})
return {
patterns:finalPatterns,
basePath,
test: (filename) => {
let isMatched = false
let file = filename
// 如果指定basePath则文件名称必须是以basePath开头
if(basePath){
if(path.isAbsolute(file)){
if(!path.normalize(file).startsWith(basePath)){
return debug ? [false,`!^${basePath}`] : false
}else{
isMatched = true
}
}
}
if(finalPatterns.length===0){
return debug ? [true,"*"] : true
}else{
for(const pattern of finalPatterns){
pattern[0].lastIndex = 0
if(pattern[1]===true){
if(pattern[0].test(file)) return debug ? [false,pattern[0].toString()] : false
}else{
if(pattern[0].test(file)) return debug ? [true,pattern[0].toString()] : true
}
}
}
return debug ? [isMatched,"*"] : isMatched
}
}
}
/**
* 以floder为基准向上查找文件package.json并返回package.json所在的文件夹
* @param {*} folder 起始文件夹如果没有指定则取当前文件夹
* @param {*} exclueCurrent 如果=true则folder的父文件夹开始查找
* @returns
*/
function getProjectRootFolder(folder="./",exclueCurrent=false){
if(!path.isAbsolute(folder)){
folder = path.join(process.cwd(),folder)
}
try{
const pkgFile =exclueCurrent ?
path.join(folder, "..", "package.json")
: path.join(folder, "package.json")
if(fs.existsSync(pkgFile)){
return path.dirname(pkgFile)
}
const parent = path.dirname(folder)
if(parent===folder) return null
return getProjectRootFolder(parent,false)
}catch(e){
return process.cwd()
}
}
/**
* 自动获取当前项目的languages
*
* 1.
*
* @param {*} location
*/
function getProjectLanguageFolder(location="./"){
// 绝对路径
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
// 发现当前项目根目录
const projectRoot = getProjectRootFolder(location)
const searchFolders = [
path.join(location,"src","languages"),
path.join(location,"languages")
]
for(let folder of searchFolders){
if(fs.existsSync(folder)){
return folder
}
}
return null
}
/**
* 根据当前输入的文件夹位置自动确定源码文件夹位置
*
* - 如果没有指定则取当前文件夹
* - 如果指定是非绝对路径则以当前文件夹作为base
* - 查找pack
* - 如果该文件夹中存在src则取src下的文件夹
* -
*
* @param {*} location
* @returns
*/
function getProjectSourceFolder(location){
if(!location) {
location = process.cwd()
}else{
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
}
let projectRoot = getProjectRootFolder(location)
// 如果当前工程存在src文件夹则自动使用该文件夹作为源文件夹
if(fs.existsSync(path.join(projectRoot,"src"))){
projectRoot = path.join(projectRoot,"src")
}
return projectRoot
}
/**
* 读取指定文件夹的package.json文件如果当前文件夹没有package.json文件则向上查找
* @param {*} folder
* @param {*} exclueCurrent = true 排除folder从folder的父级开始查找
* @returns
*/
function getCurrentPackageJson(folder,exclueCurrent=true){
let projectFolder = getCurrentProjectRootFolder(folder,exclueCurrent)
if(projectFolder){
return fs.readJSONSync(path.join(projectFolder,"package.json"))
}
}
/**
*
* 返回当前项目的模块类型
*
* 从当前文件夹开始向上查找package.json文件并解析出语言包的类型
*
* @param {*} folder
*/
function findModuleType(folder){
let packageJson = getCurrentPackageJson(folder)
try{
return packageJson.type || "commonjs"
}catch(e){
return "esm"
}
}
/**
* 判断是否已经安装了依赖
*
* isInstallDependent("@voerkai18n/runtime")
*
*/
function isInstallDependent(url){
try{
// 简单判断是否存在该文件夹node_modules/@voerkai18n/runtime
let projectFolder = getCurrentProjectRootFolder(process.cwd())
if(fs.existsSync(path.join(projectFolder,"node_modules","@voerkai18n/runtime"))){
return true
}
// 如果不存在则尝试require
require(url)
}catch(e){
return false
}
}
/**
* 判断是否是JSON对象
* @param {*} obj
* @returns
*/
function isPlainObject(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(value){
return !isNaN(parseInt(value))
}
/**
* 检测当前工程是否是git工程
*/
function isGitRepo(){
return shelljs.exec("git status", {silent: true}).code === 0;
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge(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(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(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
};
/**
* 在当前工程自动安装@voerkai18n/runtime
* @param {*} srcPath
* @param {*} opts
*/
function installVoerkai18nRuntime(srcPath){
const projectFolder = getCurrentProjectRootFolder(srcPath || process.cwd())
if(fs.existsSync("pnpm-lock.yaml")){
shelljs.exec("pnpm add @voerkai18n/runtime")
}else if(fs.existsSync("yarn.lock")){
shelljs.exec("yarn add @voerkai18n/runtime")
}else{
shelljs.exec("npm install @voerkai18n/runtime")
}
}
/**
* 在指定文件夹下创建package.json文件
* @param {*} targetPath
* @param {*} moduleType
* @returns
*/
function createPackageJsonFile(targetPath,moduleType="auto"){
if(moduleType==="auto"){
moduleType = findModuleType(targetPath)
}
const packageJsonFile = path.join(targetPath, "package.json")
if(["esm","es","module"].includes(moduleType)){
fs.writeFileSync(packageJsonFile,JSON.stringify({type:"module",license:"MIT"},null,4))
if(moduleType==="module"){
moduleType = "esm"
}
}else{
fs.writeFileSync(packageJsonFile,JSON.stringify({license:"MIT"},null,4))
}
return moduleType
}
module.exports = {
fileMatcher, // 文件名称匹配器
getProjectRootFolder, // 查找获取项目根目录
createPackageJsonFile, // 创建package.json文件
getProjectSourceFolder, // 获取项目源码目录
getCurrentPackageJson, // 查找获取当前项目package.json
getProjectLanguageFolder, // 获取当前项目的languages目录
findModuleType, // 获取当前项目的模块类型
isInstallDependent, // 判断是否已经安装了依赖
installVoerkai18nRuntime, // 在当前工程自动安装@voerkai18n/runtime
isPlainObject, // 判断是否是普通对象
isNumber, // 判断是否是数字
deepMerge, // 深度合并对象
getDataTypeName, // 获取指定变量类型名称
isGitRepo, // 判断当前工程是否是git工程
}

View File

@ -0,0 +1,17 @@
{
"name": "@voerkai18n/utils",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"release": "npm version patch && npm run build && npm publish -access public"
},
"author": "",
"license": "ISC",
"dependencies": {
"fs-extra": "^10.0.1",
"shelljs": "^0.8.5"
}
}

166
packages/vite/index.js Normal file
View File

@ -0,0 +1,166 @@
const path = require("path")
const fs = require("fs")
const { fileMatcher,getProjectRootFolder,getProjectLanguageFolder } = require("@voerkai18n/utils")
//const TranslateRegex = /\bt\(\s*("|'){1}(?:((?<namespace>\w+)::))?(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
const TranslateRegex =/(?<=\bt\(\s*("|'){1})(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
// 匹配正则表达式
const importTRegex = /^[^\w\r\n]*import\s*\{(.*)\bt\b(.*)\}\sfrom/gm
/**
* 读取idMap.js文件
*
*
*
* @param {*} options
* @returns
*/
function readIdMapFile(options){
let { location } = options
let searchIdMapFiles = []
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
searchIdMapFiles.push(path.join(location,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"idMap.js"))
let projectRoot = getProjectRootFolder(location)
searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"idMap.js"))
let idMapFile
for( idMapFile of searchIdMapFiles){
// 如果不存在idMap文件则尝试从location/languages/中导入
if(fs.existsSync(idMapFile)){
try{
// 由于idMap.js可能是esm或cjs并且babel插件不支持异步
// 当require(idMap.js)失败时对esm模块尝试采用直接读取的方式
return require(idMapFile)
}catch(e){
// 出错原因可能是因为无效require esm模块由于idMap.js文件格式相对简单因此尝试直接读取解析
try{
let idMapContent = fs.readFileSync(idMapFile).toString()
idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"")
return JSON.parse(idMapContent)
}catch{ }
}
}
}
// 所有尝试完成后触发错误
throw new Error(`${idMapFile}文件不存在,无法对翻译文本进行转换。\n原因可能是babel-plugin-voerkai18n插件的location参数未指向有效的语言包所在的目录。`)
}
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 replaceCode(code, idmap) {
return code.replaceAll(TranslateRegex, (text) => {
let message = escape(text)
if(message in idmap) {
return idmap[message]
}else{
return text
}
})
}
/**
* 判定代码中是否导入了Translate函数
* @param {*} code
* @returns
*/
function hasImportTranslateFunction(code){
return importTRegex.test(code)
}
/**
options = {
location: "./languages",
autoImport:true
}
*/
module.exports = function VoerkaI18nPlugin(opts={}) {
let options = Object.assign({
location: "./", // 指定当前工程目录
autoImport: true, // 是否自动导入t函数
debug:false, // 是否输出调试信息,当=true时在控制台输出转换匹配的文件清单
patterns:[
"!(?<!.vue\?.*).(css|json|scss|less|sass)$", // 排除所有css文件
/\.vue(\?.*)?/, // 所有vue文件
] // 提取范围
},opts)
let { debug,patterns,autoImport } = options
let projectRoot = getProjectRootFolder(options.location)
let languageFolder = getProjectLanguageFolder(projectRoot)
if(debug){
console.log("Project root: ",projectRoot)
console.log("Language folder: ",languageFolder)
}
const matcher = fileMatcher(patterns,{
basePath:projectRoot,
debug:debug
})
let idMap = readIdMapFile(options)
return {
name: 'voerkai18n',
async transform(src, id) {
let [isMatched,pattern] = debug ? matcher.test(id) : [matcher.test(id),null]
if(isMatched){
if(debug){
console.log(`File=${path.relative(projectRoot,id)}, pattern=[${pattern}], import from "${path.relative(path.dirname(id),languageFolder)}"`)
}
try{
// 判断是否使用了t函数
if(TranslateRegex.test(src)){
let code = replaceCode(src,idMap)
// 如果没有导入t函数则尝试自动导入
if(autoImport && !hasImportTranslateFunction(code)){
let importSource = path.relative(path.dirname(id),languageFolder)
if(!importSource.startsWith(".")){
importSource = "./" + importSource
}
importSource=importSource.replace("\\","/")
// 优先在<script setup></script>中导入
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
if(setupScriptRegex.test(code)){
code = code.replace(setupScriptRegex,`$1\nimport { t } from '${importSource}';`)
}else{// 如果没有<script setup>则在<script></script>中导入
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
}
}
return {
code,
map: null
}
}
}catch(e){
console.warn(`vite-plugin-voerkai18n转换<${id}>文件出错:${e.message}`)
}
}
return {
code:src,
map: null
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "@voerkai18n/vite",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"release": "npm version patch && pnpm publish --no-git-checks --access public"
},
"author": "",
"license": "ISC",
"dependencies": {
"@voerkai18n/utils": "workspace:^1.0.0"
}
}

6
packages/vite/readme.md Normal file
View File

@ -0,0 +1,6 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
`@voerkai18n/vite`插件用来进行自动文本映射和导入`t`函数
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

117
packages/vite/utils.js Normal file
View File

@ -0,0 +1,117 @@
const path = require("path")
/**
*
* 匹配指定路径或文件名称
*
* const matcher = fileMatcher([
* "<pattern>", // 匹配正则表达式字符串
* "!<pattern>", // 以!开头代表否定匹配
* /正则表达式/
* ],{
* basePath:"<指定一个基准目录,所有不是以此开头的均视为不匹配>",
* defaultPatterns:["<默认排除的模式>","<默认排除的模式>","<默认排除的模式>"],
* debug:<true/>false,是否输出调试信息,=true时.test()方法返回[<true/false>,pattern] *
* })
*
*
*
*
* @param {*} patterns
* @param {*} basePath 如果指定basePath则所有不是以basePath开头的文件都排除
* @param {*} defaultPatterns 默认的匹配模式
* @param {*} debug 是否输出调试信息
*/
function fileMatcher(patterns,{basePath,defaultPatterns=[],debug=true}={}) {
if(basePath) {
basePath = path.normalize(basePath)
}
//[[pattern,exclude],[pattern,false],[pattern,true]]
let finalPatterns = []
let inputPatterns = Array.isArray(patterns) ? patterns : (patterns ? [patterns] : [])
// 默认排除模式
if(defaultPatterns.length===0){
finalPatterns.push([/.*\/node_modules\/.*/,true])
finalPatterns.push([/.*\/languages\/.*/,true]) // 默认排除语言文件
finalPatterns.push([/\.babelrc/,true])
finalPatterns.push([/babel\.config\.js/,true])
finalPatterns.push([/package\.json$/,true])
finalPatterns.push([/vite\.config\.js$/,true])
finalPatterns.push([/^plugin-vue:.*/,true])
}
inputPatterns.forEach(pattern=>{
if(typeof pattern === "string"){
pattern.replaceAll("**",".*")
pattern.replaceAll("?","[^\/]?")
pattern.replaceAll(/(?<!\.)\*/g,"[^\/]*")
// 以!开头的表示排除
if(pattern.startsWith("!")){
finalPatterns.unshift([new RegExp(pattern.substring(1),"g"),true])
}else{
finalPatterns.push([new RegExp(pattern,"g"),false])
}
}else{
finalPatterns.push([pattern,false])
}
})
return {
patterns:finalPatterns,
basePath,
test: (filename) => {
let isMatched = false
let file = filename
// 如果指定basePath则文件名称必须是以basePath开头
if(basePath){
if(path.isAbsolute(file)){
if(!path.normalize(file).startsWith(basePath)){
return debug ? [false,`!^${basePath}`] : false
}else{
isMatched = true
}
}
}
if(finalPatterns.length===0){
return debug ? [true,"*"] : true
}else{
for(const pattern of finalPatterns){
pattern[0].lastIndex = 0
if(pattern[1]===true){
if(pattern[0].test(file)) return debug ? [false,pattern[0].toString()] : false
}else{
if(pattern[0].test(file)) return debug ? [true,pattern[0].toString()] : true
}
}
}
return debug ? [isMatched,"*"] : isMatched
}
}
}
function getProjectRootFolder(folder,exclueCurrent=false){
if(!path.isAbsolute(folder)){
folder = path.join(process.cwd(),folder)
}
try{
const pkgFile =exclueCurrent ?
path.join(folder, "..", "package.json")
: path.join(folder, "package.json")
if(fs.existsSync(pkgFile)){
return path.dirname(pkgFile)
}
const parent = path.dirname(folder)
if(parent===folder) return null
return getProjectRootFolder(parent,false)
}catch(e){
return process.cwd()
}
}
module.exports = {
fileMatcher,
getProjectRootFolder
}

View File

@ -3,32 +3,55 @@
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from '@voerkai18n/vue'
import { t, i18nScope } from './languages'
import { t,i18nScope } from './languages'
const app = createApp(Root)
app.use(i18nPlugin,{ t,i18nScope })
app.use(i18nPlugin,{ i18nScope })
app.mount('#app')
*/
import { computed,reactive,ref } from "vue"
export default {
install: (app, opts={}) => {
let options = Object.assign({
t:message=>message,
i18nScope:null,
}, opts)
i18nScope:null, // 当前作用域实例
forceUpdate:true, // 当语言切换时是否强制重新渲染
}, opts)
let i18nScope = options.i18nScope
if(i18nScope===null){
console.warn("@voerkai18n/vue: i18nScope is not provided, use default i18nScope")
i18nScope = {change:()=>{}}
}
// 插件只需要安装一次实例
if(app.voerkai18n){
return
}
let translate = options.t
if(typeof(translate)!=="function"){
console.warn("@voerkai18n/vue: t function is not provided, use default t function")
translate = message=>message
}
let activeLanguage = ref(i18nScope.global.activeLanguage)
// 当语言包发生变化时,强制重新渲染组件树
i18nScope.global.on((newLanguage)=>{
app._instance.update()
activeLanguage.value = newLanguage
})
// 全局翻译函数
app.config.globalProperties.t = function(){
return options.t(...arguments)
}
}
// 全局i18n对象
app.voerkai18n = options.i18nScope.global
app.provide('i18n', reactive({
activeLanguage: computed({
get: () => activeLanguage,
set: (value) => i18nScope.global.change(value).then(()=>{
if(options.forceUpdate){
app._instance.update()
}
})
}),
languages:i18nScope.global.languages,
defaultLanguage:i18nScope.global.defaultLanguage,
}))
}
}

View File

@ -1,16 +1,26 @@
{
"name": "@voerkai18n/vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}
"name": "@voerkai18n/vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"release": "npm version patch && pnpm publish --no-git-checks --access public"
},
"exports": {
".": "./index.js",
"./vite-plugin-voerkai18n": "./vite-plugin-voerkai18n.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
},
"dependencies": {
"minimatch": "^5.0.1",
"vite": "^2.8.6",
"vite-plugin-inspect": "^0.4.3"
}
}

View File

@ -1,4 +1,5 @@
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](https://gitee.com/zhangfisher/voerka-i18n)
# @voerkai18n/vue
@voerkai18n/vue插件,用来实现语言切换等功能。
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)

3418
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

870
readme.md
View File

@ -1,7 +1,7 @@
# ** 测试阶段有问题请issues **
[![star](https://gitee.com/zhangfisher/voerka-i18n/badge/star.svg?theme=white)](https://gitee.com/zhangfisher/voerka-i18n/stargazers) [![star](https://gitee.com/zhangfisher/voerka-i18n/badge/star.svg?theme=white)](https://gitee.com/zhangfisher/voerka-i18n/stargazers)
[![star](https://gitee.com/zhangfisher/voerka-i18n/badge/star.svg?theme=white)](https://gitee.com/zhangfisher/voerka-i18n/stargazers)
# 前言
@ -56,7 +56,7 @@
```
- **@voerkai18/runtime**
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装。
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装,一般仅在开发库项目时采用独立的运行时依赖
```javascript
npm install --save @voerkai18/runtime
@ -399,210 +399,7 @@ t("My name is { name | UpperCase | mr }",{name:"tom"})
`data | f1 | f2 | f3(1)`等效于` f3(f2(f1(data)),1)`
### 自定义格式化器
当我们使用`voerkai18n compile`编译后,会生成`languages/formatters.js`文件,可以在该文件中自定义您自己的格式化器。
`formatters.js`文件内容如下:
```javascript
module.exports = {
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
// [数据类型名称]:(value)=>{...},
// [数据类型名称]:(value)=>{...},
},
cn:{
$types:{
// 所有类型的默认格式化器
"*":{
},
Date:{},
Number:{},
Boolean:{ },
String:{},
Array:{
},
Object:{
}
},
[格式化名称]:(value)=>{.....},
//.....
},
en:{
$types:{
// [数据类型名称]:(value)=>{...},
},
[格式化名称]:(value)=>{.....},
//.....更多的格式化器.....
}
}
```
**说明:**
#### 格式化器函数
**每一个格式化器就是一个普通的同步函数**,不支持异步函数。
典型的无参数的格式化器:`(value)=>{....返回格式化的结果...}`
带参数的格式化器:`(value,arg1,...)=>{....返回格式化的结果...}`,其中`value`是上一个格式化器的输出结果。
#### 类型格式化器
可以为每一种数据类型指定一个默认的格式化器,支持对`String``Date``Error``Object``Array``Boolean``Number`等数据类型的格式化。
当插值变量传入时,如果有定义了对应的的类型格式化器,会先调用该格式化器。
比如我们定义对`Boolean`类型格式化器,
```javascript
//formatters.js
module.exports = {
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
Boolean:(value)=> value ? "ON" : "OFF"
}
}
t("灯状态:{status}",true) // === 灯状态ON
t("灯状态:{status}",false) // === 灯状态OFF
```
在上例中,如果我们想在不同的语言环境下,翻译为不同的显示文本,则可以为不同的语言指定类型格式化器
```javascript
//formatters.js
module.exports = {
cn:{
$types:{
Boolean:(value)=> value ? "开" : "关"
}
},
en:{
$types:{
Boolean:(value)=> value ? "ON" : "OFF"
}
}
}
// 当切换到中文时
t("灯状态:{status}",true) // === 灯状态:开
t("灯状态:{status}",false) // === 灯状态:关
// 当切换到英文时
t("灯状态:{status}",true) // === 灯状态ON
t("灯状态:{status}",false) // === 灯状态OFF
```
**说明:**
- 完整的类型格式化器定义形式
```javascript
module.exports = {
"*":{
$types:{...}
},
cn:{
$types:{...}
},
en:{
$types:{....}
}
}
```
在匹配应用格式化时会先在当前语言的`$types`中查找匹配的格式化器,如果找不到再上`*.$types`中查找。
- `*.$types`代表当所有语言中均没有定义时才匹配的类型格式化。
- 类型格式化器是默认执行的,不需要指定名称。
- 当前作用域的格式化器优先于全局的格式化器。(后续)
#### 通用的格式化器
类型格式化器只针对特定数据类型,并且会默认调用。而通用的格式化器需要使用`|`管道符进行显式调用。
同样的,通用的格式化器定义在`languages/formatters.js`中。
```javascript
module.exports = {
"*":{
$types:{...},
[格式化名称]:(value)=>{.....},
},
cn:{
$types:{...},
[格式化名称]:(value)=>{.....},
},
en:{
$types:{....},
[格式化名称]:(value)=>{.....},
[格式化名称]:(value,arg)=>{.....},
}
}
```
每一个格式化器均需要指定一个名称,在进行插值替换时会优先依据当前语言来匹配查找格式化器,如果找不到,再到键名为`*`中查找。
```javascript
module.exports = {
"*":{
uppercase:(value)=>value
},
cn:{
uppercase:(value)=>["一","二","三","四","五","六","七","八","九","十"][value-1]
},
en:{
uppercase:(value)=>["One","Two","Three","Four","Five","Six","seven","eight","nine","ten"][value-1]
},
jp:{
}
}
// 当切换到中文时
t("{value | uppercase}",1) // == 一
t("{value | uppercase}",2) // == 二
t("{value | uppercase}",3) // == 三
// 当切换到英文时
t("{value | uppercase}",1) // == One
t("{value | uppercase}",2) // == Two
t("{value | uppercase}",3) // == Three
// 当切换到日文时由于在该语言下没有定义uppercase格式式因此到*中查找
t("{value | uppercase}",1) // == 1
t("{value | uppercase}",2) // == 2
t("{value | uppercase}",3) // == 3
```
### 作用域格式化器
定义在`languages/formatters.js`里面的格式化器仅在当前工程生效,也就是仅在当前作用域生效。一般由应用开发者自行扩展。
关于作用域的概念详见后续介绍。
### 全局格式化器
定义在`@voerkai18n/runtime`里面的格式化器则全局有效,在所有场合均可以使用,但是其优先级低于作用域内的同名格式化器。目前内置的格式化器有:
| 名称 | | 说明 |
| ---- | ---- | ---- |
| | | |
| | | |
| | | |
### 扩展格式化器
除了可以在当前项目`languages/formatters.js`自定义格式化器和`@voerkai18n/runtime`里面的全局格式化器外,单列了`@voerkai18n/formatters`项目用来包含了更多的格式化器。
作为开源项目,欢迎大家提交贡献更多的格式化器。
## 日期时间
@ -792,6 +589,181 @@ languages
`名称空间`仅仅是为了解决当翻译内容太多时的分类问题。
## 切换语言
可以通过全局单例或当前作用域实例切换语言。
```javascript
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例可以直接访问
VoerkaI18n.change("en")
```
侦听语言切换事件:
```javascript
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on((newLanguage)=>{
...
})
//
VoerkaI18n.on((newLanguage)=>{
...
})
```
## Vue应用
`Vue3`应用中引入`voerkai18n`来添加国际化应用需要由两个插件来简化应用。
- **@voerkai18n/vue**
**Vue插件**在初始化Vue应用时引入提供访问`当前语言``切换语言``自动更新`等功能。
- **@voerkai18n/vite**
**Vite插件**,在`vite.config.js`中配置,用来实现`自动文本映射``自动导入t函数`等功能。
`@voerkai18n/vue``@voerkai18n/vite`两件插件相互配合安装配置好这两个插件后就可以在Vue文件使用多语言`t`函数。
**重点:`t`函数会在使用`@voerkai18n/vite`插件后自动注入因此在Vue文件中可以直接使用。**
```Vue
<Script setup>
// 如果没有在vite.config.js中配置`@voerkai18n/vite`插件则需要手工导入t函数
// import { t } from "./languages"
</Script>
<script>
export default {
data(){
return {
username:"",
password:"",
title:t("认证")
}
},
methods:{
login(){
alert(t("登录"))
}
}
}
</script>
<template>
<div>
<h1>{{ t("请输入用户名称") }}</h1>
<div>
<span>{{t("用户名:")}}</span><input type="text" :placeholder="t('邮件/手机号码/帐号')"/>
<span>{{t("密码:")}}</span><input type="password" :placeholder="t('至少6位的密码')"/>
</div>
</div>
<button @click="login">{{t("登录")}}</button>
</div>
</template>
```
**说明:**
- 事实上,就算没有`@voerkai18n/vue``@voerkai18n/vite`两件插件相互配合,只需要导入`t`函数也就可以直接使用。这两个插件只是很简单的封装而已。
- 如果要在应用中进行`语言动态切换`,则需要在应用中引入`@voerkai18n/vue`,请参阅`@voerkai18n/vue`插件使用说明。
- `@voerkai18n/vite`的使用请参阅后续说明。
## React应用
# 高级特性
## 运行时
`@voerkai18n/runtime``voerkai18n`的运行时依赖,支持两种依赖方式。
- **源码依赖**
默认情况下,运行`voerkai18n compile`时会在`languages`文件下生成运行时文件`runtime.js`,该文件被`languages/index.js`引入,里面是核心运行时`ES6`源代码(`@voerkai18n/runtime`源码),也就是在您的工程中是直接引入的运行时代码,因此就不需要额外安装`@voerkai18n/runtime`了。
此时,`@voerkai18n/runtime`源码就成为您工程是一部分。
- **库依赖**
当运行`voerkai18n compile --no-inline-runtime`时,就不会生成运行时文件`runtime.js`,而是采用`import "@voerkai18n/runtime`的方式导入运行时,此时会自动/手动安装`@voerkai18n/runtime`到运行依赖中。
**那么应该选择`源码依赖`还是`库依赖`呢?**
问题的重点在于,在`monorepo`工程或者`开发库`时,`源码依赖`会导致存在重复的运行时源码。而采用`库依赖`,则不存在此问题。因此:
- 普通应用采用`源码依赖`方式,运行`voerkai18n compile `来编译语言包。
- `monorepo`工程或者`开发库`采用`库依赖``voerkai18n compile --no-inline-runtime`来编译语言包。
**注意:**
- `@voerkai18n/runtime`发布了`commonjs``esm`两个经过`babel/rollup`转码后的`ES5`版本。
- 每次运行`voerkai18n compile`时均会重新生成`runtime.js`源码文件,为了确保最新的运行时,请及时更新`@voerkai18n/cli`
- 当升级了`@voerkai18n/runtime`后,需要重新运行`voerkai18n compile`以重新生成`runtime.js`文件。
## 文本映射
虽然`VoerkaI18n`推荐采用`t("中华人民共和国万岁")`形式的符合直觉的翻译形式,而不是采用`t("xxxx.xxx")`这样不符合直觉的形式,但是为什么大部份的国际化方案均采用`t("xxxx.xxx")`形式?
在我们的方案中t("中华人民共和国万岁")形式相当于采用原始文本进行查表,语言名形式如下:
```javascript
// en.js
{
"中华人民共和国":"the people's Republic of China"
}
// jp.js
{
"中华人民共和国":"中華人民共和国"
}
```
很显然,直接使用文本内容作为`key`,虽然符合直觉,但是会造成大量的冗余信息。因此,`voerkai18n compile`会将之编译成如下:
```javascript
//idMap.js
{
"1":"中华人民共和国万岁"
}
// en.js
{
"1":"Long live the people's Republic of China"
}
// jp.js
{
"2":"中華人民共和国"
}
```
如此,就消除了在`en.js``jp.js`文件中的冗余。但是在源代码文件中还存在`t("中华人民共和国万岁")`,整个运行环境中存在两份副本,一份在源代码文件中,一份在`idMap.js`中。
为了进一步减少重复内容,因此,我们需要将源代码文件中的`t("中华人民共和国万岁")`更改为`t("1")`,这样就能确保无重复冗余。但是,很显然,我们不可能手动来更改源代码文件,这就需要由`voerkai18n`提供的一个编译区插件来做这一件事了。
`babel-plugin-voerkai18n`插件为例,该插件同时还完成一份任务,就是自动读取`voerkai18n compile`生成的`idMap.js`文件,然后将`t("中华人民共和国万岁")`自动更改为`t("1")`,这样就完全消除了重复冗余信息。
所以在最终形成的代码中实际上每一个t函数均是`t("1")``t("2")``t("3")``...``t("n")`的形式,最终代码还是采用了用`key`来进行转换,只不过这个过程是自动完成的而已。
**注意:**
- 如果没有启用`babel-plugin-voerkai18n``vite`等编译区插件,还是可以正常工作,但是会有一份默认语言的冗余信息存在。
## 多库联动
`voerkai18n `支持多个库国际化的联动和协作,即**当主程序切换语言时,所有引用依赖库也会跟随主程序进行语言切换**,整个切换过程对所有库开发都是透明的。
@ -826,7 +798,243 @@ import { t } from "../../../languages"
作为国际化解决方案,一般工程的大部份源码中均会使用到翻译函数,这种使用体验比较差。
为此,我们提供了一个`babel`插件来自动完成翻译函数的自动引入。使用方法如下:
为此,我们提供了一个`babel`插件来自动完成翻译函数的自动引入,详见后续`bable`插件、`vite`插件等介绍。
## 自定义格式化器
当我们使用`voerkai18n compile`编译后,会生成`languages/formatters.js`文件,可以在该文件中自定义您自己的格式化器。
`formatters.js`文件内容如下:
```javascript
module.exports = {
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
// [数据类型名称]:(value)=>{...},
// [数据类型名称]:(value)=>{...},
},
cn:{
$types:{
// 所有类型的默认格式化器
"*":{
},
Date:{},
Number:{},
Boolean:{ },
String:{},
Array:{
},
Object:{
}
},
[格式化名称]:(value)=>{.....},
//.....
},
en:{
$types:{
// [数据类型名称]:(value)=>{...},
},
[格式化名称]:(value)=>{.....},
//.....更多的格式化器.....
}
}
```
### 格式化器函数
**每一个格式化器就是一个普通的同步函数**,不支持异步函数,格式化器函数可以支持无参数或有参数。
- 无参数的格式化器:`(value)=>{....返回格式化的结果...}`
- 带参数的格式化器:`(value,arg1,...)=>{....返回格式化的结果...}`,其中`value`是上一个格式化器的输出结果。
### 类型格式化器
可以为每一种数据类型指定一个默认的格式化器,支持对`String``Date``Error``Object``Array``Boolean``Number`等数据类型的格式化。
当插值变量传入时,如果有定义了对应的的类型格式化器,会默认调用该格式化器对数据进行转换。
比如我们定义对`Boolean`类型格式化器,
```javascript
//formatters.js
module.exports = {
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
Boolean:(value)=> value ? "ON" : "OFF"
}
}
t("灯状态:{status}",true) // === 灯状态ON
t("灯状态:{status}",false) // === 灯状态OFF
```
在上例中,如果我们想在不同的语言环境下,翻译为不同的显示文本,则可以为不同的语言指定类型格式化器
```javascript
//formatters.js
module.exports = {
cn:{
$types:{
Boolean:(value)=> value ? "开" : "关"
}
},
en:{
$types:{
Boolean:(value)=> value ? "ON" : "OFF"
}
}
}
// 当切换到中文时
t("灯状态:{status}",true) // === 灯状态:开
t("灯状态:{status}",false) // === 灯状态:关
// 当切换到英文时
t("灯状态:{status}",true) // === 灯状态ON
t("灯状态:{status}",false) // === 灯状态OFF
```
**说明:**
- 完整的类型格式化器定义形式
```javascript
module.exports = {
"*":{
$types:{...}
},
cn:{
$types:{...}
},
en:{
$types:{....}
}
}
```
在匹配应用格式化时会先在当前语言的`$types`中查找匹配的格式化器,如果找不到再上`*.$types`中查找。
- `*.$types`代表当所有语言中均没有定义时才匹配的类型格式化。
- 类型格式化器是**默认执行的,不需要指定名称**。
- 当前作用域的格式化器优先于全局的格式化器。
### 通用的格式化器
类型格式化器只针对特定数据类型,并且会默认调用。而通用的格式化器需要使用`|`管道符进行显式调用。
同样的,通用的格式化器定义在`languages/formatters.js`中。
```javascript
module.exports = {
"*":{
$types:{...},
[格式化名称]:(value)=>{.....},
},
cn:{
$types:{...},
[格式化名称]:(value)=>{.....},
},
en:{
$types:{....},
[格式化名称]:(value)=>{.....},
[格式化名称]:(value,arg)=>{.....},
}
}
```
每一个格式化器均需要指定一个名称,在进行插值替换时会优先依据当前语言来匹配查找格式化器,如果找不到,再到键名为`*`中查找。
```javascript
module.exports = {
"*":{
uppercase:(value)=>value
},
cn:{
uppercase:(value)=>["一","二","三","四","五","六","七","八","九","十"][value-1]
},
en:{
uppercase:(value)=>["One","Two","Three","Four","Five","Six","seven","eight","nine","ten"][value-1]
},
jp:{
}
}
// 当切换到中文时
t("{value | uppercase}",1) // == 一
t("{value | uppercase}",2) // == 二
t("{value | uppercase}",3) // == 三
// 当切换到英文时
t("{value | uppercase}",1) // == One
t("{value | uppercase}",2) // == Two
t("{value | uppercase}",3) // == Three
// 当切换到日文时由于在该语言下没有定义uppercase格式式因此到*中查找
t("{value | uppercase}",1) // == 1
t("{value | uppercase}",2) // == 2
t("{value | uppercase}",3) // == 3
```
### 作用域格式化器
定义在`languages/formatters.js`里面的格式化器仅在当前工程生效,也就是仅在当前作用域生效。一般由应用开发者自行扩展。
### 全局格式化器
定义在`@voerkai18n/runtime`里面的格式化器则全局有效,在所有场合均可以使用,但是其优先级低于作用域内的同名格式化器。目前内置的格式化器有:
| 名称 | | 说明 |
| ---- | ---- | ---- |
| | | |
| | | |
| | | |
### 扩展格式化器
除了可以在当前项目`languages/formatters.js`自定义格式化器和`@voerkai18n/runtime`里面的全局格式化器外,单列了`@voerkai18n/formatters`项目用来包含了更多的格式化器。
作为开源项目,欢迎大家提交贡献更多的格式化器。
## 语言包
当使用`webpack``rollup``esbuild`进行项目打包时,默认语言包采用静态加载,会被打包进行源码中,而其他语言则采用异步打包方式。在`languages/index.js`中。
```javascript
const defaultMessages = require("./cn.js")
const activeMessages = defaultMessages
// 语言作用域
const scope = new i18nScope({
default: defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
....
// 以下为每一种语言生成一个异步打包语句
loaders:{
"en" : ()=>import("./en.js")
"de" : ()=>import("./de.js")
"jp" : ()=>import("./jp.js")
})
```
# 扩展工具
## babel插件
全局安装`@voerkai18n/babel`插件用来进行自动导入`t`函数和自动文本映射。
```javascript
> npm install -g @voerkai18n/babel
> yarn global add @voerkai18n/babel
> pnpm add -g @voerkai18n/babel
```
使用方法如下:
- 在`babel.config.js`中配置插件
@ -840,7 +1048,6 @@ module.expors = {
// 可选,指定语言文件存放的目录,即保存编译后的语言文件的文件夹
// 可以指定相对路径,也可以指定绝对路径
// location:"",
autoImport:"#/languages"
}
]
@ -884,129 +1091,194 @@ module.expors = {
`webpack``rollup`等打包工具也有类似的插件可以实现别名等转换,其目的就是让`babel-plugin-voerkai18n`插件能自动导入固定路径,而不是各种复杂的相对路径。
## 文本映射
## Vue插件
虽然`VoerkaI18n`推荐采用`t("中华人民共和国万岁")`形式的符合直觉的翻译形式,而不是采用`t("xxxx.xxx")`这样不符合直觉的形式,但是为什么大部份的国际化方案均采用`t("xxxx.xxx")`形式?
`vue3`项目中可以安装`@voerkai18n/vue`来实现`枚举语言``语言切换`等功能。
在我们的方案中t("中华人民共和国万岁")形式相当于采用原始文本进行查表,语言名形式如下:
### **安装**
`@voerkai18n/vue`安装为运行时依赖
```javascript
// en.js
{
"中华人民共和国":"the people's Republic of China"
npm install @voerkai18n/vue
pnpm add @voerkai18n/vue
yarn add @voerkai18n/vue
```
### 启用插件
```javascript
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from '@voerkai18n/vue'
import { i18nScope } from './languages'
const app = createApp(Root)
app.use(i18nPlugin,{ i18nScope }) // 重点需要引入i18nScope
app.mount('#app')
```
插件安装成功后,在当前`Vue App`实例上`provide`一个`i18n`响应式实例。
### 注入`i18n`实例
接下来在组件中按需注入`i18n`实例,可以用来访问当前的`激活语言``默认语言``切换语言`等。
```javascript
<script>
import {reactive } from 'vue'
export default {
inject: ['i18n'], // 注入i18n实例该实例由@voerkai18n/vue插件提供
....
}
// jp.js
{
"中华人民共和国":"中華人民共和国"
</script>
```
声明`inject: ['i18n']`后在当前组件实例中就可以访问`this.i18n`,该实例是一个经过`reactive`封闭的响应式对象,其内容是:
```javascript
this.i18n = {
activeLanguage, // 当前激活语言,可以通过直接赋值来切换语言
defaultLanguage, // 默认语言名称
languages // 支持的语言列表=[{name,title},...]
}
```
很显然,直接使用文本内容作为`key`,虽然符合直觉,但是会造成大量的冗余信息。因此,`voerkai18n compile`会将之编译成如下:
### 应用示例
```javascript
//idMap.js
{
"1":"中华人民共和国万岁"
}
// en.js
{
"1":"Long live the people's Republic of China"
}
// jp.js
{
"2":"中華人民共和国"
注入`i18n`实例后就可以在此基础上实现`激活语言``默认语言``切换语言`等功能。
```vue
<script>
import {reactive } from 'vue'
export default {
inject: ['i18n'], // 注入i18n实例该实例由@voerkai18n/vue插件提供
....
}
</script>
<template>
<div>当前语言:{{i18n.activeLanguage}}</div>
<div>默认语言:{{i18n.defaultLanguage}}</div>
<div>
<button
@click="i18n.activeLanguage=lng.name"
v-for="lng of i18n.langauges">
{{ lng.title }}
</button>
</div>
</templage>
```
如此,就消除了在`en.js``jp.js`文件中的冗余。但是在源代码文件中还存在`t("中华人民共和国万岁")`,整个运行环境中存在两份副本,一份在源代码文件中,一份在`idMap.js`中。
### 插件参数
为了进一步减少重复内容,因此,我们需要将源代码文件中的`t("中华人民共和国万岁")`更改为`t("1")`这样就能确保无重复冗余。但是很显然我们不可能手动来更改源代码文件这就需要由babel插件来做这一件事了。
`babel-plugin-voerkai18n`插件同时还完成一份任务,就是自动读取`voerkai18n compile`生成的`idMap.js`文件,然后将`t("中华人民共和国万岁")`自动更改为`t("1")`,这样就完全消除了重复冗余信息。
所以在最终形成的代码中实际上每一个t函数均是`t("1")``t("2")``t("3")``...``t("n")`的形式,最终代码还是采用了用`key`来进行转换,只不过这个过程是自动完成的而已。
**注意:**如果没有启用`babel-plugin-voerkai18n`插件,还是可以正常工作,但是会有一份默认语言的冗余信息存在。
## 切换语言
可以通过全局单例或当前作用域实例切换语言。
`@voerkai18n/vue`插件支持以下参数:
```javascript
import { i18nScope } from "./languages"
import { i18nScope } from './languages'
app.use(i18nPlugin,{
i18nScope, // 重点需要引入当前作用域的i18nScope
forceUpdate:true // 当语言切换时是否强制重新渲染
})
// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例可以直接访问
VoerkaI18n.change("en")
```
侦听语言切换事件:
- 当`forceUpdate=true`时,`@voerkai18n/vue`插件在切换语言时会调用`app._instance.update()`对整个应用进行强制重新渲染。大部分情况下,切换语言时强制对整个应用进行重新渲染的行为是符合预期的。您也可以能够通过设`forceUpdate=false`来禁用强制重新渲染,此时,界面就不会马上看到语言的切换,需要您自己控制进行重新渲染。
-
## Vite插件
`@voerkai18n/babel`插件在`vite`应用中不能正常使用,需要使用`@voerkai18n/vite`插件来完成类似的功能,包括自动文本映射和自动导入`t`函数。
### 安装
`@voerkai18n/vite`只需要作为开发依赖安装即可。
```javascript
import { i18nScope } from "./languages"
npm install --save-dev @voerkai18n/vite
yarn add -D @voerkai18n/vite
pnpm add -D @voerkai18n/vite
```
// 切换到英文
i18nScope.on((newLanguage)=>{
...
### 启用插件
接下来在`vite.config.js`中配置启用`@voerkai18n/vite`插件。
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Inspect from 'vite-plugin-inspect'// 可选的
import Voerkai18nPlugin from "@voerkai18n/vite"
export default defineConfig({
plugins: [
Inspect(), // 可选的
Voerkai18nPlugin({debug:true}),
vue()
]
})
//
VoerkaI18n.on((newLanguage)=>{
...
```
- ` vite-plugin-inspect`是开发`vite`插件时的调试插件,启用后就可以通过`localhost:3000/__inspect/ `查看Vue源码文件经过插件处理前后的内容一般是Vite插件开发者使用。上例中安装后就可以查看`Voerkai18nPlugin`对Vue文件干了什么事可以加深理解**正常使用不需要安装**。
- `vite`插件
### 插件功能
`@voerkai18n/vite`插件配置启用后,`vite`在进行`dev``build`时,就会在`<script setup>....</script>`自动注入`import { t } from "languages" `,同时会扫描源代码文件(包括`vue`,`js`等),根据`idMap.js`文件里面的文本映射表,将`t('"xxxx")`转换成`t("<id>")`的形式。
不同于`@voerkai18n/babel`插件,`@voerkai18n/vite`插件不需要配置`location``autoImport`参数,能正确地处理导入`languages`路径。
### 插件参数
`vite`插件支持以下参数:
```javascript
import Voerkai18nPlugin from "@voerkai18n/vite"
export default defineConfig({
plugins: [
Inspect(), // 可选的
Voerkai18nPlugin({
location: "./", // 可选的,指定当前工程目录
autoImport: true, // 是否自动导入t函数
debug:false, // 是否输出调试信息,当=true时在控制台输出转换匹配的文件清单
patterns:[
"!(?<!.vue\?.*).(css|json|scss|less|sass)$", // 排除所有css文件
/\.vue(\?.*)?/, // 所有vue文件
]
}),
vue()
]
})
```
## 加载语言包
- `location`:可选的,用来指定当前工程目录,一般情况是不需要配置的,会自动取当前文件夹。并且假设`languages`文件夹在`<location>/src/languages`文件夹下。
当使用`webpack``rollup`进行项目打包时,默认语言包采用静态打包,会被打包进行源码中。而其他语言则采用异步打包方式。在`languages/index.js`
- `autoImport`:可选的,默认`true`,用来配置是否自动导入`t`函数。当vue文件没有指定导入时才会自动导入并且根据当前vue文件的路径处理好导入位置。
```javascript
const defaultMessages = require("./cn.js")
const activeMessages = defaultMessages
// 语言作用域
const scope = new i18nScope({
default: defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
....
loaders:{
"en" : ()=>import("./en.js")
"de" : ()=>import("./de.js")
"jp" : ()=>import("./jp.js")
})
```
- `debug`:可选的,开启后会在控制台输出一些调试信息,对一般用户没有用。
## 运行时
- `patterns`:可选的,一些正则表达式匹配规则,用来过滤匹配哪一些文件需要进行扫描和处理。默认的规则:
运行`voerkai18n compile`时会在`languages`文件下生成运行时文件`runtime.js`,该文件被`languages/index.js`引入,里面是核心运行时`ES6`源代码(`@voerkai18n/runtime`代码),也就是在您的工程中是直接引入的运行时代码,因此就不需要额外`@voerkai18n/runtime`了。
每次运行`voerkai18n compile`时就会自动生成`runtime.js`,请及时升级`@voerkai18n/cli`工程以更新运行时代码。
**重点:默认情况下是不需要额外安装`@voerkai18n/runtime`的。**
由于`runtime.js``ES6`代码,在某些情况下,您需要兼容性更好的代码时,就需要进行`babel`转码。比如在普通的`nodejs`应用中。`@voerkai18n/runtime`也提供转码后的发布版本。
当运行`voerkai18n compile --no-inline-runtime`时就不会生成`runtime.js`,而是直接引用的`@voerkai18n/runtime`,而`@voerkai18n/runtime`发布了`commonjs``esm`两个经过`babel/rollup`转码后的版本。
- 当运行`voerkai18n compile`并启用了`--no-inline-runtime`时,在您在工程中就需要额外安装`@voerkai18n/runtime`到运行依赖中。
- 当运行`voerkai18n compile --no-inline-runtime`时,不需要安装`@voerkai18n/runtime`。但是,您的运行环境需要支持`ES6`或者自行转码。大部分`vue/react`等应用均能支持转码。
## babel插件
全局安装`@voerkai18n/babel`插件用来进行自动导入t函数和自动文本映射。
```javascript
> npm install -g @voerkai18n/babel
> yarn global add @voerkai18n/babel
> pnpm add -g @voerkai18n/babel
```
然后在`babel.config.js`中使用,详见上节`自动导入翻译函数`介绍。
## Vue扩展
```javascript
const patterns={
"!(?<!.vue\?.*).(css|json|scss|less|sass)$", // 排除所有css文件
/\.vue(\?.*)?/, // 所有vue文件
"!.*\/node_modules\/.*", // 排除node_modules
"!/.*\/languages\/.*", // 默认排除语言文件
"!\.babelrc", // 排除.babelrc
"!babel\.config\.js", // 排除babel.config.js
"!package\.json$", // 排除package.json
"!vite\.config\.js", // 排除vite.config.js
"!^plugin-vue:.*" // 排除plugin-vue
}
```
`patterns`的匹配规则语法支持`正则表达式字符串``正则表达式`两种用来对经vite处理的文件名称进行匹配和处理。
- `正则表达式`比较容易理解,匹配上的就进行处理。
- `正则表达式字符串`支持一些简单的语法扩展,包括:
- 可以通过前置`!`符号来进行排除匹配。
- 将`**`替换为`.*`,允许使用类似`"/code/apps/test/**/node_modules/**"`的形式来匹配连续路径。
- 将``替换为`[^\/]?`
- 将`*`替换为`[^\/]*`
## React扩展

View File

@ -30,6 +30,7 @@ function expectBabelSuccess(result){
expect(result.includes(`t("4"`)).toBeTruthy()
expect(result.includes(`t("5"`)).toBeTruthy()
}
test("翻译函数转换",done=>{
babel.transform(code, {
plugins: [
@ -63,7 +64,7 @@ test("读取esm格式的idMap后进行翻译函数转换",done=>{
[
i18nPlugin,
{
location:path.join(__dirname, "../packages/demo/apps/lib1/languages"),
location:path.join(__dirname, "../packages/apps/lib1/languages"),
autoImport:"#/languages",
moduleType:"esm",
}
@ -80,7 +81,7 @@ test("读取commonjs格式的idMap后进行翻译函数转换",done=>{
[
i18nPlugin,
{
location:path.join(__dirname, "../packages/demo/apps/lib2/languages"),
location:path.join(__dirname, "../packages/apps/lib2/languages"),
autoImport:"#/languages",
moduleType:"esm",
}