fix react plugin
This commit is contained in:
parent
5904297e35
commit
d9af3b9321
@ -18,7 +18,15 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 第二步:导入`t`翻译函数
|
## 第二步:导入`t`翻译函数
|
||||||
无论采用何种工具创建`React`应用,均可以直接从`languages`直接导入`t`函数。
|
|
||||||
|
`t`翻译函数用来进行文件翻译,普通的`React`应用`t`翻译函数可以用在两个地方:
|
||||||
|
|
||||||
|
- 普通的`js`或`ts`文件
|
||||||
|
- `React`组件`jsx、tsx`文件
|
||||||
|
|
||||||
|
### 在`js`或`ts`文件中使用
|
||||||
|
|
||||||
|
只需要从`languages`直接导入`t`函数即可。
|
||||||
|
|
||||||
```javascript | pure
|
```javascript | pure
|
||||||
import { t } from "./languages"
|
import { t } from "./languages"
|
||||||
@ -33,42 +41,116 @@ import { t } from "../../../languages"
|
|||||||
|
|
||||||
导入`t`函数后就可以直接使用了。
|
导入`t`函数后就可以直接使用了。
|
||||||
|
|
||||||
## 第三步:自动导入`t`翻译函数
|
### 在`React`组件中翻译
|
||||||
|
|
||||||
当源码文件非常多时,手动导入`t`函数比较麻烦,我们提供了`vite`和`babel`两个插件可以实现自动导入`t`函数。
|
在`React`组件中使用`t`函数翻译与在`js`或`ts`文件中使用的最大区别在于:**当切换语言时,需要触发组件的重新渲染**。
|
||||||
如果应用是采用`Vite`+`@vitejs/plugin-react`创建的工程,则可以通过配置`@voerkai18n/vite`插件实现自动导入`t`函数。
|
|
||||||
|
- **配置根组件Provider**
|
||||||
|
|
||||||
|
使用`VoerkaI18nProvider`包装应用根组件,本质上是创建了一个`VoerkaI18nContext.Provider`
|
||||||
|
|
||||||
|
```jsx | pure
|
||||||
|
|
||||||
|
// 1.当前语言Scope
|
||||||
|
import { i18nScope } from "./languages"
|
||||||
|
import { VoerkaI18nProvider } from "@voerkai18n/react"
|
||||||
|
|
||||||
|
export default App(){
|
||||||
|
return (
|
||||||
|
<VoerkaI18nProvider scope={i18nScope}>
|
||||||
|
<MyComponent/>
|
||||||
|
<VoerkaI18nProvider/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **组件中使用翻译函数**
|
||||||
|
|
||||||
|
通过`useVoerkaI18n`获取当前作用域的`t`翻译函数。
|
||||||
|
|
||||||
|
```jsx | pure
|
||||||
|
import { useVoerkaI18n } from "@voerkai18n/react"
|
||||||
|
export function MyComponent(){
|
||||||
|
const { t } = useVoerkaI18n()
|
||||||
|
return (
|
||||||
|
<div>{t("要翻译的内容")}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:在组件中直接使用`import { t } from "languages`也是可以工作的,因为本质上`t`函数仅仅是一个普通的函数。但是当动态切换语言时,对应的组件不能自动重新渲染。因此,需要使用`{ t } = useVoerkaI18n()`导入`t`函数,才可以在切换语言时自动重新渲染组件。
|
||||||
|
|
||||||
|
### 第三步: 自动导入`t`翻译函数
|
||||||
|
|
||||||
|
如果应用是采用`Vite`+`@vitejs/plugin-react`创建的工程,则可以通过配置`@voerkai18n/vite`插件实现自动导入`t`函数和`翻译内容自动映射`等。
|
||||||
|
|
||||||
|
在`vite.config.js`中配置导入安装`@voerkai18n/vite`插件。
|
||||||
|
|
||||||
|
```typescript | pure
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
详见`@voerkai18n/vite`插件介绍。
|
详见`@voerkai18n/vite`插件介绍。
|
||||||
|
|
||||||
|
`@voerkai18n/vite`插件主要干两件事情:
|
||||||
|
|
||||||
|
- 对`js/ts`文件自动导入`t`函数,`jsx/tsx`文件不需要自动导入,只能使用`useVoerkaI18n`。
|
||||||
|
- 根据`idMap.(js|ts)`内容自动替换翻译内容,用来消除冗余内容。
|
||||||
|
|
||||||
|
|
||||||
## 第四步:切换语言
|
## 第四步:切换语言
|
||||||
|
|
||||||
最后,一般需要在应用中提供切换语言并自动重新渲染界面的功能。针对`React`应用,提供了`useVoerkaI18n`来实现此功能。
|
接下来在一般我们还需要实现语言切换的功能界面,`useVoerkaI18n`提供了:
|
||||||
|
- `language`: 当前激活语言名称
|
||||||
|
- `defaultLanguage`: 默认语言名称
|
||||||
|
- `changeLanguage(language)`: 用来切换当前语言
|
||||||
|
- `languages`: 读取当应用支持的语言列表。
|
||||||
|
|
||||||
|
|
||||||
```jsx | pure
|
```jsx | pure
|
||||||
// 如果没有在vite.config.js中配置`@voerkai18n/vite`插件,则需要手工导入t函数
|
|
||||||
import { t } from "./languages"
|
|
||||||
import { useVoerkaI18n } from "@voerkai18n/react"
|
import { useVoerkaI18n } from "@voerkai18n/react"
|
||||||
export default App(){
|
|
||||||
const { activeLanguage,changeLanguage,languages } = useVoerkaI18n()
|
export function MyComponent(){
|
||||||
return (<div>
|
const { t, language,changeLanguage,languages,defaultLanguage } = useVoerkaI18n()
|
||||||
<h1>{t("当前语言")}:{activeLanguage}</h1>
|
return (
|
||||||
<div> {
|
<div>
|
||||||
languages.map(lang=>{
|
<h1>{t("当前语言")}:{language}</h1>
|
||||||
return (<button
|
<h1>{t("默认语言")}:{defaultLanguage}</h1>
|
||||||
key={lang.name}
|
<div> {
|
||||||
onclick={()=>changeLanguage(lang.name)}>
|
languages.map(lang=>{
|
||||||
{lang.title}
|
return (<button
|
||||||
</button>)
|
key={lang.name}
|
||||||
})}
|
onclick={()=>changeLanguage(lang.name)}>
|
||||||
</div>
|
{lang.title}
|
||||||
</div> )
|
</button>)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## 小结
|
## 小结
|
||||||
|
|
||||||
- `useVoerkaI18n`返回当前激活语言、切换语言函数、支持的语言列表。
|
- 使用`<VoerkaI18nProvider scope={i18nScope}>`封装根组件
|
||||||
- 如果需要在切换语言时进行全局重新渲染,一般需要在顶层`App组件`中使用此`hook`, 这样可以确保在切换语言时整个应用进行重新渲染。
|
- `const { t } = useVoerkaI18n()`来导入翻译函数
|
||||||
- 一般切换语言的功能界面不会直接在`App组件`中使用,您可以使用一个专门的组件来切换语言。
|
- 使用`const { language,changeLanguage } = useVoerkaI18n()`来访问切换语言的函数
|
||||||
|
- 在普通`ts/js`文件中使用`import { t } from "./languages"`来导入`t`翻译函数
|
||||||
|
- `@voerkai18n/vite`插件是可选的,仅仅普通`ts/js`文件使用`t`翻译函数时用来自动导入。
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
|
console.log(t("这是一个测试"))
|
@ -7,7 +7,7 @@ import Voerkai18nPlugin from "@voerkai18n/vite"
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
Inspect(), // localhost:3000/__inspect/
|
Inspect(), // localhost:3000/__inspect/
|
||||||
// Voerkai18nPlugin({ debug: true }),
|
Voerkai18nPlugin({ debug: true }),
|
||||||
react()
|
react()
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -1,458 +0,0 @@
|
|||||||
/**
|
|
||||||
* 用于多包环境下的自动发布
|
|
||||||
*
|
|
||||||
* autopublish
|
|
||||||
*
|
|
||||||
* 1.在package.json中添加scripts
|
|
||||||
* {
|
|
||||||
* scripts:{
|
|
||||||
* "publish":"autopublish [options]",
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* 2. 参数
|
|
||||||
* -q: 默认情况下会比对最后一次发布的时间,来决定是否自动发布
|
|
||||||
* 当-q参数被指定时,会询问用户
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require("fs-extra");
|
|
||||||
const inquirer = require("inquirer");
|
|
||||||
const semver = require("semver")
|
|
||||||
const path = require("path");
|
|
||||||
const shelljs = require("shelljs");
|
|
||||||
const createLogger = require("logsets");
|
|
||||||
const TaskListPlugin = require("logsets/plugins/tasklist")
|
|
||||||
const TablePlugin = require("logsets/plugins/table")
|
|
||||||
|
|
||||||
const { Command ,Option} = require('commander');
|
|
||||||
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const relativeTime = require("dayjs/plugin/relativeTime");
|
|
||||||
const { rejects } = require("assert");
|
|
||||||
const { Console } = require("console");
|
|
||||||
const { resourceLimits } = require("worker_threads");
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
require('dayjs/locale/zh-CN')
|
|
||||||
dayjs.locale("zh-CN");
|
|
||||||
|
|
||||||
const logger = createLogger();
|
|
||||||
logger.use(TaskListPlugin)
|
|
||||||
logger.use(TablePlugin)
|
|
||||||
|
|
||||||
const program =new Command()
|
|
||||||
|
|
||||||
// 排除要发布的包
|
|
||||||
const exclude_packages = ["autopublish"]
|
|
||||||
|
|
||||||
function getPackages(){
|
|
||||||
let workspaceRoot = process.cwd()
|
|
||||||
if(!fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))){
|
|
||||||
console.log("命令只能在工作区根目录下执行")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 读取所有包
|
|
||||||
let packages = fs.readdirSync(path.join(workspaceRoot,"packages")).map(packageName=>{
|
|
||||||
const packageFolder = path.join(workspaceRoot,"packages",packageName)
|
|
||||||
const pkgFile = path.join(workspaceRoot,"packages",packageName,"package.json")
|
|
||||||
if(fs.existsSync(pkgFile)){
|
|
||||||
const { name, version,lastPublish,dependencies,devDependencies,description }= fs.readJSONSync(pkgFile)
|
|
||||||
// 读取工作区包依赖
|
|
||||||
let packageDependencies =[]
|
|
||||||
Object.entries({...dependencies,...devDependencies}).forEach(([name,version])=>{
|
|
||||||
if(version.startsWith("workspace:") && !exclude_packages.includes(name.replace("@voerkai18n/",""))){
|
|
||||||
packageDependencies.push(name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
name, // 完整包名
|
|
||||||
description,
|
|
||||||
value:packageName, // 文件夹名称
|
|
||||||
version,
|
|
||||||
lastPublish,
|
|
||||||
isDirty: packageIsDirty(packageFolder), // 包自上次发布之后是否已修改
|
|
||||||
dependencies:packageDependencies // 依赖的工作区包
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).filter(pkgInfo=>pkgInfo && !exclude_packages.includes(pkgInfo.value))
|
|
||||||
|
|
||||||
// 根据依赖关系进行排序
|
|
||||||
for(let i=0;i<packages.length;i++){
|
|
||||||
for(let j=i;j<packages.length;j++){
|
|
||||||
let pkgInfo2 = packages[j]
|
|
||||||
if( packages[i].dependencies.includes(pkgInfo2.name)){
|
|
||||||
let p = packages[i]
|
|
||||||
packages[i] = packages[j]
|
|
||||||
packages[j] = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果某个包isDirty=true,则依赖于其的其他包isDirty=true
|
|
||||||
packages.forEach(package => {
|
|
||||||
if(package.isDirty){
|
|
||||||
packages.forEach(p=>{
|
|
||||||
if(p.name!==package.name && p.dependencies.includes(package.name)){
|
|
||||||
p.isDirty = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return packages
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertInWorkspaceRoot(){
|
|
||||||
const workspaceRoot = process.cwd()
|
|
||||||
if(!fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))){
|
|
||||||
throw new Error("命令只能在工作区根目录下执行")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertInPackageRoot(){
|
|
||||||
const currentFolder = process.cwd()
|
|
||||||
const workspaceRoot = path.join(currentFolder,"../../")
|
|
||||||
|
|
||||||
const inPackageRoot = fs.existsSync(path.join(currentFolder,"package.json")) && fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))
|
|
||||||
|
|
||||||
if(!inPackageRoot){
|
|
||||||
throw new Error("命令只能在工作区的包目录下执行")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行脚本,出错会返回错误信息
|
|
||||||
* @param {*} script
|
|
||||||
*/
|
|
||||||
function execShellScript(script,options={}){
|
|
||||||
let {code,stdout} = shelljs.exec(script,options)
|
|
||||||
if(code>0){
|
|
||||||
new Error(`执行<${script}>失败: ${stdout.trim()}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 异步执行脚本
|
|
||||||
* @param {*} script
|
|
||||||
* @param {*} options
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async function asyncExecShellScript(script,options={}){
|
|
||||||
return new Promise((resolve,reject)=>{
|
|
||||||
shelljs.exec(script,{...options,async:true},(code,stdout)=>{
|
|
||||||
if(code>0){
|
|
||||||
reject(new Error(`执行<${script}>失败: ${stdout.trim()}`))
|
|
||||||
}else{
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 执行脚本并返回结果
|
|
||||||
* @param {*} script
|
|
||||||
*/
|
|
||||||
function execShellScriptReturns(script,options={}){
|
|
||||||
return shelljs.exec(script,options).stdout.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取指定包最近一次更新的时间
|
|
||||||
* 通过遍历所有文件夹
|
|
||||||
* @param {*} folder
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getFolderLastModified(folder,patterns=[],options={}){
|
|
||||||
patterns.push(...[
|
|
||||||
"package.json",
|
|
||||||
"**",
|
|
||||||
"**/*",
|
|
||||||
"!node_modules/**",
|
|
||||||
"!node_modules/**/*",
|
|
||||||
"!**/node_modules/**",
|
|
||||||
"!**/node_modules/**/*",
|
|
||||||
])
|
|
||||||
|
|
||||||
const glob = require("fast-glob")
|
|
||||||
let files = glob.sync(patterns, {
|
|
||||||
cwd: folder,
|
|
||||||
absolute:true,
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
let lastUpdateTime = null
|
|
||||||
for(let file of files){
|
|
||||||
const { mtimeMs } = fs.statSync(file)
|
|
||||||
lastUpdateTime = lastUpdateTime ? Math.max(lastUpdateTime,mtimeMs) : mtimeMs
|
|
||||||
}
|
|
||||||
return lastUpdateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileLastModified(file){
|
|
||||||
const { mtimeMs } = fs.statSync(file)
|
|
||||||
return mtimeMs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} packageInfo {name:"@voerkai18n/autopublish",value:"",version:"1.0.0",lastPublish:"2020-05-01T00:00:00.000Z"}
|
|
||||||
*/
|
|
||||||
async function runPackageScript(workspaceRoot,packageInfo,{silent=false}={}){
|
|
||||||
const packageFolder = path.join(workspaceRoot,"packages",packageInfo.value)
|
|
||||||
const package = fs.readJSONSync(path.join(packageFolder,"package.json"))
|
|
||||||
const lastModified = getFolderLastModified(packageFolder)
|
|
||||||
// 进入包所在的文件夹
|
|
||||||
shelljs.cd(packageFolder)
|
|
||||||
// 每个包必须定义自己的发布脚本
|
|
||||||
if("release" in package.scripts){
|
|
||||||
await asyncExecShellScript(`pnpm release`,{silent})
|
|
||||||
}else{
|
|
||||||
const reason = `包[{}]没有定义自动发布脚本release`
|
|
||||||
if(showLog) logger.log(reason,package.name)
|
|
||||||
throw new Error(`未配置<release>脚本`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 返回指定包自上次发布之后是否有更新过
|
|
||||||
*
|
|
||||||
* @param {*} packageFolder
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function packageIsDirty(packageFolder){
|
|
||||||
const pkgFile = path.join(packageFolder,"package.json")
|
|
||||||
if(!fs.existsSync(pkgFile)){
|
|
||||||
logger.log("当前包[{}]不存在package.json文件",packageFolder)
|
|
||||||
throw new Error("当前包不存在package.json文件")
|
|
||||||
}
|
|
||||||
const package = fs.readJSONSync(pkgFile)
|
|
||||||
const lastModified = getFolderLastModified(packageFolder)
|
|
||||||
const lastPublish = package.lastPublish
|
|
||||||
// 由于上一次发布时会更新package.json文件,如果最后更新的文件时间==package.json文件最后更新时间,则说明没有更新
|
|
||||||
const pkgLastModified = getFileLastModified(pkgFile)
|
|
||||||
|
|
||||||
return dayjs(lastModified).isAfter(dayjs(lastPublish)) && !dayjs(pkgLastModified).isSame(dayjs(lastModified))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发布所有包
|
|
||||||
*
|
|
||||||
* 将比对最后发布时间和最后修改时间的差别来决定是否发布
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param {*} packages
|
|
||||||
*/
|
|
||||||
async function publishAllPackages(packages,options={}){
|
|
||||||
const tasks = logger.tasklist()
|
|
||||||
const workspaceRoot = process.cwd()
|
|
||||||
// 依次对每个包进行发布
|
|
||||||
for(let package of packages){
|
|
||||||
tasks.add(`发布包[${package.name}]`)
|
|
||||||
try{
|
|
||||||
if(package.isDirty){
|
|
||||||
await runPackageScript(workspaceRoot,package,{silent:true,...options})
|
|
||||||
let { version } = fs.readJSONSync(path.join(workspaceRoot,"packages",package.value,"package.json"))
|
|
||||||
tasks.complete(`${package.version}->${version}`)
|
|
||||||
}else{
|
|
||||||
tasks.skip()
|
|
||||||
}
|
|
||||||
}catch(e){
|
|
||||||
tasks.error(`${e.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发布包,并且在package.json中记录最后发布时间
|
|
||||||
* 本命令只能在包文件夹下执行
|
|
||||||
* @param {*} options
|
|
||||||
*/
|
|
||||||
async function publishPackage(options){
|
|
||||||
const { versionIncrementStep,silent=true } = options
|
|
||||||
|
|
||||||
// 此命令需要切换到包所在目录
|
|
||||||
const packageFolder = process.cwd()
|
|
||||||
const packageName = path.basename(packageFolder)
|
|
||||||
const pkgFile = path.join(packageFolder,"package.json")
|
|
||||||
|
|
||||||
if(!fs.existsSync(pkgFile)){
|
|
||||||
logger.log("当前包[{}]不存在package.json文件",packageName)
|
|
||||||
throw new Error("当前包不存在package.json文件,请在包文件夹下执行")
|
|
||||||
}
|
|
||||||
|
|
||||||
let package = fs.readJSONSync(pkgFile)
|
|
||||||
const oldVersion = package.version
|
|
||||||
let packageBackup = Object.assign({},package) // 备份package.json,当操作失败时,还原
|
|
||||||
|
|
||||||
logger.log("发布包:{}",`@voerkai18n/${packageName}`)
|
|
||||||
|
|
||||||
const tasks = logger.tasklist()
|
|
||||||
|
|
||||||
try{
|
|
||||||
// 第一步: 更新版本号和发布时间
|
|
||||||
tasks.add("更新版本号")
|
|
||||||
await asyncExecShellScript(`npm version ${versionIncrementStep}`,{silent})
|
|
||||||
// 重新读取包
|
|
||||||
package = fs.readJSONSync(pkgFile)
|
|
||||||
packageBackup = Object.assign({},package)
|
|
||||||
tasks.complete(`${oldVersion}->${package.version}`)
|
|
||||||
|
|
||||||
// 第二步:构建包
|
|
||||||
if("build" in package.scripts){
|
|
||||||
tasks.add("构建包")
|
|
||||||
await asyncExecShellScript(`pnpm build`,{silent})
|
|
||||||
tasks.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 第三步:发布
|
|
||||||
// 由于工程可能引用了工作区内的其他包,必须pnpm publish才能发布
|
|
||||||
// pnpm publish会修正引用工作区其他包到的依赖信息,而npm publish不能识别工作区内的依赖,会导致报错
|
|
||||||
tasks.add("发布包")
|
|
||||||
await asyncExecShellScript(`pnpm publish --no-git-checks --access public`,{silent})
|
|
||||||
tasks.complete()
|
|
||||||
|
|
||||||
// 第四步:更新发布时间
|
|
||||||
tasks.add("更新发布时间")
|
|
||||||
package.lastPublish = dayjs().format()
|
|
||||||
fs.writeFileSync(pkgFile,JSON.stringify(package,null,4))
|
|
||||||
tasks.complete()
|
|
||||||
|
|
||||||
}catch(e){// 如果发布失败,则还原package.json
|
|
||||||
fs.writeFileSync(pkgFile,JSON.stringify(packageBackup,null,4))
|
|
||||||
tasks.error(`${e.message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
program
|
|
||||||
.command("list")
|
|
||||||
.description("列出各个包的最后一次提交时间和版本信息")
|
|
||||||
.action(options => {
|
|
||||||
assertInWorkspaceRoot()
|
|
||||||
workspaceRoot = process.cwd()
|
|
||||||
const table = logger.table({grid:1})
|
|
||||||
table.addHeader("包名","版本号","最后提交时间","最后修改时间")
|
|
||||||
getPackages().forEach(package => {
|
|
||||||
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("MM/DD hh:mm:ss") : "None"
|
|
||||||
const lastPublishRef = package.lastPublish ? `(${dayjs(package.lastPublish).fromNow()})` : ""
|
|
||||||
const lastModified = getFolderLastModified(path.join(workspaceRoot,"packages",package.value))
|
|
||||||
const lastUpdate = dayjs(lastModified).format("MM/DD hh:mm:ss")
|
|
||||||
const lastUpdateRef = dayjs(lastModified).fromNow()
|
|
||||||
if(package.lastPublish){
|
|
||||||
table.addRow(package.name,package.version,`${lastPublish}(${lastPublishRef})`,`${lastUpdate}(${lastUpdateRef})`)
|
|
||||||
}else{
|
|
||||||
table.addRow(package.name,package.version,"None",`${lastUpdate}(${lastUpdateRef})`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
table.render()
|
|
||||||
generatePackageVersionDoc()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生成包版本列表文件到文档中
|
|
||||||
function generatePackageVersionDoc(){
|
|
||||||
let workspaceRoot = path.join(__dirname,"../../")
|
|
||||||
shelljs.cd(workspaceRoot)
|
|
||||||
let results = []
|
|
||||||
results.push("# 版本信息")
|
|
||||||
results.push("| 包| 版本号| 最后更新|说明|")
|
|
||||||
results.push("| --- | :---:| --- |---|")
|
|
||||||
getPackages().forEach(package => {
|
|
||||||
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("YYYY/MM/DD") : "None"
|
|
||||||
results.push(`|**${package.name}**|${package.version}|${lastPublish}|${package.description}|`)
|
|
||||||
})
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(workspaceRoot,"docs/src/guide/intro/versions.md"), results.join("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function answerForSelectPackages(packages,options){
|
|
||||||
const workspaceRoot = process.cwd()
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
inquirer
|
|
||||||
.prompt([
|
|
||||||
{
|
|
||||||
type: "checkbox",
|
|
||||||
name: "selectPackages",
|
|
||||||
message: "请选择要发布的库:",
|
|
||||||
choices: packages.map(package => {
|
|
||||||
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("MM/DD hh:mm:ss") : "None"
|
|
||||||
const lastPublishRef = package.lastPublish ? `(${dayjs(package.lastPublish).fromNow()})` : ""
|
|
||||||
const lastModified = getFolderLastModified(path.join(workspaceRoot,"packages",package.value))
|
|
||||||
const lastUpdate = dayjs(lastModified).format("MM/DD hh:mm:ss")
|
|
||||||
const lastUpdateRef = dayjs(lastModified).fromNow()
|
|
||||||
return {
|
|
||||||
...package,
|
|
||||||
value: package,
|
|
||||||
name:`${package.name.padEnd(24)}Version: ${package.version.padEnd(8)} LastPublish: ${lastPublish.padEnd(16)}${lastPublishRef} lastModified: ${lastUpdate}(${lastUpdateRef})`, }
|
|
||||||
}),
|
|
||||||
pageSize:12,
|
|
||||||
loop: false
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then((answers) => {
|
|
||||||
resolve(answers.selectPackages)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.log(error.message)
|
|
||||||
reject(error)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 发布包的模式
|
|
||||||
*
|
|
||||||
* 1. 在包中使用
|
|
||||||
* {
|
|
||||||
* scripts:{
|
|
||||||
* "release":"pnpm autopublish"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* 2. 发布所有包
|
|
||||||
* {
|
|
||||||
* scripts:{
|
|
||||||
* "autopublish":"pnpm autopublish -a"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* > pnpm autopublish -- -a // 自动发布,会询问要发布的
|
|
||||||
* > pnpm autopublish -- -a --no-ask // 自动发布,不会询问全自动发布
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const VERSION_STEPS = ["major", "minor", "patch","premajor","preminor","prepatch","prerelease"]
|
|
||||||
program
|
|
||||||
.description("自动发布包")
|
|
||||||
.option("-a, --all", "发布所有包")
|
|
||||||
.option("-n, --no-ask", "不询问")
|
|
||||||
.option("-s, --no-silent", "静默显示脚本输出")
|
|
||||||
.addOption(new Option('-i, --version-increment-step [value]', '版本增长方式').default("patch").choices(VERSION_STEPS))
|
|
||||||
.action(async (options) => {
|
|
||||||
// 发布所有包时只能在工作区根目录下执行
|
|
||||||
if(options.all){
|
|
||||||
assertInWorkspaceRoot()
|
|
||||||
}else{// 发布指定包时只能在包目录下执行
|
|
||||||
assertInPackageRoot()
|
|
||||||
}
|
|
||||||
if(options.all){ // 自动发布所有包
|
|
||||||
const workspaceRoot = process.cwd()
|
|
||||||
let packages = getPackages()
|
|
||||||
if(options.ask){
|
|
||||||
packages = await answerForSelectPackages(packages,options)
|
|
||||||
}
|
|
||||||
if(packages.length > 0){
|
|
||||||
await publishAllPackages(packages,options)
|
|
||||||
}
|
|
||||||
}else{// 只发布指定的包
|
|
||||||
await publishPackage(options)
|
|
||||||
}
|
|
||||||
// 在文档中输出各包的版本信息
|
|
||||||
generatePackageVersionDoc()
|
|
||||||
})
|
|
||||||
|
|
||||||
program.parseAsync(process.argv);
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@voerkai18n/autopublish",
|
|
||||||
"version": "1.0.3",
|
|
||||||
"description": "自动发布工作区的包",
|
|
||||||
"main": "index.js",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"release": "node ./index.js publish"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"autopublish": "./index.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^9.0.0",
|
|
||||||
"fast-glob": "^3.2.11"
|
|
||||||
},
|
|
||||||
"lastPublish": "2022-04-06T15:36:15+08:00"
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# 概述
|
|
||||||
|
|
||||||
`@voerkai18n`项目是一个标准的`monorepo`包工程,包含了`@voerkai18n/cli`、`@voerkai18n/runtime`、`@voerkai18n/utils`、`@voerkai18n/vue`、`@voerkai18n/vite`、`@voerkai18n/babel`、`@voerkai18n/react`、`@voerkai18n/formatters`等多个包,发布包时容易引起混乱问题,最大问题时:
|
|
||||||
- 经常忘记哪个包最近什么时间修改,哪个包应该发布。
|
|
||||||
- 由于包之间存在依赖关系,需要按一定的顺序进行发布
|
|
||||||
|
|
||||||
`@voerkai18n/autopublish`用来实现全自动或手动辅助进行发布。
|
|
||||||
|
|
||||||
**源码与文档:**[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)
|
|
||||||
|
|
||||||
[](https://gitee.com/zhangfisher/voerka-i18n)
|
|
||||||
|
|
||||||
# 使用
|
|
||||||
|
|
||||||
## 准备
|
|
||||||
|
|
||||||
`@voerkai18n/autopublish`用于采用`pnpmp`创建的`monorepo`包工程,不支持`lerna/yarn`。
|
|
||||||
按照常规约定,包存放在`<projectRoot>/packages/<name>`。
|
|
||||||
|
|
||||||
## 第一步:配置包的发布脚本
|
|
||||||
|
|
||||||
将`@voerkai18n/autopublish`添加为包的开发依赖。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 进入包文件夹后执行
|
|
||||||
> pnpm add -D @voerkai18n/autopublish
|
|
||||||
```
|
|
||||||
然后,配置发布脚本:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts":{
|
|
||||||
"build":"默认的包构建命令",
|
|
||||||
"release":"pnpm autopublish"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@voerkai18n/autopublish": "workspace:^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 发布脚本必须为`release`,不能是其他名称,特别是`publish`
|
|
||||||
- `pnpm autopublish`也可以在包路径下单独执行。
|
|
||||||
- `pnpm autopublish`默认依次执行:
|
|
||||||
- `npm version patch`:升级版本号
|
|
||||||
- `pnpm run build`(可选)
|
|
||||||
- `pnpm publish --no-git-checks --access publish`
|
|
||||||
- 默认每次发布均会升级`patch`版本号,可以通过`pnpm autopublish -i <版本递增方式>`来增加版本号,递增方式可选:[`"major"`, `"minor"`, `"patch"`,`"premajor"`,`"preminor"`,`"prepatch"`,`"prerelease"`]
|
|
||||||
- 每次执行`pnpm autopublish`均会在当前包的`package.json`中添加`lastPublish`字段,用来记录发布的时间。这是下一次发布时进行自动比对发布的依据。
|
|
||||||
|
|
||||||
|
|
||||||
## 第二步:配置工作区发布脚本
|
|
||||||
|
|
||||||
在当前工程的根文件夹下配置`package.json`
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts":{
|
|
||||||
"list:package": "node ./packages/autopublish/index.js list",
|
|
||||||
"autopublish": "node ./packages/autopublish/index.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@voerkai18n/autopublish": "workspace:^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
## 第三步:自动发布所有包
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
> pnpm autopublish -- -a -n
|
|
||||||
```
|
|
||||||
|
|
||||||
`pnpm autopublish`会自动枚举出当前所有包,然后对比包路径`packages/<包名>`最后修改时间和`package.json`的`lastPublish`字段值,如果:
|
|
||||||
- 最后修改时间大于最后发布时间,则发布该包
|
|
||||||
- 最后修改时间关于或者小于最后发布时间,则忽略发布该包
|
|
||||||
|
|
||||||
因此,每次当修改完工程后,可以自动执行`pnpm autopublish -- -a -n`就可以进行全自动发布。
|
|
||||||
|
|
||||||
`pnpm autopublish`命令行参数:
|
|
||||||
```shell
|
|
||||||
自动发布包
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-a, --all 发布所有包
|
|
||||||
-n, --no-ask 不询问
|
|
||||||
-s, --no-silent 静默显示脚本输出
|
|
||||||
-i, --version-increment-step [value] 版本增长方式 (choices: "major", "minor", "patch", "premajor", "preminor", "prepatch",
|
|
||||||
"prerelease", default: "patch")
|
|
||||||
-h, --help display help for command
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
list 列出各个包的最后一次提交时间和版本信息
|
|
||||||
```
|
|
||||||
|
|
||||||
- `-a`代表要发布所有包,如果没有启用`-n`,则会让用户选择要发布哪一个包。如果启用`-n`参数,则会全自动比对发布时间和修改时间后发布。
|
|
||||||
- `-no-ask`代表不会询问让用户选择要发布的包.
|
|
||||||
- `--no-silent`代表是否不输出脚本输出。
|
|
||||||
- 由于包之间存在依赖关系,`autopublish`会根据依赖关系进行排序发布和关联发布。比如`@voerkai18n/cli`依赖于`@voerkai18n/utils`,当`@voerkai18n/utils`有更新需要发布时,`@voerkai18n/cli`也会自动发布。
|
|
||||||
|
|
||||||
|
|
||||||
## 第四步: 手动选择发布
|
|
||||||
|
|
||||||
`pnpm autopublish -- -a -n`会根据发布时间和修改时间进行自动发布。也支持手动选择要发布的包。
|
|
||||||
```javascript
|
|
||||||
> pnpm autopublish -- -a
|
|
||||||
````
|
|
||||||
如果不启用`-n`参数,则会列出当前工作区的所有包,让用户选择要发布的包。
|
|
||||||
|
|
||||||
## 列出包
|
|
||||||
|
|
||||||
`pnpm autopublish -- list`列出当前工程的所有包,并显示当前包最近更新和最近发布时间。
|
|
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
1
packages/react/index.d.ts
vendored
1
packages/react/index.d.ts
vendored
@ -4,6 +4,7 @@ import type React from "react"
|
|||||||
export type useVoerkaI18n = ()=>{
|
export type useVoerkaI18n = ()=>{
|
||||||
language:string
|
language:string
|
||||||
changeLanguage:(newLanguage:string)=>Promise<void>
|
changeLanguage:(newLanguage:string)=>Promise<void>
|
||||||
|
defaultLanguage:string, // 默认语言
|
||||||
languages:VoerkaI18nSupportedLanguages,
|
languages:VoerkaI18nSupportedLanguages,
|
||||||
t:VoerkaI18nTranslate
|
t:VoerkaI18nTranslate
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import React, { useState, useEffect,useContext,useCallback} from 'react';
|
|||||||
export const VoerkaI18nContext = React.createContext({
|
export const VoerkaI18nContext = React.createContext({
|
||||||
languages:null,
|
languages:null,
|
||||||
language:'zh',
|
language:'zh',
|
||||||
|
defaultLanguage:null,
|
||||||
changeLanguage:() => {},
|
changeLanguage:() => {},
|
||||||
t:()=>{}
|
t:()=>{}
|
||||||
})
|
})
|
||||||
@ -30,6 +31,7 @@ export function VoerkaI18nProvider(props){
|
|||||||
<VoerkaI18nContext.Provider value={{
|
<VoerkaI18nContext.Provider value={{
|
||||||
language,
|
language,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
|
defaultLanguage:VoerkaI18n.defaultLanguage,
|
||||||
languages:VoerkaI18n.languages,
|
languages:VoerkaI18n.languages,
|
||||||
t:scope.t
|
t:scope.t
|
||||||
}}>
|
}}>
|
||||||
|
@ -59,11 +59,12 @@ const importTRegex = /^[^\w\r\n]*import\s*\{(.*)\bt\b(.*)\}\sfrom/gm
|
|||||||
|
|
||||||
function replaceCode(code, idmap) {
|
function replaceCode(code, idmap) {
|
||||||
return code.replaceAll(TranslateRegex, (message) => {
|
return code.replaceAll(TranslateRegex, (message) => {
|
||||||
if(message in idmap) {
|
if(message in idmap) {
|
||||||
return idmap[message]
|
return idmap[message]
|
||||||
}else{
|
}else{
|
||||||
return message
|
const msg = unescape(message.replaceAll("\\u","%u"))
|
||||||
}
|
return msg in idmap ? idmap[msg] : message
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
|
|||||||
/\.vue(\?.*)?/, // 所有vue文件
|
/\.vue(\?.*)?/, // 所有vue文件
|
||||||
"!(?<!.jsx\?.*).(css|json|scss|less|sass)$",
|
"!(?<!.jsx\?.*).(css|json|scss|less|sass)$",
|
||||||
/\.jsx(\?.*)?/,
|
/\.jsx(\?.*)?/,
|
||||||
|
/\.ts(\?.*)?/,
|
||||||
] // 提取范围
|
] // 提取范围
|
||||||
},opts)
|
},opts)
|
||||||
|
|
||||||
@ -103,7 +105,7 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
|
|||||||
let languageFolder = getProjectLanguageFolder(projectRoot)
|
let languageFolder = getProjectLanguageFolder(projectRoot)
|
||||||
|
|
||||||
if(!fs.existsSync(languageFolder)){
|
if(!fs.existsSync(languageFolder)){
|
||||||
console.warn(`Voerkai18n语言文件夹不存在,@voerkai18n/vite未启用`)
|
console.warn(`Voerkai18n语言文件夹不存在,@voerkai18n/vite未启用`)
|
||||||
}
|
}
|
||||||
if(debug){
|
if(debug){
|
||||||
console.log("Project root: ",projectRoot)
|
console.log("Project root: ",projectRoot)
|
||||||
@ -141,8 +143,9 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
|
|||||||
importSource = "./" + importSource
|
importSource = "./" + importSource
|
||||||
}
|
}
|
||||||
importSource=importSource.replace("\\","/")
|
importSource=importSource.replace("\\","/")
|
||||||
|
const extName = path.extname(id)
|
||||||
// 转换Vue文件
|
// 转换Vue文件
|
||||||
if(path.extname(id)==".vue"){
|
if(extName==".vue"){
|
||||||
// 优先在<script setup></script>中导入
|
// 优先在<script setup></script>中导入
|
||||||
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
|
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
|
||||||
if(setupScriptRegex.test(code)){
|
if(setupScriptRegex.test(code)){
|
||||||
@ -150,7 +153,7 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
|
|||||||
}else{// 如果没有<script setup>则在<script></script>中导入
|
}else{// 如果没有<script setup>则在<script></script>中导入
|
||||||
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
|
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
|
||||||
}
|
}
|
||||||
}else{// 普通js文件需要添加到最前面
|
}else if(['.js','.ts'].includes(extName)){// 普通js/ts文件需要添加到最前面
|
||||||
code = code = `import { t } from '${importSource}';\n${code}`
|
code = code = `import { t } from '${importSource}';\n${code}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -229,6 +229,7 @@ importers:
|
|||||||
rollup-plugin-clear: ^2.0.7
|
rollup-plugin-clear: ^2.0.7
|
||||||
rollup-plugin-terser: ^7.0.2
|
rollup-plugin-terser: ^7.0.2
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@babel/runtime': 7.18.9
|
||||||
'@babel/runtime-corejs3': 7.20.7
|
'@babel/runtime-corejs3': 7.20.7
|
||||||
core-js: 3.21.1
|
core-js: 3.21.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@ -236,7 +237,6 @@ importers:
|
|||||||
'@babel/core': 7.18.10
|
'@babel/core': 7.18.10
|
||||||
'@babel/plugin-transform-runtime': 7.18.10_@babel+core@7.18.10
|
'@babel/plugin-transform-runtime': 7.18.10_@babel+core@7.18.10
|
||||||
'@babel/preset-env': 7.18.10_@babel+core@7.18.10
|
'@babel/preset-env': 7.18.10_@babel+core@7.18.10
|
||||||
'@babel/runtime': 7.18.9
|
|
||||||
'@rollup/plugin-babel': 5.3.1_tui6liyexu3zy4m5r2rytc7ixu
|
'@rollup/plugin-babel': 5.3.1_tui6liyexu3zy4m5r2rytc7ixu
|
||||||
'@rollup/plugin-commonjs': 21.1.0_rollup@2.77.2
|
'@rollup/plugin-commonjs': 21.1.0_rollup@2.77.2
|
||||||
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.77.2
|
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.77.2
|
||||||
@ -1504,7 +1504,6 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.13.9
|
regenerator-runtime: 0.13.9
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@babel/standalone/7.18.10:
|
/@babel/standalone/7.18.10:
|
||||||
resolution: {integrity: sha512-0KWHiRX9TUHiWE+dKYYEOIiRJcPwGU6u8Bq/p+ldekj7Kew9PCwl4S4FTSEPpTrn3Vc+r3iRSaN1l9AcGgLx4Q==}
|
resolution: {integrity: sha512-0KWHiRX9TUHiWE+dKYYEOIiRJcPwGU6u8Bq/p+ldekj7Kew9PCwl4S4FTSEPpTrn3Vc+r3iRSaN1l9AcGgLx4Q==}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user