This commit is contained in:
wxzhang 2022-04-11 21:48:17 +08:00
parent af2384683d
commit 0e77b21127
66 changed files with 12864 additions and 1623 deletions

44
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: 部署文档
on:
push:
branches:
# 确保这是你正在使用的分支名称
- master
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
# 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true
- uses: actions/cache@v3
id: node-modules
with:
path: node_modules/
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-modules-
- name: 安装依赖
if: steps.node-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=4096
run: yarn run docs:build
- name: 部署文档
uses: JamesIves/github-pages-deploy-action@v4
with:
# 这是文档部署到的分支名称
branch: gh-pages
folder: docs/.vuepress/dist

29
docs/.vuepress/config.ts Normal file
View File

@ -0,0 +1,29 @@
import { defineHopeConfig } from "vuepress-theme-hope";
import themeConfig from "./themeConfig";
export default defineHopeConfig({
base: "/voerka-i18n/",
head: [
[
"link",
{
rel: "stylesheet",
href: "//at.alicdn.com/t/font_2410206_mfj6e1vbwo.css",
},
],
],
locales: {
"/": {
lang: "zh-CN",
title: "VoerkaI18n",
description: "适用于Nodejs/Vue/React的国际化解决方案",
},
"/en/": {
lang: "en-US",
title: "VoerkaI18n",
description: "适用于Nodejs/Vue/React的国际化解决方案",
}
},
themeConfig,
});

View File

@ -0,0 +1,7 @@
import { defineNavbarConfig } from "vuepress-theme-hope";
export const en = defineNavbarConfig([
"/en/",
"/home",
{ text: "Guide", icon: "creative", link: "/guide/" }
]);

View File

@ -0,0 +1,2 @@
export * from "./en";
export * from "./zh";

View File

@ -0,0 +1,23 @@
import { defineNavbarConfig } from "vuepress-theme-hope";
export const zh = defineNavbarConfig([
{
text: "主页",
icon: "home",
link: "/"
},
{
text: "指南",
link: "/zh/guide/intro"
},
{
text: "参考",
link: "/zh/reference",
},
{
text: "贡献源码",
link: "/zh/contribute",
}
]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,16 @@
import { defineSidebarConfig } from "vuepress-theme-hope";
export const en = defineSidebarConfig({
"/en/": [
"",
"home",
"slide",
{
icon: "creative",
text: "Guide",
prefix: "guide/",
link: "guide/",
children: "structure",
}
],
});

View File

@ -0,0 +1,2 @@
export * from "./en";
export * from "./zh";

View File

@ -0,0 +1,60 @@
import { defineSidebarConfig } from "vuepress-theme-hope";
export const zh = defineSidebarConfig({
"/zh/guide/": [
{
text:"开始",
prefix:"intro/",
children:[
"",
"install.md",
"get-started.md",
]
},
{
text:"指南",
link:false,
prefix:"use/",
children:[
"t",
"interpolation",
"datetime",
"plural",
"currency",
"namespace",
"change-langeuage",
"vue",
"react"
]
},
{
text:"高级特性",
prefix:"advanced/",
children:[
"runtime",
"textMap",
"multi-libs",
"autoimport",
"customformatter",
"langpack",
"autotranslate"
]
},
{
text:"工具",
prefix:"tools/",
children:[
"cli",
"babel",
"vue",
"vite",
]
}
],
"/zh/reference": [
"i18nscope",
"voerkai18n",
"formatters",
"lang-code"
]
});

View File

@ -0,0 +1,5 @@
.sidebar-group > p.sidebar-heading > span.title {
font-weight: bold;
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,84 @@
import { defineThemeConfig } from "vuepress-theme-hope";
import * as navbar from "./navbar";
import * as sidebar from "./sidebar";
export default defineThemeConfig({
hostname: "https://vuepress-theme-hope-v2-demo.mrhope.site",
author: {
name: "wxzhang",
url: "https://mrhope.site",
},
iconPrefix: "iconfont icon-",
logo: "/logo.svg",
home:"/zh/home",
repo: "vuepress-theme-hope/vuepress-theme-hope",
docsDir: "docs",
breadcrumb :false,
pageInfo: ["Author", "Original", "Date", "Category", "Tag", "ReadingTime"],
locales: {
/**
* Chinese locale config
*/
"/": {
// navbar
navbar: navbar.zh,
// sidebar
sidebar: sidebar.zh,
//footer: "默认页脚",
displayFooter: true
},
"/en/": {
// navbar
navbar: navbar.en,
// sidebar
sidebar: sidebar.en,
footer: "Default footer",
displayFooter: true,
}
},
plugins: {
// If you don't need comment feature, you can remove following option
// The following config is for demo ONLY, if you need comment feature, please generate and use your own config, see comment plugin documentation for details.
// To avoid disturbing the theme developer and consuming his resources, please DO NOT use the following config directly in your production environment!!!!!
// comment: {
// /**
// * Using giscus
// */
// type: "giscus",
// repo: "vuepress-theme-hope/giscus-discussions",
// repoId: "R_kgDOG_Pt2A",
// category: "Announcements",
// categoryId: "DIC_kwDOG_Pt2M4COD69",
// /**
// * Using twikoo
// */
// // type: "twikoo",
// // envId: "https://twikoo.ccknbc.vercel.app",
// /**
// * Using Waline
// */
// // type: "waline",
// // serverURL: "https://vuepress-theme-hope-comment.vercel.app",
// },
mdEnhance: {
enableAll: true,
presentation: {
plugins: ["highlight", "math", "search", "notes", "zoom"],
},
},
},
});

42
docs/en/guide/disable.md Normal file
View File

@ -0,0 +1,42 @@
---
index: 3
title: Component disabled
icon: config
category:
- Guide
tag:
- disable
navbar: false
sidebar: false
breadcrumb: false
pageInfo: false
contributors: false
editLink: false
lastUpdated: false
prev: false
next: false
comment: false
footer: false
backtotop: false
---
You can disable some functions on the page by setting the Frontmatter of the page.
<!-- more -->
These should be disabled on this page:
- Navbar
- Sidebar
- Breadcrumb
- Page information
- Contributors
- Edit link
- Update time
- Prev/Next link
- Comment
- Footer
- Back to top button

15
docs/en/guide/encrypt.md Normal file
View File

@ -0,0 +1,15 @@
---
icon: lock
category:
- Guide
tag:
- encryption
---
# Encryption article
The actual article content.
Paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text paragraph 1 text.
Paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text paragraph 2 text.

40
docs/en/guide/install.md Normal file
View File

@ -0,0 +1,40 @@
# 安装
`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成:
- **@voerkai18/cli**
包含文本提取/编译等命令行工具,一般应该安装到全局。
```javascript
npm install --g @voerkai18/cli
yarn global add @voerkai18/cli
pnpm add -g @voerkai18/cli
```
- **@voerkai18/runtime**
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装,一般仅在开发库项目时采用独立的运行时依赖。
```javascript
npm install --save @voerkai18/runtime
yarn add @voerkai18/runtime
pnpm add @voerkai18/runtime
```
- **@voerkai18/formatters**
**可选的**,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
- **@voerkai18/babel**
可选的`babel`插件,用来实现自动导入翻译函数和翻译文本映射自动替换。
- **@voerkai18/vue**
可选的`vue`插件用来为Vue应用提供语言动态切换功能。
- **@voerkai18/vite**
可选的`vite`插件,用来为`vite`应用提供自动导入翻译函数和翻译文本映射自动替换。

341
docs/en/guide/markdown.md Normal file
View File

@ -0,0 +1,341 @@
---
index: 2
icon: markdown
title: Markdown Enhance
category:
- Guide
tag:
- markdown
---
Every document page in VuePress is rendered by Markdown.
You need to build your document or blog page by creating and writing Markdown in the corresponding path.
<!-- more -->
## Markdown introduction
If you are a new learner and dont know how to write Markdown, please read [Markdown Intro](https://vuepress-theme-hope.github.io/v2/basic/markdown/README.html) and [Markdown Demo](https://vuepress-theme-hope.github.io/v2/basic/markdown/demo.html).
::: info Frontmatter
Frontmatter is a important concept in VuePress. If you dont know it, you need to read [Frontmatter Introduction](https://vuepress-theme-hope.github.io/v2/basic/vuepress/page.html#front-matter).
:::
## VuePress Enhancement
To enrich document writing, VuePress has extended Markdown syntax.
For these extensions, please read [Markdown extensions in VuePress](https://vuepress-theme-hope.github.io/v2/basic/vuepress/markdown.html).
## Theme Enhancement
### Enable all
You can set `themeconfig.plugins.htmlEnhance.enableAll` to enable all features of the [md-enhance](https://vuepress-theme-hope.github.io/v2/md-enhance/) plugin.
```js {3-5}
module.exports = {
themeConfig: {
plugins: {
mdEnhance: {
enableAll: true,
},
},
},
};
```
## New Feature
### Custom Container
::: v-pre
Safely use {{ variable }} in markdown.
:::
::: info Custom Title
A custom information container with `code`, [link](#markdown).
```js
const a = 1;
```
:::
::: tip Custom Title
A custom tip container
:::
::: warning Custom Title
A custom warning container
:::
::: danger Custom Title
A custom danger container
:::
::: details Custom Title
A custom details container
:::
:::: details Code
```md
::: v-pre
Safely use {{ variable }} in markdown.
:::
::: info Custom Title
A custom information container
:::
::: tip Custom Title
A custom tip container
:::
::: warning Custom Title
A custom warning container
:::
::: danger Custom Title
A custom danger container
:::
::: details Custom Title
A custom details container
:::
```
::::
### CodeGroup
:::: code-group
::: code-group-item yarn
```bash
yarn add -D vuepress-theme-hope
```
:::
::: code-group-item npm:active
```bash
npm i -D vuepress-theme-hope
```
:::
::::
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/code-group.html)
### Superscript and Subscript
19^th^ H~2~O
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/sup-sub.html)
### Align
::: center
I am center
:::
::: right
I am right align
:::
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/align.html)
### Footnote
This text has footnote[^first].
[^first]: This is footnote content
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/footnote.html)
### Mark
You can mark ==important words== .
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/mark.html)
### Tasklist
- [x] Plan A
- [ ] Plan B
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/tasklist.html)
### Chart
::: chart A Scatter Chart
```json
{
"type": "scatter",
"data": {
"datasets": [
{
"label": "Scatter Dataset",
"data": [
{ "x": -10, "y": 0 },
{ "x": 0, "y": 10 },
{ "x": 10, "y": 5 },
{ "x": 0.5, "y": 5.5 }
],
"backgroundColor": "rgb(255, 99, 132)"
}
]
},
"options": {
"scales": {
"x": {
"type": "linear",
"position": "bottom"
}
}
}
}
```
:::
- [View Detail](<[chart.md](https://vuepress-theme-hope.github.io/v2/guide/markdown/chart.html)>)
### Flowchart
```flow
cond=>condition: Process?
process=>operation: Process
e=>end: End
cond(yes)->process->e
cond(no)->e
```
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/flowchart.html)
### Mermaid
```mermaid
flowchart TB
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
one --> two
three --> two
two --> c2
```
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/mermaid.html)
### Tex
$$
\frac {\partial^r} {\partial \omega^r} \left(\frac {y^{\omega}} {\omega}\right)
= \left(\frac {y^{\omega}} {\omega}\right) \left\{(\log y)^r + \sum_{i=1}^r \frac {(-1)^i r \cdots (r-i+1) (\log y)^{r-i}} {\omega^i} \right\}
$$
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/tex.html)
### Code Demo
::: demo A normal demo
```html
<h1>VuePress Theme Hope</h1>
<p>Is <span id="very">very</span> powerful!</p>
```
```js
document.querySelector("#very").addEventListener("click", () => {
alert("Very powerful!");
});
```
```css
span {
color: red;
}
```
:::
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/demo.html)
### Presentation
@slidestart
## Slide 1
A paragraph with some text and a [link](https://mrhope.site)
---
## Slide 2
- Item 1
- Item 2
---
## Slide 3.1
```js
const a = 1;
```
--
## Slide 3.2
$$
J(\theta_0,\theta_1) = \sum_{i=0}
$$
@slideend
- [View Detail](https://vuepress-theme-hope.github.io/v2/guide/markdown/presentation.html)

66
docs/en/guide/page.md Normal file
View File

@ -0,0 +1,66 @@
---
# This control sidebar index
index: 1
# This is the icon of the page
icon: page
# This is the title of the article
title: page config
# Set author
author: Ms.Hope
# Set writing time
date: 2020-01-01
# A page can have multiple categories
category:
- Guide
# A page can have multiple tags
tag:
- Page config
- Guide
# this page is sticky in article list
sticky: true
# this page will appear in aricle channel in home page
star: true
# You can customize the footer
footer: Footer content for test
---
Content before `more` comment is regarded as page excerpt.
<!-- more -->
## Page information
You can set page information in Markdowns Frontmatter.
- The author is set to Ms.Hope.
- The writing time should be January 1, 2020
- Category is "Guide"
- Tags are "Page Config" and "Guide"
## Page content
You are free to write your Markdown here.
::: tip
- Please use the relative link `./` for pictures in the Markdown folder.
- For pictures in `.vuepress/public` folder, please use absolute link `/` for reference
:::
The theme contains a custom badge:
> A dark blue badge text badge at the end of line. <Badge text="Badge text" color="#242378" />
## Page structure
This page should contain:
- Back to top button
- Route navigation
- Comments
- Footer

82
docs/en/guide/readme.md Normal file
View File

@ -0,0 +1,82 @@
---
headerDepth: 3
---
# 前言
基于`javascript`的国际化方案很多,比较有名的有`fbt``i18next``react-i18next``vue-i18n``react-intl`等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子想造一个轮子,也不想想自己什么水平。
哪么到底是对现有解决方案有什么不满?最主要有三点:
- 大部份均为要翻译的文本信息指定一个`key`,然后在源码文件中使用形如`$t("message.login")`之类的方式,然后在翻译时将之转换成最终的文本信息。此方式最大的问题是,在源码中必须人为地指定每一个`key`,在中文语境中,想为每一句中文均配套想一句符合语义的`英文key`是比较麻烦的,也很不直观不符合直觉。我希望在源文件中就直接使用中文,如`t("中华人民共和国万岁")`,然后国际化框架应该能自动处理后续的一系列麻烦。
- 要能够比较友好地支持多库多包`monorepo`场景下的国际化协作,当主程序切换语言时,其他包或库也可以自动切换,并且在开发上每个包或库均可以独立地进行开发,集成到主程序时能无缝集成。这点在现有方案上没有找到比较理想的解决方案。
- 大部份国际化框架均将中文视为二等公民,大部份情况下您应该采用英文作为第一语言,虽然这不是太大的问题,但是既然要再造一个轮子,为什么不将中文提升到一等公民呢。
基于此就开始造出`VoerkaI18n`这个**全新的国际化多语言解决方案**,主要特性包括:
# 主要特性
- 全面工程化解决方案,提供初始化、提取文本、自动翻译、编译等工具链支持。
- 符合直觉,不需要手动定义文本`Key`映射。
- 强大的插值变量`格式化器`机制,可以扩展出强大的多语言特性。
- 支持`babel`插件自动导入`t`翻译函数。
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
- 采用`工具链``运行时`分开设计,发布时只需要集成很小的运行时。
- 高度可扩展的`复数``货币``数字`等常用的多语言处理机制。
- 翻译过程内,提取文本可以自动进行同步,并保留已翻译的内容。
- 可以随时添加支持的语言
- 支持调用在线自动翻译对提取文本进行翻译。
# 安装
`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成:
- **@voerkai18/cli**
包含文本提取/编译等命令行工具,一般应该安装到全局。
```javascript
npm install --g @voerkai18/cli
yarn global add @voerkai18/cli
pnpm add -g @voerkai18/cli
```
- **@voerkai18/runtime**
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装,一般仅在开发库项目时采用独立的运行时依赖。
```javascript
npm install --save @voerkai18/runtime
yarn add @voerkai18/runtime
pnpm add @voerkai18/runtime
```
- **@voerkai18/formatters**
**可选的**,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
- **@voerkai18/babel**
可选的`babel`插件,用来实现自动导入翻译函数和翻译文本映射自动替换。
- **@voerkai18/vue**
可选的`vue`插件用来为Vue应用提供语言动态切换功能。
- **@voerkai18/vite**
可选的`vite`插件,用来为`vite`应用提供自动导入翻译函数和翻译文本映射自动替换。

60
docs/readme.md Normal file
View File

@ -0,0 +1,60 @@
---
home: true
icon: home
title: 主页
heroImage: /logo.svg
heroText: VoerkaI18n
tagline: 适用于Nodejs/Vue/React的国际化解决方案
actions:
- text: 快速入门
link: /zh/guide/intro/get-started
- text: 源码
link: https://gitee.com/zhangfisher/voerka-i18n
type: secondary
features:
- title: 工程化支持
icon: markdown
details: 从文本提取/自动翻译/编译/动态切换的全流程工程化支持,适用于大型项目
link:
- title: 集成自动翻译
icon: slides
details: 调用在线翻译服务API支持对提取的文本进行自动翻译大幅度提高工程效率
link:
- title: 符合直觉
icon: layout
details: 在源码中直接使用符合直觉的翻译形式不需要绞尽脑汁想种种key
link:
- title: 自动提取文本
icon: comment
details: 提供扫描提取工具对源码文件中需要翻译的文本进行提取
link:
- title: 适用性
icon: info
details: 支持任意Javascript应用包括Nodejs/Vue/React/ReactNative等。
link:
- title: 多库协作
icon: blog
details: 支持monorepo工程下多库进行语言切换的联动机制
link:
- title: 自动扩展工具
icon: palette
details: 提供Vue/React/Babel等扩展插件简化各种应用下
link:
- title: 扩展特性
icon: contrast
details: 强大的插值变量机制,能扩展支持复数、日期、货币等灵活强大的多语言机制
link:
footer: MIT Licensed | Copyright © 2022-present wxzhang
---

View File

@ -0,0 +1,73 @@
---
sidebar: heading
---
# 源码贡献
`voerkai18n`是开源项目,欢迎大家贡献源码。
## 获取源码
`voerkai18n`在Github和[Gitee](https://gitee.com/zhangfisher/voerka-i18n)上面开源。
### 拉取源码
```shell
git clone https://gitee.com/zhangfisher/voerka-i18n
```
### 安装依赖
`voerkai18n`是一个`monorepo`多包工程,使用`pnpm`作为包管理器。所以首先需要安装`pnpm`
```javascript
> npm install -g pnpm
```
然后再使用`pnpm install`
## 源码结构
```javascript
voerkai18n
packages
autopublish // 自动发布工具,仅用于开发阶段
babel // @voerkai18n/babel插件
cli // @voerkai18n/cli命令行工具
formatters // @voerkai18n/formatters通用的格式化器
react // @voerkai18n/react
runtime // @voerkai18n/runtime
utils // @voerkai18n/utils工具库
vite // @voerkai18n/vite插件
vue // @voerkai18n/vue插件
apps // 测试应用
test // 单元测试用例
docs // 文档网站Vuepress
readme.md
```
## 工作原理
![](/images/arch.png)
## 开发格式化器
## 单元测试
## 文档
## 发布

View File

@ -0,0 +1,25 @@
# 自动导入翻译函数
使用`voerkai18 compile`后,要进行翻译时需要从`./languages`导入`t`翻译函数。
```javascript
import { t } from "./languages"
```
由于默认情况下,`voerkai18 compile`命令会在当前工程的`/languages`文件夹下,这样我们为了导入`t`翻译函数不得不使用各种相对引用,这即容易出错,又不美观,如下:
```javascript
import { t } from "./languages"
import { t } from "../languages"
import { t } from "../../languages"
import { t } from "../../../languages"
```
作为国际化解决方案,一般工程的大部份源码中均会使用到翻译函数,这种使用体验比较差。
为此,我们提供了一个几个插件可以来自动完成翻译函数的自动引入,包括:
- `babel`插件
- `vite`插件
关于插件如何使用请参阅文档。

View File

@ -0,0 +1,9 @@
# 自动翻译
传统的国际化解决方案均是需要手工进行翻译的,`voerkai18n`解决方案支持调用在线翻译服务进行自动翻译。
- 内置的`voerkai18n translate`命令能调用在线翻译服务完成对提取的文本的自动翻译。
- 目前支持访问百度在线API进行自动翻译。百度提供了免费的在线API虽然只支持`QPS=1`,即每秒调用一次。但是`voerkai18n translate`命令会对要翻译的文本进行合并后再调用,因此大部分情况下,均足够使用了。
`voerkai18n translate`命令的使用请参阅扩展文档。

View File

@ -0,0 +1,197 @@
# 自定义格式化器
当我们使用`voerkai18n compile`编译后,会生成`languages/formatters.js`文件,可以在该文件中自定义您自己的格式化器。
`formatters.js`文件内容如下:
```javascript
module.exports = {
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
// [数据类型名称]:(value)=>{...},
// [数据类型名称]:(value)=>{...},
},
zh:{
$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 = {
zh:{
$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:{...}
},
zh:{
$types:{...}
},
en:{
$types:{....}
}
}
```
在匹配应用格式化时会先在当前语言的`$types`中查找匹配的格式化器,如果找不到再上`*.$types`中查找。
- `*.$types`代表当所有语言中均没有定义时才匹配的类型格式化。
- 类型格式化器是**默认执行的,不需要指定名称**。
- 当前作用域的格式化器优先于全局的格式化器。
## 通用的格式化器
类型格式化器只针对特定数据类型,并且会默认调用。而通用的格式化器需要使用`|`管道符进行显式调用。
同样的,通用的格式化器定义在`languages/formatters.js`中。
```javascript
module.exports = {
"*":{
$types:{...},
[格式化名称]:(value)=>{.....},
},
zh:{
$types:{...},
[格式化名称]:(value)=>{.....},
},
en:{
$types:{....},
[格式化名称]:(value)=>{.....},
[格式化名称]:(value,arg)=>{.....},
}
}
```
每一个格式化器均需要指定一个名称,在进行插值替换时会优先依据当前语言来匹配查找格式化器,如果找不到,再到键名为`*`中查找。
```javascript
module.exports = {
"*":{
uppercase:(value)=>value
},
zh:{
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`里面的格式化器则全局有效,在所有场合均可以使用,但是其优先级低于作用域内的同名格式化器。
目前内置的全局格式化器请参阅API参考
## 扩展格式化器
除了可以在当前项目`languages/formatters.js`自定义格式化器和`@voerkai18n/runtime`里面的全局格式化器外,单列了`@voerkai18n/formatters`项目用来包含了更多的格式化器。
作为开源项目,欢迎大家提交贡献更多的格式化器。

View File

@ -0,0 +1,22 @@
# 语言包
当使用`webpack``rollup``esbuild`进行项目打包时,默认语言包采用静态加载,会被打包进行源码中,而其他语言则采用异步打包方式。在`languages/index.js`中。
```javascript
const defaultMessages = require("./zh.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")
})
```
利用异步打包机制,从而避免将多个语言静态打包到源码包。

View File

@ -0,0 +1,14 @@
# 多库联动
`voerkai18n `支持多个库国际化的联动和协作,即**当主程序切换语言时,所有引用依赖库也会跟随主程序进行语言切换**,整个切换过程对所有库开发都是透明的。
![结构图](/images/arch.png)
当我们在开发一个应用或者库并`import "./languages"`时,在`langauges/index.js`进行了如下处理:
- 创建一个`i18nScope`作用域实例
- 检测当前应用环境下是否具有全局单例`VoerkaI18n`
- 如果存在`VoerkaI18n`全局单例,则会将当前`i18nScope`实例注册到`VoerkaI18n.scopes`
- 如果不存在`VoerkaI18n`全局单例,则使用当前`i18nScope`实例的参数来创建一个`VoerkaI18n`全局单例。
- 在每个应用与库中均可以使用`import { t } from ".langauges`导入本工程的`t`翻译函数,该`t`翻译函数被绑定当前`i18nScope`作用域实例,因此翻译时就只会使用到本工程的文本。这样就割离了不同工程和库之间的翻译。
- 由于所有引用的`i18nScope`均注册到了全局单例`VoerkaI18n`,当切换语言时,`VoerkaI18n`会刷新切换所有注册的`i18nScope`,这样就实现了各个`i18nScope`即独立,又可以联动语言切换。

View File

@ -0,0 +1,37 @@
# 运行时
`@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`文件。

View File

@ -0,0 +1,45 @@
# 文本映射
虽然`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`等编译区插件,还是可以正常工作,但是会有一份默认语言的冗余信息存在。

View File

@ -0,0 +1,223 @@
---
title: 快速入门
---
# 快速入门
本节以标准的`Nodejs`应用程序为例,简要介绍`VoerkaI18n`国际化框架的基本使用。其他`vue``react`应用的使用也基本相同。
```shell
myapp
|--package.json
|--index.js
```
在本项目的所有支持的源码文件中均可以使用`t`函数对要翻译的文本进行包装,简单而粗暴。
```javascript
// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))
```
`t`翻译函数是从`myapp/languages/index.js`文件导出的翻译函数,但是现在`myapp/languages`还不存在,后续会使用工具自动生成。`voerkai18n`后续会使用正则表达式对提取要翻译的文本。
## 第一步:安装命令行工具
```shell
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
>pnpm add -g @voerkai18/cli
```
## 第二步:初始化工程
在工程目录中运行`voerkai18n init`命令进行初始化。
```javascript
> voerkai18n init
```
上述命令会在当前工程目录下创建`languages/settings.json`文件。如果您的源代码在`src`子文件夹中,则会创建在`src/languages/settings.json`
`settings.json`内容如下:
```json
{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
```
上述命令代表了:
- 本项目拟支持`中文``英文`两种语言。
- 默认语言是`中文`(即在源代码中直接使用中文)
- 激活语言是`中文`
**注意:**
- `voerkai18n init`是可选的,`voerkai18n extract`也可以实现相同的功能。
- 一般情况下,您可以手工修改`settings.json`,如定义名称空间。
## 第三步:提取文本
接下来我们使用`voerkai18n extract`命令来自动扫描工程源码文件中的需要的翻译的文本信息。
```shell
myapp>voerkai18n extract
```
执行`voerkai18n extract`命令后,就会在`myapp/languages`通过生成`translates/default.json``settings.json`等相关文件。
- **translates/default.json** 该文件就是需要进行翻译的文本信息。
- **settings.json** 语言环境的基本配置信息,可以进行修改。
最后文件结构如下:
```shell
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js
```
**如果略过第一步中的`voerkai18n init`,也可以使用以下命令来为创建和更新`settinbgs.json`**
```javascript
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
```
以上命令代表:
- 扫描当前文件夹下所有源码文件,默认是`js``jsx``html``vue`文件类型。
- 计划支持`zh``en``de``jp`四种语言
- 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
- `-D`代表显示扫描调试信息
## 第四步:翻译文本
接下来就可以分别对`language/translates`文件夹下的所有`JSON`文件进行翻译了。每个`JSON`文件大概如下:
```json
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>"
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>"
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}
```
我们只需要修改该文件翻译对应的语言即可。
**重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下`voerkai18n extract`命令,该命令会进行以下操作:**
- 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
因此,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
大部分国际化解决方案至此就需要交给人工进行翻译了,但是`voerkai18n`除了手动翻译外,通过`voerkai18n translate`命令来实现**调用在线翻译服务**进行自动翻译。
```javascript
>voerkai18n translate --provider baidu --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
```
在项目文件夹下执行上面的语句将会自动调用百度的在线翻译API进行翻译以现在的翻译水平而言您只需要进行少量的微调即可。关于`voerkai18n translate`命令的使用请查阅后续介绍。
## 第五步:编译语言包
当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。
```shell
myapp> voerkai18n compile
```
`compile`命令根据`myapp/languages/translates/*.json``myapp/languages/settings.json`文件编译生成以下文件:
```javascript
|-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- runtime.js // 运行时源码
|-- index.js // 包含该应用作用域下的翻译函数等
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js
```
## 第六步:导入翻译函数
第一步中我们在源文件中直接使用了`t`翻译函数包装要翻译的文本信息,该`t`翻译函数就是在编译环节自动生成并声明在`myapp/languages/index.js`中的。
```javascript
import { t } from "./languages"
```
因此,我们需要在需要进行翻译时导入该函数即可。
但是如果源码文件很多,重次重复导入`t`函数也是比较麻烦的,所以我们也提供了一个`babel/vite`等插件来自动导入`t`函数。
## 第六步:切换语言
当需要切换语言时,可以通过调用`change`方法来切换语言。
```javascript
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例可以直接访问
await VoerkaI18n.change("en")
```
`i18nScope.change``VoerkaI18n.change`两者是等价的。
一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。
```javascript
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on((newLanguage)=>{
...
})
//
VoerkaI18n.on((newLanguage)=>{
...
})
```

View File

@ -0,0 +1,3 @@
---
title: 版本历史
---

View File

@ -0,0 +1,43 @@
---
title: 安装
---
# 安装
`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成:
## **@voerkai18/cli**
包含文本提取/编译等命令行工具,一般应该安装到全局。
```javascript
npm install --g @voerkai18/cli
yarn global add @voerkai18/cli
pnpm add -g @voerkai18/cli
```
## **@voerkai18/runtime**
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装,一般仅在开发库项目时采用独立的运行时依赖。
```javascript
npm install --save @voerkai18/runtime
yarn add @voerkai18/runtime
pnpm add @voerkai18/runtime
```
## **@voerkai18/formatters**
**可选的**,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
## **@voerkai18/babel**
可选的`babel`插件,用来实现自动导入翻译函数和翻译文本映射自动替换。
## **@voerkai18/vue**
可选的`vue`插件用来为Vue应用提供语言动态切换功能。
## **@voerkai18/vite**
可选的`vite`插件,用来为`vite`应用提供自动导入翻译函数和翻译文本映射自动替换。

View File

@ -0,0 +1,3 @@
---
title: 常见问题
---

View File

@ -0,0 +1,38 @@
# 概述
基于`javascript`的国际化方案很多,比较有名的有`fbt``i18next``react-i18next``vue-i18n``react-intl`等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子想造一个轮子,也不想想自己什么水平。
哪么到底是对现有解决方案有什么不满?最主要有三点:
- 大部份均为要翻译的文本信息指定一个`key`,然后在源码文件中使用形如`$t("message.login")`之类的方式,然后在翻译时将之转换成最终的文本信息。此方式最大的问题是,在源码中必须人为地指定每一个`key`,在中文语境中,想为每一句中文均配套想一句符合语义的`英文key`是比较麻烦的,也很不直观不符合直觉。我希望在源文件中就直接使用中文,如`t("中华人民共和国万岁")`,然后国际化框架应该能自动处理后续的一系列麻烦。
- 要能够比较友好地支持多库多包`monorepo`场景下的国际化协作,当主程序切换语言时,其他包或库也可以自动切换,并且在开发上每个包或库均可以独立地进行开发,集成到主程序时能无缝集成。这点在现有方案上没有找到比较理想的解决方案。
- 大部份国际化框架均将中文视为二等公民,大部份情况下您应该采用英文作为第一语言,虽然这不是太大的问题,但是既然要再造一个轮子,为什么不将中文提升到一等公民呢。
基于此就开始造出`VoerkaI18n`这个**全新的国际化多语言解决方案**,主要特性包括:
- 全面工程化解决方案,提供初始化、提取文本、自动翻译、编译等工具链支持。
- 符合直觉,不需要手动定义文本`Key`映射。
- 强大的插值变量`格式化器`机制,可以扩展出强大的多语言特性。
- 支持`babel`插件自动导入`t`翻译函数。
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
- 采用`工具链``运行时`分开设计,发布时只需要集成很小的运行时。
- 高度可扩展的`复数``货币``数字`等常用的多语言处理机制。
- 翻译过程内,提取文本可以自动进行同步,并保留已翻译的内容。
- 可以随时添加支持的语言
- 支持调用在线自动翻译对提取文本进行翻译。

View File

@ -0,0 +1,73 @@
# Babel插件
全局安装`@voerkai18n/babel`插件用来进行自动导入`t`函数和自动文本映射。
## 安装
```javascript
> npm install -g @voerkai18n/babel
> yarn global add @voerkai18n/babel
> pnpm add -g @voerkai18n/babel
```
## 启用插件
使用方法如下:
- 在`babel.config.js`中配置插件
```javascript
const i18nPlugin = require("@voerkai18n/babel")
module.expors = {
plugins: [
[
i18nPlugin,
{
// 可选,指定语言文件存放的目录,即保存编译后的语言文件的文件夹
// 可以指定相对路径,也可以指定绝对路径
// location:"",
autoImport:"#/languages"
}
]
]
}
```
这样,当在进行`babel`转码时,就会自动在`js`源码文件中导入`t`翻译函数。
## 插件参数
插件支持以下参数:
- **location**
配置`langauges`文件夹位置,默认会使用当前文件夹下的`languages`文件。
因此,如果你的`babel.config.js`在项目根文件夹,而`languages`文件夹位于`src/languages`,则可以将`location="src/languages"`,这样插件会自动从该文件夹读取需要的数据。
- **autoImport**
用来配置导入的路径。比如 `autoImport="#/languages" `则当在babel转码时如果插件检测到t函数的存在并没有导入就会自动在该源码中自动导入`import { t } from "#/languages"`
配置`autoImport`时需要注意的是,为了提供一致的导入路径,视所使用的打包工具或转码插件,如`webpack``rollup`等。比如使用`babel-plugin-module-resolver`
```javascript
module.expors = {
plugins: [
[
"module-resolver",
{
root:"./",
alias:{
"languages":"./src/languages"
}
}
]
]
}
```
这样配置`autoImport="languages"`,则自动导入`import { t } from "languages"`
`webpack``rollup`等打包工具也有类似的插件可以实现别名等转换,其目的就是让`@voerkai18n/babel`插件能自动导入固定路径,而不是各种复杂的相对路径。

251
docs/zh/guide/tools/cli.md Normal file
View File

@ -0,0 +1,251 @@
# 命令行工具
`@voerkai18n/cli`命令行工具用来实现工程初始化、扫描提取文本、自动翻译和编译语言等功能。
::: info
建议将`@voerkai18n/cli`命令行工具安装在全局
:::
## 安装
全局安装`@voerkai18n/cli`工具。
```javascript
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18n/cli
```
然后就可以执行:
```javascript
> voerkai18n init
> voerkai18n extract
> voerkai18n compile
```
如果没有全局安装,则需要:
```javascript
> yarn voerkai18n init
> yarn voerkai18n extract
> yarn voerkai18n compile
---
> pnpm voerkai18n init
> pnpm voerkai18n extract
> pnpm voerkai18n compile
```
## 初始化 - init
用于在指定项目创建`voerkai18n`国际化配置文件。
```shell
> voerkai18n init --help
初始化项目国际化配置
Arguments:
location 工程项目所在目录
Options:
-D, --debug 输出调试信息
-r, --reset 重新生成当前项目的语言配置
-lngs, --languages <languages...> 支持的语言列表 (default: ["zh","en"])
-d, --defaultLanguage 默认语言
-a, --activeLanguage 激活语言
-h, --help display help for command
```
**使用方法如下:**
首先需要在工程文件下运行`voerkai18n init`命令对当前工程进行初始化。
```javascript
//- `lngs`参数用来指定拟支持的语言名称列表
> voerkai18n init . -lngs zh en jp de -d zh
```
运行`voerkai18n init`命令后,会在当前工程中创建相应配置文件。
```javascript
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- package.json
|-- index.js
```
`settings.json`文件很简单,主要是用来配置要支持的语言等基本信息。
```javascript
module.exports = {
// 拟支持的语言列表
"languages": [
{
"name": "zh",
"title": "中文"
},
{
"name": "en",
"title": "英文"
}
],
// 默认语言,即准备在源码中写的语言,一般我们可以直接使用中文
"defaultLanguage": "zh",
// 激活语言即默认要启用的语言一般等于defaultLanguage
"activeLanguage": "zh",
// 翻译名称空间定义,详见后续介绍。
"namespaces": {}
}
```
**说明:**
- 您也可以手动自行创建`languages/settings.json`文件。这样就不需运行`voerkai18n init`命令了。
- 如果你的源码放在`src`文件夹,则`init`命令会自动在在`src`文件夹下创建`languages`文件夹。
- `voerkai18n init`是可选的,直接使用`extract`时也会自动创建相应的文件。
- `-m`参数用来指定生成的`settings.json`的模块类型:
- 当`-m=auto`时,会自动读取前工程`package.json`中的`type`字段
- 当`-m=esm`时,会生成`ESM`模块类型的`settings.json`
- 当`-m=cjs`时,会生成`commonjs`模块类型的`settings.json`
- `location`参数是可选的,如果没有指定则采用当前目录。
如果你想将`languages`安装在`src/languages`下,则可以指定`voerkai18n init ./src`
## 提取文本 - extract
扫描提取当前项目中的所有源码,提取出所有需要翻译的文本内容并保存在到`<工程源码目录>/languages/translates/*.json`
```shell
> voerkai18n extract --help
扫描并提取所有待翻译的字符串到<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
```
**说明:**
- 启用`-d`参数时会输出提取过程,显示从哪些文件提取了几条信息。
- 如果已手动创建或通过`init`命令创建了`languages/settings.json`文件,则可以不指定`-ns``-lngs``-d``-a`参数。`extract`会优先使用`languages/settings.json`文件中的参数来进行提取。
- `-u`参数用来指定如何将提取的文本与现存的文件进行合并。因为在国际化流程中,我们经常面临源代码变更时需要更新翻译的问题。支持三种合并策略。
- **sync**:同步(默认值),两者自动合并,并且会删除在源码文件中不存在的文本。如果某个翻译已经翻译了一半也会保留。此值适用于大部情况,推荐。
- **overwrite**:覆盖现存的翻译内容。这会导致已经进行了一半的翻译数据丢失,**慎用**。
- **merge**合并与sync的差别在于不会删除源码中已不存在的文本。
- `-e`参数用来排除扫描的文件夹,多个用逗号分隔。内部采用`gulp.src`来进行文件提取,请参数。如 `-e !libs,core/**/*`。默认会自动排除`node_modules`文件夹
- `-f`参数用来指定要扫描的文件类型,默认`js,jsx,ts,tsx,vue,html`
- `extract`是基于正则表达式方式进行匹配的,而不是像`i18n-next`采用基于`AST`解析。
>**重点:**
>
>默认情况下,`voerkai18n extract`可以安全地反复多次执行,不会导致已经翻译一半的内容丢失。
>
>如果想添加新的语言支持,也`voerkai18n extract`也可以如预期的正常工作。
## 自动翻译 - translate
在工程文件夹下执行`voerkai18n translate`命令,该命令会读取`languages/settings.json`配置文件,并调用在线翻译服务(如百度在线翻译)对提取的文本(`languages/translates/*.json`)进行自动翻译。
```shell
Usage: voerkai18n translate [options] [location]
调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务
Arguments:
location 工程项目所在目录
Options:
-p, --provider <value> 在线翻译服务提供者名称或翻译脚本文件 (default: "baidu")
-m, --max-package-size <value> 将多个文本合并提交的最大包字节数 (default: 3000)
--appkey [key] API密钥
--appid [id] API ID
--no-backup 备份原始文件
--mode 翻译模式取值auto=仅翻译未翻译的,full=全部翻译
-q, --qps <value> 翻译速度限制,即每秒可调用的API次数 (default: 1)
-h, --help 显示帮助
```
- 内置支持调用百度的在线翻译服务,您需要百度的网站上(http://api.fanyi.baidu.com/)申请开通服务,开通后可以得到`appid``appkey`(密钥)。
- `--provider`用来指定在线翻译服务提供者内置支持的是百度在线翻译。也可以传入一个js脚本如下
```javascript
// youdao.js
module.exports = async function(options){
let { appkey,appid } = options
return {
translate:async (texts,from,to){
// texts是一个Array
// from,to代表要从哪一种语言翻译到何种语言
.....
// 在此对texts内容调用在线翻译API
// 翻译结果应该返回与texts对应的数组
// 如果出错则应该throw new Error()
return [...]
}
}
}
```
- `qps`用来指定调用在线翻译API的速度默认是1代表每秒调用一次此参数的引入是考虑到有些翻译平台的免费API有QPS限制。比如百度在线翻译免费版本限制`QPS`就是1即每秒只能调用一次。如果您购买了服务则可以将`QPS`调高。
- 默认情况下,每次运行时均会备份原始的翻译文件至`languages/translates/backup``--no-backup`可以禁止备份。
- 默认情况下,`voerkai18n translate`会在每次运行时跳过已经翻译过的内容,这样可以保留翻译成果。此特性在您对自动翻译的内容进行修改后,再多次运行`voerkai18n translate`命令时均能保留翻译内容,不会导致您修改调整过的内容丢失。`--mode full`参数可以完全覆盖翻译,请慎用。
- 为了提高在线翻译的速度,`voerkai18n translate`并不是一条文本调用一次API而是将多条文本合并起来进行调用但是单次调用也是有数据包大小的限制的`--max-package-size`参数用来指定数据包的最大值。比如百度建议,为保证翻译质量,请将单次请求长度控制在 6000 bytes以内汉字约为输入参数 2000 个)。
- 需要注意的是,自动翻译虽然准确性还不错,真实场景还是需要进行手工调整的,特别是自动翻译一般不能识别插值变量。
## 编译 - compile
编译当前工程的语言包,编译结果输出在.`/langauges`文件夹。
```shell
Usage: voerkai18n compile [options] [location]
编译指定项目的语言包
Arguments:
location 工程项目所在目录 (default: "./")
Options:
-D, --debug 输出调试信息
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "esm")
--no-inline-runtime 不嵌入运行时源码
-h, --help display help for command
```
`voerkai18n compile`执行后会在`langauges`文件夹下输出:
```javascript
myapp
|--- langauges
|-- index.js // 当前作用域的源码
|-- idMap.js // 翻译文本与id的映射文件
|-- formatters.js // 自定义格式化器
|-- zh.js // 中文语言包
|-- en.js // 英文语言包
|-- xx.js // 其他语言包
|-- ...
```
**说明:**
- 在当前工程目录下,一般不需要指定参数就可以反复多次进行编译。
- 您每次修改了源码并`extract`后,均应该再次运行`compile`命令。
- 如果您修改了`formatters.js`,执行`compile`命令不会重新生成和修改该文件。
- `--no-inline-runtime `参数用来指示如何引用运行时。默认会将运行时代码生成保存在`languages/runtime.js`,应用以源码形式引用。当启用`--no-inline-runtime `参数时会采用`require("@voerkai18n/runtime")`的方式。

View File

@ -0,0 +1,97 @@
# Vite插件
`@voerkai18n/babel`插件在`vite`应用中不能正常使用,需要使用`@voerkai18n/vite`插件来完成类似的功能,包括自动文本映射和自动导入`t`函数。
## 安装
`@voerkai18n/vite`只需要作为开发依赖安装即可。
```javascript
npm install --save-dev @voerkai18n/vite
yarn add -D @voerkai18n/vite
pnpm add -D @voerkai18n/vite
```
## 启用插件
接下来在`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()
]
})
```
- ` vite-plugin-inspect`是开发`vite`插件时的调试插件,启用后就可以通过`localhost:3000/__inspect/ `查看Vue源码文件经过插件处理前后的内容一般是Vite插件开发者使用。上例中安装后就可以查看`Voerkai18nPlugin``Vue`文件干了什么事,可以加深理解,**正常使用不需要安装**。
## 插件功能
`@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`文件夹下。
- `autoImport`:可选的,默认`true`,用来配置是否自动导入`t`函数。当vue文件没有指定导入时才会自动导入并且根据当前vue文件的路径处理好导入位置。
- `debug`:可选的,开启后会在控制台输出一些调试信息,对一般用户没有用。
- `patterns`:可选的,一些正则表达式匹配规则,用来过滤匹配哪一些文件需要进行扫描和处理。默认的规则:
```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/**"`的形式来匹配连续路径。
- ``:将``替换为`[^\/]?`,用来匹配单个字符
- `*`:将`*`替换为`[^\/]*`,匹配路径名称

View File

@ -0,0 +1,92 @@
# Vue插件
`vue3`项目中可以安装`@voerkai18n/vue`来实现`枚举语言``语言切换`等功能。
## **安装**
`@voerkai18n/vue`安装为运行时依赖
```javascript
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插件提供
....
}
</script>
```
声明`inject: ['i18n']`后在当前组件实例中就可以访问`this.i18n`,该实例是一个经过`reactive`封闭的响应式对象,其内容是:
```javascript
this.i18n = {
activeLanguage, // 当前激活语言,可以通过直接赋值来切换语言
defaultLanguage, // 默认语言名称
languages // 支持的语言列表=[{name,title},...]
}
```
## 应用示例
注入`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>
```
### 插件参数
`@voerkai18n/vue`插件支持以下参数:
```javascript
import { i18nScope } from './languages'
app.use(i18nPlugin,{
i18nScope, // 重点需要引入当前作用域的i18nScope
forceUpdate:true // 当语言切换时是否强制重新渲染
})
```
- 当`forceUpdate=true`时,`@voerkai18n/vue`插件在切换语言时会调用`app._instance.update()`对整个应用进行强制重新渲染。大部分情况下,切换语言时强制对整个应用进行重新渲染的行为是符合预期的。您也可以能够通过设`forceUpdate=false`来禁用强制重新渲染,此时,界面就不会马上看到语言的切换,需要您自己控制进行重新渲染。

View File

@ -0,0 +1,32 @@
---
title: 切换语言
---
# 切换语言
## 切换语言
可以通过全局单例或当前作用域实例的`change`方法来切换语言。
```javascript
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例可以直接访问
await VoerkaI18n.change("en")
```
## 侦听语言切换事件
```javascript
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on((newLanguage)=>{
...
})
// 直接在全局单例上调用
VoerkaI18n.on((newLanguage)=>{
...
})
```

View File

@ -0,0 +1,5 @@
---
title: 货币
---
# 货币

View File

@ -0,0 +1,19 @@
---
title: 日期时间
---
# 日期时间
`@voerkai18n/runtime`内置了对日期时间进行处理的格式化器,可以直接使用,不需要额外的安装。
```javascript
// 切换到中文
t("现在是{d | date}",new Date()) // == 现在是2022年3月12日
t("现在是{d | time}",new Date()) // == 现在是18点28分12秒
t("现在是{d | shorttime}",new Date()) // == 现在是18:28:12
t("现在是{}",new Date()) // == 现在是2022年3月12日 18点28分12秒
// 切换到英文
t("现在是{d | date}",new Date()) // == Now is 2022/3/12
t("现在是{d | time}",new Date()) // == Now is 18:28:12
t("现在是{}",new Date()) // == Now is 2022/3/20 19:17:24'
```

View File

@ -0,0 +1,84 @@
---
title: 插值变量
---
# 插值变量
`voerkai18n``t`函数支持使用**插值变量**,用来传入一个可变内容。
插值变量有`命名插值变量``位置插值变量`
## **命名插值变量**
可以在t函数中使用`{变量名称}`表示一个命名插值变量。
```javascript
t("我姓名叫{name},我今年{age}岁",{name:"tom",age:12})
// 如果值是函数会自动调用
t("我姓名叫{name},我今年{age}岁",{name:"tom",age:()=>12})
```
仅当`t`函数仅有两个参数且第2个参数是`{}`类型时,启用字典插值变量,翻译时会自动进行插值。
## 位置插值变量
可以在t函数中使用一个空的`{}`表示一个位置插值变量。
```javascript
t("我姓名叫{},我今年{}岁","tom",12)
// 如果值是函数会自动调用
t("我姓名叫{},我今年{}岁","tom",()=>12})
// 如果只有两个参数且第2个参数是一个数组会自动展开
t("我姓名叫{},我今年{}岁",["tom",12])
//如果第2个参数不是{}时就启用位置插值。
t("我姓名叫{name},我今年{age}岁","tom",()=>12)
```
## 插值变量格式化
`voerka-i18n`支持强大的插值变量格式化机制,可以在插值变量中使用`{变量名称 | 格式化器名称 | 格式化器名称(...参数) | ... }`类似管道操作符的语法,将上一个输出作为下一个输入,从而实现对变量值的转换。此机制是`voerka-i18n`实现复数、货币、数字等多语言支持的基础。
我们假设定义以下格式化器(如果定义格式化器,详见后续)来进行示例。
- **UpperCase**:将字符转换为大写
- **division**对数字按每n位一个逗号分割支持一个可选参数分割位数`division(123456)===123,456``division(123456,4)===12,3456`
- **mr** : 自动添加一个先生称呼
```javascript
// My name is TOM
t("My name is { name | UpperCase }",{name:"tom"})
// 我国2021年的GDP是¥14,722,730,697,890
t("我国2021年的GDP是¥{ gdp | division}",{gdp:14722730697890})
// 支持为格式化器提供参数按4位一逗号分割
// 我国2021年的GDP是¥14,7227,3069,7890
t("我国2021年的GDP是¥{ gdp | division(4)}",{gdp:14722730697890})
// 支持连续使用多个格式化器
// My name is Mr.TOM
t("My name is { name | UpperCase | mr }",{name:"tom"})
```
每个格式化器本质上是一个`(value)=>{...}`的函数,并且能**将上一个格式化器的输出作为下一个格式化器的输入**,格式化器具有如下特性:
### **无参数格式化器**
使用无参数格式化器时只需传入名称即可。例如:`My name is { name | UpperCase }`
### **有参数格式化器**
格式化器支持传入参数,如`{ gdp | division(4)}``{ date | format('yyyy/MM/DD')}`
特别需要注意的是,格式化器的参数只能支持简单的类型的参数,如`数字``布尔型``字符串`
**不支持数组、对象和函数参数,也不支持复杂的表达式参数。**
### **连续使用多个格式化器**
就如您预期的一样,**将上一个格式化器的输出作为下一个格式化器的输入**。
`data | f1 | f2 | f3(1)`等效于` f3(f2(f1(data)),1)`

View File

@ -0,0 +1,47 @@
# 名称空间
`voerkai18n `的名称空间是为了解决当源码文件非常多时,通过名称空间对翻译内容进行分类翻译的。
假设一个大型项目,其中源代码文件有上千个。默认情况下,`voerkai18n extract`会扫描所有源码文件将需要翻译的文本提取到`languages/translates/default.json`文件中。由于文件太多会导致以下问题:
- 内容太多导致`default.json`文件太大,有利于管理
- 有些翻译往往需要联系上下文才可以作出更准确的翻译,没有适当分类,不容易联系上下文。
因此,引入`名称空间`就是目的就是为了解决此问题。
配置名称空间,需要配置`languages/settings.json`文件。
```javascript
// 工程目录d:/code/myapp
// languages/settings.json
module.exports = {
namespaces:{
//"名称":"相对路径"
“routes”:“routes”,
"auth":"core/auth",
"admin":"views/admin"
}
}
```
以上例子代表:
- 将`d:\code\myapp\routes`中扫描到的文本提取到`routes.json`中。
- 将`d:\code\myapp\auth`中扫描到的文本提取到`auth.json`中。
- 将`d:\code\myapp\views/admin`中扫描到的文本提取到`admin.json`中。
最终在` languages/translates`中会包括:
```shell
languages
|-- translates
|-- default.json
|-- routes.sjon
|-- auth.json
|-- admin.json
```
然后,`voerkai18n compile`在编译时会自动合并这些文件,后续就不再需要名称空间的概念了。
`名称空间`仅仅是为了解决当翻译内容太多时的分类问题。

112
docs/zh/guide/use/plural.md Normal file
View File

@ -0,0 +1,112 @@
---
title: 复数
---
# 复数
当翻译文本内容是一个`数组`时启用复数处理机制。即在`langauges/tranclates/*.json`中的文本翻译项是一个数组。
## 启用复数处理机制
假设在`index.html`文件中具有一个翻译内容
```javascript
t("我{}一辆车")
```
经过`extract`命令提取为翻译文件后,如下:
```json
// languages/translates/default.json
{
"我有{}辆车":{
"en":"",
"de":"...."
}
}
```
现在我们要求引入复数处理机制,为不同数量采用不同的翻译,只需要将上述翻译文本更改为数组形式。
```json
{
"我有{}辆车":{
"en":["I don't have car","I have a car","I have two cars","I have {} cars"],
"en":["I don't have car","I have a car","I have {} cars"],
"en":["I don't have car","I have {} cars"],
"de":"...."
}
}
```
上例中,只需要在翻译文件中将上述的`en:""`更改为`[<0对应的复数文本>,<1对应的复数文本>,...,<n对应的复数文本>]`形式代表启动复数机制.
- 可以灵活地为每一个数字(`0、1、2、...、n`)对应的复数形式进行翻译
- 数量数字大于数组长度,则总是取最后一个复数形式
- 复数形式的文本同样支持位置插值和变量插值。
## 对应的翻译函数
启用复数处理机制后,在`t`函数根据变量值来决定采用单数还是复数,按如下规则进行处理。
- **不存在插值变量且t函数的第2个参数是数字**
```javascript
t("我有一辆车",0) // == "I don't have a car"
t("我有一辆车",1) // == "I have a car"
t("我有一辆车",2) // == "I have two cars"
t("我有一辆车",100) // == "I have 100 cars"
```
- **存在插值变量且t函数的第2个参数是数字**
就中文而言,上述没有指定插值变量是比较别扭的,一般可以引入一个位置插值变量更加友好。
```javascript
t("我有{}辆车",0) // == "I don't have a car"
t("我有{}辆车",1) // == "I have a car"
t("我有{}辆车",2) // == "I have two cars"
t("我有{}辆车",100) // == "I have 100 cars"
```
- **复数命名插值变量**
当启用复数功能时,`t`函数需要知道根据哪个变量来决定采用何种复数形式。
**当采用位置变量插值时,`t`函数取第一个数字类型参数作为位置插值复数。**
```javascript
t("{}有{}辆车","张三",0)
```
**当采用命名变量插值时,`t`函数约定当插值字典中存在以`$字符开头`的变量时,并且值是`数字`时,根据该变量来引用复数。**
下例中,`t`函数根据`$count`值来处理复数。
```javascript
t("{name}有{$count}辆车",{name:"张三",$count:1})
```
## **示例**
```javascript
// languages/translates/default.json
{
"第{}章":{
en:[
"Chapter Zero","Chapter One", "Chapter Two", "Chapter Three","Chapter Four",
"Chapter Five","Chapter Six","Chapter Seven","Chapter Eight","Chapter Nine",
"Chapter {}"
],
zh:["起始","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章",“第{}章”]
}
}
// 翻译函数
t("第{}章",0) // == Chapter Zero
t("第{}章",1) // == Chapter One
t("第{}章",2) // == Chapter Two
t("第{}章",3) // == Chapter Three
t("第{}章",4) // == Chapter Four
t("第{}章",5) // == Chapter Five
t("第{}章",6) // == Chapter Six
t("第{}章",7) // == Chapter Seven
...
// 超过取最后一项
t("第{}章",100) // == Chapter 100
```

View File

@ -0,0 +1 @@
# React应用

39
docs/zh/guide/use/t.md Normal file
View File

@ -0,0 +1,39 @@
---
title: 翻译函数
---
# 翻译函数
默认提供翻译函数`t`用来进行翻译。一般情况下,`t`函数声明在执行`voerkai18n compile`命令生成在工程目录下的`languages/index.js`文件中。
```javascript
// 从当前语言包文件夹index.js中导入翻译函数
import { t } from "<myapp>/languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
// 当仅有两个参数且第2个参数是[]类型时,自动展开第一个参数进行位置插值
t("中华人民共和国成立于{year}年,首都{capital}",[1949,"北京"])
// 当仅有两个参数且第2个参数是{}类型时,启用字典插值变量
t("中华人民共和国成立于{year}年,首都{capital}",{year:1949,capital:"北京"})
// 插值变量可以是同步函数,在进行插值时自动调用。
t("中华人民共和国成立于{year}年,首都{capital}",()=>1949,"北京")
// 对插值变量启用格式化器
t("中华人民共和国成立于{birthday | year}年",{birthday:new Date()})
```
**注意:**
- `voerkai18n`使用正则表达式来提取要翻译的内容,因此`t("")`可以使用在任意地方。
-

57
docs/zh/guide/use/vue.md Normal file
View File

@ -0,0 +1,57 @@
# 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`的使用请参阅后续说明。

61
docs/zh/home.md Normal file
View File

@ -0,0 +1,61 @@
---
home: true
icon: home
title: 主页
heroImage: /logo.svg
heroText: VoerkaI18n
tagline: 适用于Nodejs/Vue/React的国际化解决方案
actions:
- text: 快速入门
link: /guide/intro/get-started
- text: 源码
link: /
type: secondary
features:
- title: 工程化支持
icon: markdown
details: 从文本提取/自动翻译/编译/动态切换的全流程工程化支持,适用于大型项目
link:
- title: 集成自动翻译
icon: slides
details: 调用在线翻译服务API支持对提取的文本进行自动翻译大幅度提高工程效率
link:
- title: 符合直觉
icon: layout
details: 在源码中直接使用符合直觉的翻译形式不需要绞尽脑汁想种种key
link:
- title: 自动提取文本
icon: comment
details: 提供扫描提取工具对源码文件中需要翻译的文本进行提取
link:
- title: 适用性
icon: info
details: 支持任意Javascript应用包括Nodejs/Vue/React/ReactNative等。
link:
- title: 多库协作
icon: blog
details: 支持monorepo工程下多库进行语言切换的联动机制
link:
- title: 自动扩展工具
icon: palette
details: 提供Vue/React/Babel等扩展插件简化各种应用下
link:
- title: 扩展特性
icon: contrast
details: 强大的插值变量机制,能扩展支持复数、日期、货币等灵活强大的多语言机制
link:
copyright: true
footer: MIT Licensed | Copyright © 2022-present wxzhang
---

View File

@ -0,0 +1,3 @@
# 格式化器

View File

@ -0,0 +1,33 @@
# i18nScope
每个工程会创建一个`i18nScope`实例。
```javascript
import { i18nScope } from "./languages"
// 订阅语言切换事件
i18nScope.on((newLanguage)=>{...})
// 取消语言切换事件订阅
i18nScope.off(callback)
// 当前作用域配置
i18nScope.settings
// 当前语言
i18nScope.activeLanguage // 如zh
// 默认语言
i18nScope.defaultLanguage
// 返回当前支持的语言列表,可以用来显示
i18nScope.languages // [{name:"zh",title:"中文"},{name:"en",title:"英文"},...]
// 返回当前作用域的格式化器
i18nScope.formatters
// 当前作用id
i18nScope.id
// 切换语言,异步函数
await i18nScope.change(newLanguage)
// 当前语言包
i18nScope.messages // {1:"...",2:"...","3":"..."}
// 引用全局VoerkaI18n实例
i18nScope.global
// 注册当前作用域格式化器
i18nScope.registerFormatter(name,formatter,{language:"*"})
```

View File

@ -0,0 +1,3 @@
# 语言代码
请参阅[这里](https://fanyi-api.baidu.com/doc/21)

View File

@ -0,0 +1,8 @@
# 参考
- 作用域实例
- 全局VoerkaI18n实例
- 格式化器
- 可用的语言代码

View File

@ -0,0 +1,26 @@
# VoerkaI18n
`import {} form "./languages"`时会自动创建全局单`VoerkaI18n`
```javascript
// 订阅语言切换事件
VoerkaI18n.on((newLanguage)=>{...})
// 取消语言切换事件订阅
VoerkaI18n.off(callback)
// 取消所有语言切换事件订阅
VoerkaI18n.offAll()
// 返回当前默认语言
VoerkaI18n.defaultLanguage
// 返回当前激活语言
VoerkaI18n.activeLanguage
// 返回当前支持的语言
VoerkaI18n.languages
// 切换语言
await VoerkaI18n.change(newLanguage)
// 返回全局格式化器
VoerkaI18n.formatters
// 注册全局格式化器
VoerkaI18n.registerFormatter(name,formatter,{language:"*"})
```

View File

@ -313,9 +313,6 @@ let inlineFormatters = formatters; // 内置格式化器
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
// 有效的语言名称列表
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"];
/**
* 考虑到通过正则表达式进行插件的替换可能较慢因此提供一个简单方法来过滤掉那些
* 不需要进行插值处理的字符串
@ -931,7 +928,6 @@ var runtime ={
replaceInterpolatedVars,
I18nManager,
translate,
languages,
i18nScope,
defaultLanguageSettings,
getDataTypeName,

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/cli",
"version": "1.0.26",
"version": "1.0.27",
"description": "VoerkaI18n command line interactive tools",
"main": "index.js",
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
@ -50,5 +50,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-04-10T20:14:07+08:00"
"lastPublish": "2022-04-11T17:25:13+08:00"
}

2
packages/runtime/dist/index.cjs vendored Normal file

File diff suppressed because one or more lines are too long

1
packages/runtime/dist/index.cjs.map vendored Normal file

File diff suppressed because one or more lines are too long

2
packages/runtime/dist/index.esm.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

938
packages/runtime/dist/runtime.cjs vendored Normal file
View File

@ -0,0 +1,938 @@
'use strict';
/**
* 判断是否是JSON对象
* @param {*} obj
* @returns
*/
function isPlainObject$1(obj){
if (typeof obj !== 'object' || obj === null) return false;
var proto = Object.getPrototypeOf(obj);
if (proto === null) return true;
var baseProto = proto;
while (Object.getPrototypeOf(baseProto) !== null) {
baseProto = Object.getPrototypeOf(baseProto);
}
return proto === baseProto;
}
function isNumber$1(value){
return !isNaN(parseInt(value))
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge$1(toObj,formObj,options={}){
let results = Object.assign({},toObj);
Object.entries(formObj).forEach(([key,value])=>{
if(key in results){
if(typeof value === "object" && value !== null){
if(Array.isArray(value)){
if(options.array === 0){
results[key] = value;
}else if(options.array === 1){
results[key] = [...results[key],...value];
}else if(options.array === 2){
results[key] = [...new Set([...results[key],...value])];
}
}else {
results[key] = deepMerge$1(results[key],value,options);
}
}else {
results[key] = value;
}
}else {
results[key] = value;
}
});
return results
}
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName$1(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
}
var utils ={
isPlainObject: isPlainObject$1,
isNumber: isNumber$1,
deepMerge: deepMerge$1,
getDataTypeName: getDataTypeName$1
};
/**
*
* 简单的事件触发器
*
*/
var eventemitter = class EventEmitter{
constructor(){
this._callbacks = [];
}
on(callback){
if(this._callbacks.includes(callback)) return
this._callbacks.push(callback);
}
off(callback){
for(let i=0;i<this._callbacks.length;i++){
if(this._callbacks[i]===callback ){
this._callbacks.splice(i,1);
}
}
}
offAll(){
this._callbacks = [];
}
async emit(...args){
if(Promise.allSettled){
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
}else {
await Promise.all(this._callbacks.map(cb=>cb(...args)));
}
}
};
var scope = class i18nScope {
constructor(options={},callback){
// 每个作用域都有一个唯一的id
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
this._languages = options.languages; // 当前作用域的语言列表
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
this._activeLanguage = options.activeLanguage; // 当前语言名称
this._default = options.default; // 默认语言包
this._messages = options.messages; // 当前语言包
this._idMap = options.idMap; // 消息id映射列表
this._formatters = options.formatters; // 当前作用域的格式化函数列表
this._loaders = options.loaders; // 异步加载语言文件的函数列表
this._global = null; // 引用全局VoerkaI18n配置注册后自动引用
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
this.$cache={
activeLanguage : null,
typedFormatters: {},
formatters : {},
};
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域
// 则使用当前作用域来初始化全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
const { I18nManager } = runtime;
globalThis.VoerkaI18n = new I18nManager({
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
languages: options.languages,
});
}
this.global = globalThis.VoerkaI18n;
// 正在加载语言包标识
this._loading=false;
// 在全局注册作用域
this.register(callback);
}
// 作用域
get id(){return this._id}
// 默认语言名称
get defaultLanguage(){return this._defaultLanguage}
// 默认语言名称
get activeLanguage(){return this._activeLanguage}
// 默认语言包
get default(){return this._default}
// 当前语言包
get messages(){return this._messages}
// 消息id映射列表
get idMap(){return this._idMap}
// 当前作用域的格式化函数列表
get formatters(){return this._formatters}
// 异步加载语言文件的函数列表
get loaders(){return this._loaders}
// 引用全局VoerkaI18n配置注册后自动引用
get global(){return this._global}
set global(value){this._global = value;}
/**
* 在全局注册作用域
* @param {*} callback 当注册
*/
register(callback){
if(!typeof(callback)==="function") callback = ()=>{};
this.global.register(this).then(callback).catch(callback);
}
registerFormatter(name,formatter,{language="*"}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
if(DataTypes.includes(name)){
this.formatters[language].$types[name] = formatter;
}else {
this.formatters[language][name] = formatter;
}
}
/**
* 回退到默认语言
*/
_fallback(){
this._messages = this._default;
this._activeLanguage = this.defaultLanguage;
}
/**
* 刷新当前语言包
* @param {*} newLanguage
*/
async refresh(newLanguage){
this._loading = Promise.resolve();
if(!newLanguage) newLanguage = this.activeLanguage;
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
if(newLanguage === this.defaultLanguage){
this._messages = this._default;
return
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
const loader = this.loaders[newLanguage];
if(typeof(loader) === "function"){
try{
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
}catch(e){
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
}
}else {
this._fallback();
}
}
// 以下方法引用全局VoerkaI18n实例的方法
get on(){return this.global.on.bind(this.global)}
get off(){return this.global.off.bind(this.global)}
get offAll(){return this.global.offAll.bind(this.global)}
get change(){
return this.global.change.bind(this.global)
}
};
/**
* 内置的格式化器
*
*/
/**
* 字典格式化器
* 根据输入data的值返回后续参数匹配的结果
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
*
*
* dict(1,1,"one",2,"two",3,"three"4,"four") == "one"
* dict(2,1,"one",2,"two",3,"three"4,"four") == "two"
* dict(3,1,"one",2,"two",3,"three"4,"four") == "three"
* dict(4,1,"one",2,"two",3,"three"4,"four") == "four"
* // 无匹配时返回原始值
* dict(5,1,"one",2,"two",3,"three"4,"four") == 5
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
* dict(5,1,"one",2,"two",3,"three"4,"four","more") == "more"
*
* 在翻译中使用
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
*
* @param {*} value
* @param {...any} args
* @returns
*/
function dict(value,...args){
for(let i=0;i<args.length;i+=2){
if(args[i]===value){
return args[i+1]
}
}
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
return value
}
var formatters = {
"*":{
$types:{
Date:(value)=>value.toLocaleString()
},
time:(value)=> value.toLocaleTimeString(),
shorttime:(value)=> value.toLocaleTimeString(),
date: (value)=> value.toLocaleDateString(),
dict, //字典格式化器
},
zh:{
$types:{
Date:(value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}${value.getHours()}${value.getMinutes()}${value.getSeconds()}`
},
shortime:(value)=> value.toLocaleTimeString(),
time:(value)=>`${value.getHours()}${value.getMinutes()}${value.getSeconds()}`,
date: (value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}`,
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
currency:(value)=>`${value}`,
},
en:{
currency:(value)=>{
return `$${value}`
}
}
};
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils;
const EventEmitter = eventemitter;
const i18nScope = scope;
let inlineFormatters = formatters; // 内置格式化器
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
/**
* 考虑到通过正则表达式进行插件的替换可能较慢因此提供一个简单方法来过滤掉那些
* 不需要进行插值处理的字符串
* 原理很简单就是判断一下是否同时具有{}字符如果有则认为可能有插值变量如果没有则一定没有插件变量则就不需要进行正则匹配
* 从而可以减少不要的正则匹配
* 注意该方法只能快速判断一个字符串不包括插值变量
* @param {*} str
* @returns {boolean} true=可能包含插值变量,
*/
function hasInterpolation(str){
return str.includes("{") && str.includes("}")
}
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
formatters="| aaa(1,1) | bbb "
需要统一解析为
[
[aaa,[1,1]], // [formatter'name,[args,...]]
[bbb,[]],
]
formatters="| aaa(1,1,"dddd") | bbb "
目前对参数采用简单的split(",")来解析因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
@returns [[<formatterName>,[<arg>,<arg>,...]]]
*/
function parseFormatters(formatters){
if(!formatters) return []
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
// 2. 解析格式化器参数
return result.map(formatter=>{
let firstIndex = formatter.indexOf("(");
let lastIndex = formatter.lastIndexOf(")");
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
arg = arg.trim();
if(!isNaN(parseInt(arg))){
return parseInt(arg) // 数字
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
return arg.substr(1,arg.length-2) // 字符串
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
return arg.toLowerCase()==="true" // 布尔值
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
try{
return JSON.parse(arg)
}catch(e){
return String(arg)
}
}else {
return String(arg)
}
});
return [formatter.substr(0,firstIndex),args]
}else {// 不带参数的格式化器
return [formatter,[]]
}
})
}
/**
* 提取字符串中的插值变量
* // [
// {
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]],<匹配字符串>],
// ....
//
* @param {*} str
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
* @returns {Array}
* [
* {
* name:"<变量名称>",
* formatters:[
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* ],
* match:"<匹配字符串>"
* },
* ...
* ]
*/
function getInterpolatedVars(str){
let vars = [];
forEachInterpolatedVars(str,(varName,formatters,match)=>{
let varItem = {
name:varName,
formatters:formatters.map(([formatter,args])=>{
return {
name:formatter,
args:args
}
}),
match:match
};
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
vars.push(varItem);
}
return ""
});
return vars
}
/**
* 遍历str中的所有插值变量传递给callback将callback返回的结果替换到str中对应的位置
* @param {*} str
* @param {Function(<变量名称>,[formatters],match[0])} callback
* @returns 返回替换后的字符串
*/
function forEachInterpolatedVars(str,callback,options={}){
let result=str, match;
let opts = Object.assign({
replaceAll:true, // 是否替换所有插值变量当使用命名插值时应置为true当使用位置插值时应置为false
},options);
varWithPipeRegexp.lastIndex=0;
while ((match = varWithPipeRegexp.exec(result)) !== null) {
const varname = match.groups.varname || "";
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters);
if(typeof(callback)==="function"){
try{
if(opts.replaceAll){
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
}else {
result=result.replace(match[0],callback(varname,formatters,match[0]));
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
}
}
varWithPipeRegexp.lastIndex=0;
}
return result
}
function resetScopeCache(scope,activeLanguage=null){
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
}
/**
* 取得指定数据类型的默认格式化器
*
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时
* 会自动调用该格式化器来对值进行格式化转换
const formatters = {
"*":{
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
}, // 在所有语言下生效的格式化器
zh:{
$types:{
[数据类型]:(value)=>{...},
},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
},
}
* @param {*} scope
* @param {*} activeLanguage
* @param {*} dataType 数字类型
* @returns {Function} 格式化函数
*/
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
if(!scope.$cache) resetScopeCache(scope);
if(scope.$cache.activeLanguage === activeLanguage) {
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
}else {// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage);
}
// 先在当前作用域中查找,再在全局查找
const targets = [scope.formatters,scope.global.formatters];
for(const target of targets){
if(!target) continue
// 优先在当前语言的$types中查找
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
let formatters = target[activeLanguage].$types;
if(dataType in formatters && typeof(formatters[dataType])==="function"){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
// 在所有语言的$types中查找
if(("*" in target) && isPlainObject(target["*"].$types)){
let formatters = target["*"].$types;
if(dataType in formatters && typeof(formatters[dataType])==="function"){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
}
}
/**
* 获取指定名称的格式化器函数
* @param {*} scope
* @param {*} activeLanguage
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
function getFormatter(scope,activeLanguage,name){
// 缓存格式化器引用,避免重复检索
if(!scope.$cache) resetScopeCache(scope);
if(scope.$cache.activeLanguage === activeLanguage) {
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
}else {// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage);
}
// 先在当前作用域中查找,再在全局查找
const targets = [scope.formatters,scope.global.formatters];
for(const target of targets){
// 优先在当前语言查找
if(activeLanguage in target){
let formatters = target[activeLanguage] || {};
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
}
// 在所有语言的$types中查找
let formatters = target["*"] || {};
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
}
}
/**
* 执行格式化器并返回结果
* @param {*} value
* @param {*} formatters 多个格式化器顺序执行前一个输出作为下一个格式化器的输入
*/
function executeFormatter(value,formatters){
if(formatters.length===0) return value
let result = value;
try{
for(let formatter of formatters){
if(typeof(formatter) === "function") {
result = formatter(result);
}else {// 如果碰到无效的格式化器,则跳过过续的格式化器
return result
}
}
}catch(e){
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
}
return result
}
/**
* [[格式化器名称,[参数,参数,...]][格式化器名称,[参数,参数,...]]]格式化器转化为
*
*
*
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
*/
function buildFormatters(scope,activeLanguage,formatters){
let results = [];
for(let formatter of formatters){
if(formatter[0]){
const func = getFormatter(scope,activeLanguage,formatter[0]);
if(typeof(func)==="function"){
results.push((v)=>{
return func(v,...formatter[1])
});
}else {
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
results.push((v)=>{
if(typeof(v[formatter[0]])==="function"){
return v[formatter[0]].call(v,...formatter[1])
}else {
return v
}
});
}
}
}
return results
}
/**
* 将value经过格式化器处理后返回
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @param {*} value
* @returns
*/
function getFormattedValue(scope,activeLanguage,formatters,value){
// 1. 取得格式化器函数列表
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
// 2. 查找每种数据类型默认格式化器,并添加到formatters最前面默认数据类型格式化器优先级最高
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
if(defaultFormatter){
formatterFuncs.splice(0,0,defaultFormatter);
}
// 3. 执行格式化器
value = executeFormatter(value,formatterFuncs);
return value
}
/**
* 字符串可以进行变量插值替换
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
*
- 当只有两个参数并且第2个参数是{}将第2个参数视为命名变量的字典
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
- 当只有两个参数并且第2个参数是[]将第2个参数视为位置参数
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
- 普通位置参数替换
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
-
this == scope == { formatters: {}, ... }
* @param {*} template
* @returns
*/
function replaceInterpolatedVars(template,...args) {
const scope = this;
// 当前激活语言
const activeLanguage = scope.global.activeLanguage;
// 没有变量插值则的返回原字符串
if(args.length===0 || !hasInterpolation(template)) return template
// ****************************变量插值****************************
if(args.length===1 && isPlainObject(args[0])){
// 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0];
return forEachInterpolatedVars(template,(varname,formatters)=>{
let value = (varname in varValues) ? varValues[varname] : '';
return getFormattedValue(scope,activeLanguage,formatters,value)
})
}else {
// ****************************位置插值****************************
// 如果只有一个Array参数则认为是位置变量列表进行展开
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
let i = 0;
return forEachInterpolatedVars(template,(varname,formatters)=>{
if(params.length>i){
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
}else {
throw new Error() // 抛出异常,停止插值处理
}
},{replaceAll:false})
}
}
// 默认语言配置
const defaultLanguageSettings = {
defaultLanguage: "zh",
activeLanguage: "zh",
languages:[
{name:"zh",title:"中文",default:true},
{name:"en",title:"英文"}
],
formatters:inlineFormatters
};
function isMessageId(content){
return parseInt(content)>0
}
/**
* 根据值的单数和复数形式从messages中取得相应的消息
*
* @param {*} messages 复数形式的文本内容 = [<=0时的内容><=1时的内容><=2时的内容>,...]
* @param {*} value
*/
function getPluraMessage(messages,value){
try{
if(Array.isArray(messages)){
return messages.length > value ? messages[value] : messages[messages.length-1]
}else {
return messages
}
}catch{
return Array.isArray(messages) ? messages[0] : messages
}
}
function escape(str){
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
.replaceAll("\t","\\t")
.replaceAll("\n","\\n")
.replaceAll("\b","\\b")
.replaceAll("\r","\\r")
.replaceAll("\f","\\f")
.replaceAll("\'","\\'")
.replaceAll('\"','\\"')
.replaceAll('\v','\\v')
}
function unescape(str){
return str
.replaceAll("\\t","\t")
.replaceAll("\\n","\n")
.replaceAll("\\b","\b")
.replaceAll("\\r","\r")
.replaceAll("\\f","\f")
.replaceAll("\\'","\'")
.replaceAll('\\"','\"')
.replaceAll('\\v','\v')
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
}
/**
* 翻译函数
*
* translate("要翻译的文本内容") 如果默认语言是中文则不会进行翻译直接返回
* translate("I am {} {}","man") == I am man 位置插值
* translate("I am {p}",{p:"man"}) 字典插值
* translate("total {$count} items", {$count:1}) //复数形式
* translate("total {} {} {} items",a,b,c) // 位置变量插值
*
* this===scope 当前绑定的scope
*
*/
function translate(message) {
const scope = this;
const activeLanguage = scope.global.activeLanguage;
let content = message;
let vars=[]; // 插值变量列表
let pluralVars= []; // 复数变量
let pluraValue = null; // 复数值
if(!typeof(message)==="string") return message
try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){
Object.entries(arguments[1]).forEach(([name,value])=>{
if(typeof(value)==="function"){
try{
vars[name] = value();
}catch(e){
vars[name] = value;
}
}
// 以$开头的视为复数变量
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name);
});
vars = [arguments[1]];
}else if(arguments.length >= 2){
vars = [...arguments].splice(1).map((arg,index)=>{
try{
arg = typeof(arg)==="function" ? arg() : arg;
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg);
}catch(e){ }
return arg
});
}
// 3. 取得翻译文本模板字符串
if(activeLanguage === scope.defaultLanguage){
// 2.1 从默认语言中取得翻译文本模板字符串
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
if(isMessageId(content)){
content = scope.default[content] || message;
}
}else {
// 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
content = scope.messages[msgId] || content;
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
}
// 2. 处理复数
// 经过上面的处理content可能是字符串或者数组
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
if(Array.isArray(content) && content.length>0){
// 如果存在复数命名变量,只取第一个复数变量
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
content = getPluraMessage(content,pluraValue);
}else if(pluralVar.length>0){
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
}else { // 如果找不到复数变量,则使用第一个内容
content = content[0];
}
}
// 进行插值处理
if(vars.length==0){
return content
}else {
return replaceInterpolatedVars.call(scope,content,...vars)
}
}catch(e){
return content // 出错则返回原始文本
}
}
/**
* 多语言管理类
*
* 当导入编译后的多语言文件时(import("./languages"))会自动生成全局实例VoerkaI18n
*
* VoerkaI18n.languages // 返回支持的语言列表
* VoerkaI18n.defaultLanguage // 默认语言
* VoerkaI18n.language // 当前语言
* VoerkaI18n.change(language) // 切换到新的语言
*
*
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
* VoerkaI18n.off("change",(language)=>{})
*
* */
class I18nManager extends EventEmitter{
constructor(settings={}){
super();
if(I18nManager.instance!=null){
return I18nManager.instance;
}
I18nManager.instance = this;
this._settings = deepMerge(defaultLanguageSettings,settings);
this._scopes=[];
return I18nManager.instance;
}
get settings(){ return this._settings }
get scopes(){ return this._scopes }
// 当前激活语言
get activeLanguage(){ return this._settings.activeLanguage}
// 默认语言
get defaultLanguage(){ return this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 内置格式化器
get formatters(){ return inlineFormatters }
/**
* 切换语言
*/
async change(value){
value=value.trim();
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
// 通知所有作用域刷新到对应的语言包
await this._refreshScopes(value);
this._settings.activeLanguage = value;
/// 触发语言切换事件
await this.emit(value);
}else {
throw new Error("Not supported language:"+value)
}
}
/**
* 当切换语言时调用此方法来加载更新语言包
* @param {*} newLanguage
*/
async _refreshScopes(newLanguage){
// 并发执行所有作用域语言包的加载
try{
const scopeRefreshers = this._scopes.map(scope=>{
return scope.refresh(newLanguage)
});
if(Promise.allSettled){
await Promise.allSettled(scopeRefreshers);
}else {
await Promise.all(scopeRefreshers);
}
}catch(e){
console.warn("Error while refreshing i18n scopes:",e.message);
}
}
/**
*
* 注册一个新的作用域
*
* 每一个库均对应一个作用域每个作用域可以有多个语言包且对应一个翻译函数
* 除了默认语言外其他语言采用动态加载的方式
*
* @param {*} scope
*/
async register(scope){
if(!(scope instanceof i18nScope)){
throw new TypeError("Scope must be an instance of I18nScope")
}
this._scopes.push(scope);
await scope.refresh(this.activeLanguage);
}
/**
* 注册全局格式化器
* 格式化器是一个简单的同步函数value=>{...}用来对输入进行格式化后返回结果
*
* registerFormatters(name,value=>{...}) // 适用于所有语言
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
* @param {*} formatters
*/
registerFormatter(name,formatter,{language="*"}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
if(DataTypes$1.includes(name)){
this.formatters[language].$types[name] = formatter;
}else {
this.formatters[language][name] = formatter;
}
}
}
var runtime ={
getInterpolatedVars,
replaceInterpolatedVars,
I18nManager,
translate,
i18nScope,
defaultLanguageSettings,
getDataTypeName,
isNumber,
isPlainObject
};
module.exports = runtime;

936
packages/runtime/dist/runtime.mjs vendored Normal file
View File

@ -0,0 +1,936 @@
/**
* 判断是否是JSON对象
* @param {*} obj
* @returns
*/
function isPlainObject$1(obj){
if (typeof obj !== 'object' || obj === null) return false;
var proto = Object.getPrototypeOf(obj);
if (proto === null) return true;
var baseProto = proto;
while (Object.getPrototypeOf(baseProto) !== null) {
baseProto = Object.getPrototypeOf(baseProto);
}
return proto === baseProto;
}
function isNumber$1(value){
return !isNaN(parseInt(value))
}
/**
* 简单进行对象合并
*
* options={
* array:0 , // 数组合并策略0-替换1-合并2-去重合并
* }
*
* @param {*} toObj
* @param {*} formObj
* @returns 合并后的对象
*/
function deepMerge$1(toObj,formObj,options={}){
let results = Object.assign({},toObj);
Object.entries(formObj).forEach(([key,value])=>{
if(key in results){
if(typeof value === "object" && value !== null){
if(Array.isArray(value)){
if(options.array === 0){
results[key] = value;
}else if(options.array === 1){
results[key] = [...results[key],...value];
}else if(options.array === 2){
results[key] = [...new Set([...results[key],...value])];
}
}else {
results[key] = deepMerge$1(results[key],value,options);
}
}else {
results[key] = value;
}
}else {
results[key] = value;
}
});
return results
}
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName$1(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
}
var utils ={
isPlainObject: isPlainObject$1,
isNumber: isNumber$1,
deepMerge: deepMerge$1,
getDataTypeName: getDataTypeName$1
};
/**
*
* 简单的事件触发器
*
*/
var eventemitter = class EventEmitter{
constructor(){
this._callbacks = [];
}
on(callback){
if(this._callbacks.includes(callback)) return
this._callbacks.push(callback);
}
off(callback){
for(let i=0;i<this._callbacks.length;i++){
if(this._callbacks[i]===callback ){
this._callbacks.splice(i,1);
}
}
}
offAll(){
this._callbacks = [];
}
async emit(...args){
if(Promise.allSettled){
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
}else {
await Promise.all(this._callbacks.map(cb=>cb(...args)));
}
}
};
var scope = class i18nScope {
constructor(options={},callback){
// 每个作用域都有一个唯一的id
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
this._languages = options.languages; // 当前作用域的语言列表
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
this._activeLanguage = options.activeLanguage; // 当前语言名称
this._default = options.default; // 默认语言包
this._messages = options.messages; // 当前语言包
this._idMap = options.idMap; // 消息id映射列表
this._formatters = options.formatters; // 当前作用域的格式化函数列表
this._loaders = options.loaders; // 异步加载语言文件的函数列表
this._global = null; // 引用全局VoerkaI18n配置注册后自动引用
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
this.$cache={
activeLanguage : null,
typedFormatters: {},
formatters : {},
};
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域
// 则使用当前作用域来初始化全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
const { I18nManager } = runtime;
globalThis.VoerkaI18n = new I18nManager({
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
languages: options.languages,
});
}
this.global = globalThis.VoerkaI18n;
// 正在加载语言包标识
this._loading=false;
// 在全局注册作用域
this.register(callback);
}
// 作用域
get id(){return this._id}
// 默认语言名称
get defaultLanguage(){return this._defaultLanguage}
// 默认语言名称
get activeLanguage(){return this._activeLanguage}
// 默认语言包
get default(){return this._default}
// 当前语言包
get messages(){return this._messages}
// 消息id映射列表
get idMap(){return this._idMap}
// 当前作用域的格式化函数列表
get formatters(){return this._formatters}
// 异步加载语言文件的函数列表
get loaders(){return this._loaders}
// 引用全局VoerkaI18n配置注册后自动引用
get global(){return this._global}
set global(value){this._global = value;}
/**
* 在全局注册作用域
* @param {*} callback 当注册
*/
register(callback){
if(!typeof(callback)==="function") callback = ()=>{};
this.global.register(this).then(callback).catch(callback);
}
registerFormatter(name,formatter,{language="*"}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
if(DataTypes.includes(name)){
this.formatters[language].$types[name] = formatter;
}else {
this.formatters[language][name] = formatter;
}
}
/**
* 回退到默认语言
*/
_fallback(){
this._messages = this._default;
this._activeLanguage = this.defaultLanguage;
}
/**
* 刷新当前语言包
* @param {*} newLanguage
*/
async refresh(newLanguage){
this._loading = Promise.resolve();
if(!newLanguage) newLanguage = this.activeLanguage;
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
if(newLanguage === this.defaultLanguage){
this._messages = this._default;
return
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
const loader = this.loaders[newLanguage];
if(typeof(loader) === "function"){
try{
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
}catch(e){
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
}
}else {
this._fallback();
}
}
// 以下方法引用全局VoerkaI18n实例的方法
get on(){return this.global.on.bind(this.global)}
get off(){return this.global.off.bind(this.global)}
get offAll(){return this.global.offAll.bind(this.global)}
get change(){
return this.global.change.bind(this.global)
}
};
/**
* 内置的格式化器
*
*/
/**
* 字典格式化器
* 根据输入data的值返回后续参数匹配的结果
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
*
*
* dict(1,1,"one",2,"two",3,"three"4,"four") == "one"
* dict(2,1,"one",2,"two",3,"three"4,"four") == "two"
* dict(3,1,"one",2,"two",3,"three"4,"four") == "three"
* dict(4,1,"one",2,"two",3,"three"4,"four") == "four"
* // 无匹配时返回原始值
* dict(5,1,"one",2,"two",3,"three"4,"four") == 5
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
* dict(5,1,"one",2,"two",3,"three"4,"four","more") == "more"
*
* 在翻译中使用
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
*
* @param {*} value
* @param {...any} args
* @returns
*/
function dict(value,...args){
for(let i=0;i<args.length;i+=2){
if(args[i]===value){
return args[i+1]
}
}
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
return value
}
var formatters = {
"*":{
$types:{
Date:(value)=>value.toLocaleString()
},
time:(value)=> value.toLocaleTimeString(),
shorttime:(value)=> value.toLocaleTimeString(),
date: (value)=> value.toLocaleDateString(),
dict, //字典格式化器
},
zh:{
$types:{
Date:(value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}${value.getHours()}${value.getMinutes()}${value.getSeconds()}`
},
shortime:(value)=> value.toLocaleTimeString(),
time:(value)=>`${value.getHours()}${value.getMinutes()}${value.getSeconds()}`,
date: (value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}`,
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
currency:(value)=>`${value}`,
},
en:{
currency:(value)=>{
return `$${value}`
}
}
};
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils;
const EventEmitter = eventemitter;
const i18nScope = scope;
let inlineFormatters = formatters; // 内置格式化器
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
/**
* 考虑到通过正则表达式进行插件的替换可能较慢因此提供一个简单方法来过滤掉那些
* 不需要进行插值处理的字符串
* 原理很简单就是判断一下是否同时具有{}字符如果有则认为可能有插值变量如果没有则一定没有插件变量则就不需要进行正则匹配
* 从而可以减少不要的正则匹配
* 注意该方法只能快速判断一个字符串不包括插值变量
* @param {*} str
* @returns {boolean} true=可能包含插值变量,
*/
function hasInterpolation(str){
return str.includes("{") && str.includes("}")
}
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
formatters="| aaa(1,1) | bbb "
需要统一解析为
[
[aaa,[1,1]], // [formatter'name,[args,...]]
[bbb,[]],
]
formatters="| aaa(1,1,"dddd") | bbb "
目前对参数采用简单的split(",")来解析因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
@returns [[<formatterName>,[<arg>,<arg>,...]]]
*/
function parseFormatters(formatters){
if(!formatters) return []
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
// 2. 解析格式化器参数
return result.map(formatter=>{
let firstIndex = formatter.indexOf("(");
let lastIndex = formatter.lastIndexOf(")");
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
arg = arg.trim();
if(!isNaN(parseInt(arg))){
return parseInt(arg) // 数字
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
return arg.substr(1,arg.length-2) // 字符串
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
return arg.toLowerCase()==="true" // 布尔值
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
try{
return JSON.parse(arg)
}catch(e){
return String(arg)
}
}else {
return String(arg)
}
});
return [formatter.substr(0,firstIndex),args]
}else {// 不带参数的格式化器
return [formatter,[]]
}
})
}
/**
* 提取字符串中的插值变量
* // [
// {
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]],<匹配字符串>],
// ....
//
* @param {*} str
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
* @returns {Array}
* [
* {
* name:"<变量名称>",
* formatters:[
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* ],
* match:"<匹配字符串>"
* },
* ...
* ]
*/
function getInterpolatedVars(str){
let vars = [];
forEachInterpolatedVars(str,(varName,formatters,match)=>{
let varItem = {
name:varName,
formatters:formatters.map(([formatter,args])=>{
return {
name:formatter,
args:args
}
}),
match:match
};
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
vars.push(varItem);
}
return ""
});
return vars
}
/**
* 遍历str中的所有插值变量传递给callback将callback返回的结果替换到str中对应的位置
* @param {*} str
* @param {Function(<变量名称>,[formatters],match[0])} callback
* @returns 返回替换后的字符串
*/
function forEachInterpolatedVars(str,callback,options={}){
let result=str, match;
let opts = Object.assign({
replaceAll:true, // 是否替换所有插值变量当使用命名插值时应置为true当使用位置插值时应置为false
},options);
varWithPipeRegexp.lastIndex=0;
while ((match = varWithPipeRegexp.exec(result)) !== null) {
const varname = match.groups.varname || "";
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters);
if(typeof(callback)==="function"){
try{
if(opts.replaceAll){
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
}else {
result=result.replace(match[0],callback(varname,formatters,match[0]));
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
}
}
varWithPipeRegexp.lastIndex=0;
}
return result
}
function resetScopeCache(scope,activeLanguage=null){
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
}
/**
* 取得指定数据类型的默认格式化器
*
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时
* 会自动调用该格式化器来对值进行格式化转换
const formatters = {
"*":{
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
}, // 在所有语言下生效的格式化器
zh:{
$types:{
[数据类型]:(value)=>{...},
},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
},
}
* @param {*} scope
* @param {*} activeLanguage
* @param {*} dataType 数字类型
* @returns {Function} 格式化函数
*/
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
if(!scope.$cache) resetScopeCache(scope);
if(scope.$cache.activeLanguage === activeLanguage) {
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
}else {// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage);
}
// 先在当前作用域中查找,再在全局查找
const targets = [scope.formatters,scope.global.formatters];
for(const target of targets){
if(!target) continue
// 优先在当前语言的$types中查找
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
let formatters = target[activeLanguage].$types;
if(dataType in formatters && typeof(formatters[dataType])==="function"){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
// 在所有语言的$types中查找
if(("*" in target) && isPlainObject(target["*"].$types)){
let formatters = target["*"].$types;
if(dataType in formatters && typeof(formatters[dataType])==="function"){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
}
}
/**
* 获取指定名称的格式化器函数
* @param {*} scope
* @param {*} activeLanguage
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
function getFormatter(scope,activeLanguage,name){
// 缓存格式化器引用,避免重复检索
if(!scope.$cache) resetScopeCache(scope);
if(scope.$cache.activeLanguage === activeLanguage) {
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
}else {// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage);
}
// 先在当前作用域中查找,再在全局查找
const targets = [scope.formatters,scope.global.formatters];
for(const target of targets){
// 优先在当前语言查找
if(activeLanguage in target){
let formatters = target[activeLanguage] || {};
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
}
// 在所有语言的$types中查找
let formatters = target["*"] || {};
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
}
}
/**
* 执行格式化器并返回结果
* @param {*} value
* @param {*} formatters 多个格式化器顺序执行前一个输出作为下一个格式化器的输入
*/
function executeFormatter(value,formatters){
if(formatters.length===0) return value
let result = value;
try{
for(let formatter of formatters){
if(typeof(formatter) === "function") {
result = formatter(result);
}else {// 如果碰到无效的格式化器,则跳过过续的格式化器
return result
}
}
}catch(e){
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
}
return result
}
/**
* [[格式化器名称,[参数,参数,...]][格式化器名称,[参数,参数,...]]]格式化器转化为
*
*
*
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
*/
function buildFormatters(scope,activeLanguage,formatters){
let results = [];
for(let formatter of formatters){
if(formatter[0]){
const func = getFormatter(scope,activeLanguage,formatter[0]);
if(typeof(func)==="function"){
results.push((v)=>{
return func(v,...formatter[1])
});
}else {
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
results.push((v)=>{
if(typeof(v[formatter[0]])==="function"){
return v[formatter[0]].call(v,...formatter[1])
}else {
return v
}
});
}
}
}
return results
}
/**
* 将value经过格式化器处理后返回
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @param {*} value
* @returns
*/
function getFormattedValue(scope,activeLanguage,formatters,value){
// 1. 取得格式化器函数列表
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
// 2. 查找每种数据类型默认格式化器,并添加到formatters最前面默认数据类型格式化器优先级最高
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
if(defaultFormatter){
formatterFuncs.splice(0,0,defaultFormatter);
}
// 3. 执行格式化器
value = executeFormatter(value,formatterFuncs);
return value
}
/**
* 字符串可以进行变量插值替换
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
*
- 当只有两个参数并且第2个参数是{}将第2个参数视为命名变量的字典
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
- 当只有两个参数并且第2个参数是[]将第2个参数视为位置参数
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
- 普通位置参数替换
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
-
this == scope == { formatters: {}, ... }
* @param {*} template
* @returns
*/
function replaceInterpolatedVars(template,...args) {
const scope = this;
// 当前激活语言
const activeLanguage = scope.global.activeLanguage;
// 没有变量插值则的返回原字符串
if(args.length===0 || !hasInterpolation(template)) return template
// ****************************变量插值****************************
if(args.length===1 && isPlainObject(args[0])){
// 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0];
return forEachInterpolatedVars(template,(varname,formatters)=>{
let value = (varname in varValues) ? varValues[varname] : '';
return getFormattedValue(scope,activeLanguage,formatters,value)
})
}else {
// ****************************位置插值****************************
// 如果只有一个Array参数则认为是位置变量列表进行展开
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
let i = 0;
return forEachInterpolatedVars(template,(varname,formatters)=>{
if(params.length>i){
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
}else {
throw new Error() // 抛出异常,停止插值处理
}
},{replaceAll:false})
}
}
// 默认语言配置
const defaultLanguageSettings = {
defaultLanguage: "zh",
activeLanguage: "zh",
languages:[
{name:"zh",title:"中文",default:true},
{name:"en",title:"英文"}
],
formatters:inlineFormatters
};
function isMessageId(content){
return parseInt(content)>0
}
/**
* 根据值的单数和复数形式从messages中取得相应的消息
*
* @param {*} messages 复数形式的文本内容 = [<=0时的内容><=1时的内容><=2时的内容>,...]
* @param {*} value
*/
function getPluraMessage(messages,value){
try{
if(Array.isArray(messages)){
return messages.length > value ? messages[value] : messages[messages.length-1]
}else {
return messages
}
}catch{
return Array.isArray(messages) ? messages[0] : messages
}
}
function escape(str){
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
.replaceAll("\t","\\t")
.replaceAll("\n","\\n")
.replaceAll("\b","\\b")
.replaceAll("\r","\\r")
.replaceAll("\f","\\f")
.replaceAll("\'","\\'")
.replaceAll('\"','\\"')
.replaceAll('\v','\\v')
}
function unescape(str){
return str
.replaceAll("\\t","\t")
.replaceAll("\\n","\n")
.replaceAll("\\b","\b")
.replaceAll("\\r","\r")
.replaceAll("\\f","\f")
.replaceAll("\\'","\'")
.replaceAll('\\"','\"')
.replaceAll('\\v','\v')
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
}
/**
* 翻译函数
*
* translate("要翻译的文本内容") 如果默认语言是中文则不会进行翻译直接返回
* translate("I am {} {}","man") == I am man 位置插值
* translate("I am {p}",{p:"man"}) 字典插值
* translate("total {$count} items", {$count:1}) //复数形式
* translate("total {} {} {} items",a,b,c) // 位置变量插值
*
* this===scope 当前绑定的scope
*
*/
function translate(message) {
const scope = this;
const activeLanguage = scope.global.activeLanguage;
let content = message;
let vars=[]; // 插值变量列表
let pluralVars= []; // 复数变量
let pluraValue = null; // 复数值
if(!typeof(message)==="string") return message
try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){
Object.entries(arguments[1]).forEach(([name,value])=>{
if(typeof(value)==="function"){
try{
vars[name] = value();
}catch(e){
vars[name] = value;
}
}
// 以$开头的视为复数变量
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name);
});
vars = [arguments[1]];
}else if(arguments.length >= 2){
vars = [...arguments].splice(1).map((arg,index)=>{
try{
arg = typeof(arg)==="function" ? arg() : arg;
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg);
}catch(e){ }
return arg
});
}
// 3. 取得翻译文本模板字符串
if(activeLanguage === scope.defaultLanguage){
// 2.1 从默认语言中取得翻译文本模板字符串
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
if(isMessageId(content)){
content = scope.default[content] || message;
}
}else {
// 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
content = scope.messages[msgId] || content;
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
}
// 2. 处理复数
// 经过上面的处理content可能是字符串或者数组
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
if(Array.isArray(content) && content.length>0){
// 如果存在复数命名变量,只取第一个复数变量
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
content = getPluraMessage(content,pluraValue);
}else if(pluralVar.length>0){
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
}else { // 如果找不到复数变量,则使用第一个内容
content = content[0];
}
}
// 进行插值处理
if(vars.length==0){
return content
}else {
return replaceInterpolatedVars.call(scope,content,...vars)
}
}catch(e){
return content // 出错则返回原始文本
}
}
/**
* 多语言管理类
*
* 当导入编译后的多语言文件时(import("./languages"))会自动生成全局实例VoerkaI18n
*
* VoerkaI18n.languages // 返回支持的语言列表
* VoerkaI18n.defaultLanguage // 默认语言
* VoerkaI18n.language // 当前语言
* VoerkaI18n.change(language) // 切换到新的语言
*
*
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
* VoerkaI18n.off("change",(language)=>{})
*
* */
class I18nManager extends EventEmitter{
constructor(settings={}){
super();
if(I18nManager.instance!=null){
return I18nManager.instance;
}
I18nManager.instance = this;
this._settings = deepMerge(defaultLanguageSettings,settings);
this._scopes=[];
return I18nManager.instance;
}
get settings(){ return this._settings }
get scopes(){ return this._scopes }
// 当前激活语言
get activeLanguage(){ return this._settings.activeLanguage}
// 默认语言
get defaultLanguage(){ return this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 内置格式化器
get formatters(){ return inlineFormatters }
/**
* 切换语言
*/
async change(value){
value=value.trim();
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
// 通知所有作用域刷新到对应的语言包
await this._refreshScopes(value);
this._settings.activeLanguage = value;
/// 触发语言切换事件
await this.emit(value);
}else {
throw new Error("Not supported language:"+value)
}
}
/**
* 当切换语言时调用此方法来加载更新语言包
* @param {*} newLanguage
*/
async _refreshScopes(newLanguage){
// 并发执行所有作用域语言包的加载
try{
const scopeRefreshers = this._scopes.map(scope=>{
return scope.refresh(newLanguage)
});
if(Promise.allSettled){
await Promise.allSettled(scopeRefreshers);
}else {
await Promise.all(scopeRefreshers);
}
}catch(e){
console.warn("Error while refreshing i18n scopes:",e.message);
}
}
/**
*
* 注册一个新的作用域
*
* 每一个库均对应一个作用域每个作用域可以有多个语言包且对应一个翻译函数
* 除了默认语言外其他语言采用动态加载的方式
*
* @param {*} scope
*/
async register(scope){
if(!(scope instanceof i18nScope)){
throw new TypeError("Scope must be an instance of I18nScope")
}
this._scopes.push(scope);
await scope.refresh(this.activeLanguage);
}
/**
* 注册全局格式化器
* 格式化器是一个简单的同步函数value=>{...}用来对输入进行格式化后返回结果
*
* registerFormatters(name,value=>{...}) // 适用于所有语言
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
* @param {*} formatters
*/
registerFormatter(name,formatter,{language="*"}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
if(DataTypes$1.includes(name)){
this.formatters[language].$types[name] = formatter;
}else {
this.formatters[language][name] = formatter;
}
}
}
var runtime ={
getInterpolatedVars,
replaceInterpolatedVars,
I18nManager,
translate,
i18nScope,
defaultLanguageSettings,
getDataTypeName,
isNumber,
isPlainObject
};
export { runtime as default };

View File

@ -655,7 +655,7 @@ module.exports ={
getInterpolatedVars,
replaceInterpolatedVars,
I18nManager,
translate
translate,
i18nScope,
defaultLanguageSettings,
getDataTypeName,

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/runtime",
"version": "1.0.16",
"version": "1.0.17",
"description": "Voerkai18n Runtime",
"main": "./dist/index.cjs",
"module": "dist/index.esm.js",
@ -35,5 +35,5 @@
"rollup-plugin-terser": "^7.0.2",
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-04-07T19:14:01+08:00"
"lastPublish": "2022-04-11T17:25:01+08:00"
}

1617
readme.md

File diff suppressed because it is too large Load Diff

8281
yarn.lock Normal file

File diff suppressed because it is too large Load Diff