Repository: IFreeOvO/i18n-cli
Branch: main
Commit: a4c84f2fe8c3
Files: 99
Total size: 123.7 KB
Directory structure:
gitextract_3o6dmje6/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── issue-close.yml
│ ├── issue-inactive.yml
│ ├── issue-remove-inactive.yml
│ └── release.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── commitlint.config.js
├── examples/
│ ├── react-demo/
│ │ ├── .editorconfig
│ │ ├── .gitignore
│ │ ├── .prettierignore
│ │ ├── .prettierrc
│ │ ├── .umirc.ts
│ │ ├── README.md
│ │ ├── i18n.config.js
│ │ ├── mock/
│ │ │ └── .gitkeep
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── pages/
│ │ │ │ └── index.tsx
│ │ │ └── utils/
│ │ │ └── i18n.ts
│ │ ├── tsconfig.json
│ │ └── typings.d.ts
│ └── vue-demo/
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── components/
│ │ │ └── HelloWorld.vue
│ │ ├── main.ts
│ │ ├── style.css
│ │ ├── utils/
│ │ │ └── i18n.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── package.json
├── packages/
│ ├── i18n-extract-cli/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── index.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ └── transformJs.test.ts
│ │ │ ├── collector.ts
│ │ │ ├── commands/
│ │ │ │ ├── init/
│ │ │ │ │ └── index.ts
│ │ │ │ └── loadExcel/
│ │ │ │ └── index.ts
│ │ │ ├── core.ts
│ │ │ ├── default.config.ts
│ │ │ ├── exportExcel.ts
│ │ │ ├── index.ts
│ │ │ ├── parse.ts
│ │ │ ├── transform.ts
│ │ │ ├── transformJs.ts
│ │ │ ├── transformVue.ts
│ │ │ ├── translate.ts
│ │ │ └── utils/
│ │ │ ├── assertType.ts
│ │ │ ├── constants.ts
│ │ │ ├── error-logger.ts
│ │ │ ├── escapeQuotes.ts
│ │ │ ├── excelUtil.ts
│ │ │ ├── flatObjectDeep.ts
│ │ │ ├── getAbsolutePath.ts
│ │ │ ├── getLang.ts
│ │ │ ├── getLocaleDir.ts
│ │ │ ├── includeChinese.ts
│ │ │ ├── initConfig.ts
│ │ │ ├── isDirectory.ts
│ │ │ ├── log.ts
│ │ │ ├── removeLineBreaksInTag.ts
│ │ │ ├── saveLocaleFile.ts
│ │ │ ├── serializeCode.ts
│ │ │ ├── spreadObject.ts
│ │ │ └── stateManager.ts
│ │ ├── tsconfig.json
│ │ └── types/
│ │ └── index.d.ts
│ └── translate-utils/
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── index.d.ts
│ ├── package.json
│ ├── src/
│ │ ├── alicloud.ts
│ │ ├── baidu.ts
│ │ ├── google.ts
│ │ ├── index.ts
│ │ └── youdao.ts
│ ├── tsconfig.cjs.json
│ └── tsconfig.esm.json
├── patches/
│ └── mustache@4.2.0.patch
├── pnpm-workspace.yaml
├── tsconfig.base.json
└── turbo.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
node_modules
dist
examples
================================================
FILE: .eslintrc.js
================================================
module.exports = {
env: {
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-var-requires': 0,
'@typescript-eslint/no-explicit-any': 'off',
},
}
================================================
FILE: .gitattributes
================================================
.husky/* linguist-vendored
================================================
FILE: .github/workflows/issue-close.yml
================================================
name: Issue Close
on:
schedule:
# GMT+8 03:00
- cron: '0 19 * * *'
jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- name: needs more info
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: 'needs more info'
inactive-day: 3
body: |
Since the issue was labeled with `needs-more-info`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
================================================
FILE: .github/workflows/issue-inactive.yml
================================================
name: Issue Inactive
on:
schedule:
# GMT+8 03:00
- cron: '0 19 * * *'
jobs:
check-inactive:
runs-on: ubuntu-latest
steps:
- name: check-inactive
uses: actions-cool/issues-helper@v3
with:
actions: 'check-inactive'
inactive-day: 14
================================================
FILE: .github/workflows/issue-remove-inactive.yml
================================================
name: Issue Remove Inactive
on:
issues:
types: [edited]
issue_comment:
types: [created, edited]
jobs:
issue-remove-inactive:
runs-on: ubuntu-latest
steps:
- name: remove inactive
if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login
uses: actions-cool/issues-helper@v3
with:
actions: 'remove-labels'
issue-number: ${{ github.event.issue.number }}
labels: 'inactive, needs more info, need reproduction'
================================================
FILE: .github/workflows/release.yml
================================================
name: deployment
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
deployment:
runs-on: ubuntu-latest
environment: release
steps:
- name: 拉取代码
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
persist-credentials: true
fetch-depth: 0
- name: 安装pnpm
uses: pnpm/action-setup@v4
- name: 安装node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 发布npm
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm version-packages
commit: 'chore: publish packages'
env:
NPM_CONFIG_PROVENANCE: true
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
================================================
FILE: .gitignore
================================================
node_modules/
.DS_Store
*.log
dist
.turbo
.cache
.vscode
tsconfig.tsbuildinfo
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged
================================================
FILE: .npmrc
================================================
registry=https://registry.npmjs.org/
engine-strict=true
strict-peer-dependencies=false
================================================
FILE: .nvmrc
================================================
v18
================================================
FILE: .prettierignore
================================================
node_modules
dist
================================================
FILE: .prettierrc
================================================
{
"printWidth": 100,
"semi": false,
"singleQuote": true
}
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2022-present IFreeOvO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================

[](https://github.com/IFreeOvO/i18n-cli/actions/workflows/release.yml)
# 介绍
该项目是一个支持将中文替换成 i18n 国际化标记,并支持自动翻译的命令行工具
## 流程设计
见[掘金文章](https://juejin.cn/post/7174082242426175525)
## 功能
- [x] 支持.mjs.cjs.js.ts.jsx.tsx.vue 后缀文件提取中文
- [x] 支持 vue2.0,vue3.0,react 提取中文
- [x] 支持通过/\*i18n-ignore\*/注释,忽略中文提取
- [x] 支持将提取的中文以 key-value 形式存入\*.json 语言包里
- [x] 支持 prettier 格式化代码
- [x] 支持将中文语言包自动翻译成其他语言
- [x] 支持将翻译结果导出成 excel
- [x] 支持读取 excel 文件并转换成语言包
- [x] 自定义语言包 key 的层级嵌套
- [x] 自定义语言包的 key
- [x] 自定义 i18n 工具的调用对象
- [x] 自定义 i18n 工具的方法名
- [x] 自定义 i18n 第三方包的导入
- [x] 自定义忽略提取的方法
## 安装
```
npm i @ifreeovo/i18n-extract-cli -g
```
## 使用文档
[点击这里](https://github.com/IFreeOvO/i18n-cli/tree/master/packages/i18n-extract-cli)
## 转换效果示例
#### react 转换示例
转换前
```jsx
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState('你好')
return (
<div>
<p title="标题">{msg + '呵呵'}</p>
<button onClick={() => setMsg(msg + '啊')}>点击</button>
</div>
)
}
export default Example
```
转换后
```jsx
import { t } from 'i18n'
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState(t('你好'))
return (
<div>
<p title={t('标题')}>{msg + t('呵呵')}</p>
<button onClick={() => setMsg(msg + t('啊'))}>{t('点击')}</button>
</div>
)
}
export default Example
```
#### vue 转换示例
转换前
```vue
<template>
<div :label="'标签'" :title="1 + '标题'">
<p title="测试注释">内容</p>
<button @click="handleClick('信息')">点击</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('点了')
},
},
}
</script>
```
转换后
```vue
<template>
<div :label="$t('标签')" :title="1 + $t('标题')">
<p :title="$t('测试注释')">{{ $t('内容') }}</p>
<button @click="handleClick($t('信息'))">{{ $t('点击') }}</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log(this.$t('点了'))
},
},
}
</script>
```
## 开源许可证
[MIT](./LICENSE)
================================================
FILE: commitlint.config.js
================================================
/** @type {import('cz-git').UserConfig} */
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'release',
],
],
},
prompt: {
types: [
{ value: 'feat', name: 'feat: A new feature', emoji: ':sparkles:' },
{ value: 'fix', name: 'fix: A bug fix', emoji: ':bug:' },
{ value: 'docs', name: 'docs: Documentation only changes', emoji: ':memo:' },
{
value: 'style',
name: 'style: Changes that do not affect the meaning of the code',
emoji: ':lipstick:',
},
{
value: 'refactor',
name: 'refactor: A code change that neither fixes a bug nor adds a feature',
emoji: ':recycle:',
},
{ value: 'perf', name: 'perf: A code change that improves performance', emoji: ':zap:' },
{
value: 'test',
name: 'test: Adding missing tests or correcting existing tests',
emoji: ':white_check_mark:',
},
{
value: 'build',
name: 'build: Changes that affect the build system or external dependencies',
emoji: ':package:',
},
{
value: 'ci',
name: 'ci: Changes to our CI configuration files and scripts',
emoji: ':ferris_wheel:',
},
{
value: 'chore',
name: "chore: Other changes that don't modify src or test files",
emoji: ':hammer:',
},
{ value: 'revert', name: 'revert: Reverts a previous commit', emoji: ':rewind:' },
{ value: 'release', name: 'release: publish packages', emoji: ':tada:' },
],
// 跳过要询问的步骤
skipQuestions: ['body'],
},
}
================================================
FILE: examples/react-demo/.editorconfig
================================================
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
================================================
FILE: examples/react-demo/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
/dist
# misc
.DS_Store
# umi
/src/.umi
/src/.umi-production
/src/.umi-test
/.env.local
================================================
FILE: examples/react-demo/.prettierignore
================================================
**/*.md
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test
================================================
FILE: examples/react-demo/.prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}
================================================
FILE: examples/react-demo/.umirc.ts
================================================
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: [{ path: '/', component: '@/pages/index' }],
fastRefresh: {},
locale: {},
mfsu: {},
});
================================================
FILE: examples/react-demo/README.md
================================================
# 介绍
react项目模板,用于测试翻译效果
## 安装
装项目依赖
```
yarn i
```
装命令行工具
```
yarn i -g @ovointl/i18n-extract-cli
```
## 运行项目
在当前目录(react-demo)下执行
```
yarn start
```
## 体验@ifreeovo/i18n-extract-cli转换效果
在当前目录(react-demo)下执行(有道翻译配置可填appKey: '2d8e89a6fd072117', appSecret: 'HiX7rGmYRad3ISMLYexRLfpkJi2taMPh')
```
it -c ./i18n.config.js
```
================================================
FILE: examples/react-demo/i18n.config.js
================================================
module.exports = {
localePath: './src/locales/zh-CN.json',
rules: {
tsx: {
importDeclaration: 'import { t } from "@/utils/i18n"',
},
},
};
================================================
FILE: examples/react-demo/mock/.gitkeep
================================================
================================================
FILE: examples/react-demo/package.json
================================================
{
"private": true,
"scripts": {
"start": "umi dev",
"build": "umi build",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@ant-design/pro-layout": "^6.5.0",
"react": "17.x",
"react-dom": "17.x",
"umi": "^3.5.35"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.5.35",
"lint-staged": "^10.0.7",
"prettier": "^2.2.0",
"typescript": "^4.1.2",
"yorkie": "^2.0.0"
}
}
================================================
FILE: examples/react-demo/src/pages/index.tsx
================================================
import { setLocale, getLocale } from 'umi';
import { useState } from 'react';
export default function HomePage() {
let [count, setCount] = useState(1);
const toggleLocale = () => {
const lang = getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN';
setLocale(lang, false);
};
const add = () => {
setCount(count + 1);
};
const title = '计数器';
return (
<div>
<button title="哈哈" onClick={toggleLocale}>
切换语言
</button>
<h2>{title}</h2>
<p>{'数字' + count}</p>
<button onClick={add}>点击数字加1</button>
</div>
);
}
================================================
FILE: examples/react-demo/src/utils/i18n.ts
================================================
import { useIntl } from 'umi';
export function t(key: string, params: Record<string, string> = {}) {
const intl = useIntl();
return intl.formatMessage(
{
id: key,
},
params,
);
}
================================================
FILE: examples/react-demo/tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"importHelpers": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"sourceMap": true,
"baseUrl": "./",
"strict": true,
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"]
},
"allowSyntheticDefaultImports": true
},
"include": [
"mock/**/*",
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts"
],
"exclude": [
"node_modules",
"lib",
"es",
"dist",
"typings",
"**/__test__",
"test",
"docs",
"tests"
]
}
================================================
FILE: examples/react-demo/typings.d.ts
================================================
declare module '*.css';
declare module '*.less';
declare module '*.png';
declare module '*.svg' {
export function ReactComponent(
props: React.SVGProps<SVGSVGElement>,
): React.ReactElement;
const url: string;
export default url;
}
================================================
FILE: examples/vue-demo/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: examples/vue-demo/README.md
================================================
# 介绍
vue 项目模板,用于测试翻译效果
## 安装
装项目依赖
```
npm i
```
装命令行工具
```
npm i -g @ovointl/i18n-extract-cli
```
## 运行项目
在当前目录(vue-demo)下执行
```
npm start
```
## 体验@ifreeovo/i18n-extract-cli 转换效果
在当前目录(vue-demo)下执行(有道翻译配置可填 appKey: '2d8e89a6fd072117', appSecret: 'HiX7rGmYRad3ISMLYexRLfpkJi2taMPh')
```
it
```
================================================
FILE: examples/vue-demo/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
FILE: examples/vue-demo/package.json
================================================
{
"name": "vue-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.41",
"vue-i18n": "^9.3.0-beta.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.2.0",
"typescript": "^4.6.4",
"vite": "^3.2.3",
"vue-tsc": "^1.0.9"
}
}
================================================
FILE: examples/vue-demo/src/App.vue
================================================
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="传入的内容" />
</template>
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
================================================
FILE: examples/vue-demo/src/components/HelloWorld.vue
================================================
<template>
<h1>{{ msg + '---组合' }}</h1>
<div class="card">
<button type="button" @click="count++">数量为 {{ count }}</button>
<p :title="'标题'">
测试项目
</p>
<button type="button" @click="toggleLocale">点击切换语言</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import i18n from '../utils/i18n'
defineProps<{ msg: string }>()
const count = ref(0)
const toggleLocale = () => {
const locale: string = localStorage.getItem('locale') || ''
i18n.locale = locale === 'zh-CN'? 'en-US': 'zh-CN'
localStorage.setItem('locale', i18n.locale)
location.reload()
}
</script>
<style scoped>
.read-the-docs {
color: #888;
}
</style>
================================================
FILE: examples/vue-demo/src/main.ts
================================================
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import i18n from './utils/i18n'
createApp(App).use(i18n).mount('#app')
================================================
FILE: examples/vue-demo/src/style.css
================================================
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
================================================
FILE: examples/vue-demo/src/utils/i18n.ts
================================================
import { createI18n } from 'vue-i18n'
import zh from '../../locales/zh-CN.json'
import en from '../../locales/en-US.json'
const i18n: any = createI18n({
locale: localStorage.getItem('locale') || 'zh-CN',
// 默认语言
messages: {
'zh-CN': zh,
// 中文
'en-US': en, // 英文
},
})
export default i18n
================================================
FILE: examples/vue-demo/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/vue-demo/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
================================================
FILE: examples/vue-demo/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/vue-demo/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})
================================================
FILE: package.json
================================================
{
"private": true,
"version": "1.0.0",
"description": "提取项目里的中文,并替换为i18n函数",
"scripts": {
"preinstall": "only-allow pnpm",
"prepare": "husky install",
"cz": "turbo run test && pnpm changeset-add && cz",
"dev": "turbo run dev --parallel",
"build": "turbo run build",
"changeset-add": "changeset add",
"version-packages": "changeset version && pnpm install --no-frozen-lockfile",
"release": "pnpm build && pnpm changeset publish"
},
"author": "IFreeOvO",
"license": "MIT",
"packageManager": "pnpm@9.15.9",
"engines": {
"node": ">=18",
"pnpm": ">=9"
},
"devDependencies": {
"@changesets/cli": "^2.27.5",
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@types/node": "^18.11.6",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"commitizen": "^4.2.5",
"cz-git": "^1.3.12",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"only-allow": "^1.1.1",
"prettier": "^2.7.1",
"turbo": "^1.6.3",
"typescript": "^4.8.4"
},
"lint-staged": {
"*.{md,json}": [
"prettier --write"
],
"*.ts": [
"eslint --fix",
"prettier --parser=typescript --write"
]
},
"config": {
"commitizen": {
"path": "cz-git"
}
},
"pnpm": {
"patchedDependencies": {
"mustache@4.2.0": "patches/mustache@4.2.0.patch"
}
}
}
================================================
FILE: packages/i18n-extract-cli/CHANGELOG.md
================================================
# @ifreeovo/i18n-extract-cli
## 4.3.4
### Patch Changes
- 7e331d5: 修复解析时会将 html 错误塞入 key
## 4.3.3
### Patch Changes
- 99ba209: fix: 修复 Vue 指令属性重复包裹的问题,以及 keyMap 值保留原始换行符和防止重复插入 import 语句
## 4.3.2
### Patch Changes
- 19f80ae: 1. 配置 input 无法遍历目标文件 2. 依赖报错
- Updated dependencies [19f80ae]
- @ifreeovo/translate-utils@1.2.1
## 4.3.1
### Patch Changes
- 1a23d63: 设置 dist 参数后,输出路径不对
## 4.3.0
### Minor Changes
- 3ec1789: input 配置支持传数组形式
### Patch Changes
- 5cbe9fe: 如果 vue 的自定义规则里配置了 importDeclaration 值,每次转换相同文件会导致 import 重复导入
- c89ea7b: 不应该写死翻译接口请求的最大文本长度限制
## 4.2.1
### Patch Changes
- bdf2a32: 读取文件时,需确保是文件类型
## 4.2.0
### Minor Changes
- 7f4e610: 放开谷歌翻译限制
## 4.1.0
### Minor Changes
- b570f7a: 默认开启增量翻译
## 4.0.1
### Patch Changes
- 04c3d42: 没有 locales 文件时,执行翻译会报错
## 4.0.0
### Major Changes
- b5713b2: 重写针对 vue 的 script 解析;解决使用 ts 强制类型转换报错
## 3.6.3
### Patch Changes
- df3cfe5: 解析空文件报错
- a1b1540: @prop 装饰器里的中文转换错误
## 3.6.2
### Patch Changes
- 181173f: 函数声明场景下,functionSnippets 配置未生效
## 3.6.1
### Patch Changes
- d4405f6: 导出 excel 报错
## 3.6.0
### Minor Changes
- 9d00b52: 支持模版字段串转换格式的定制
## 3.5.1
### Patch Changes
- e44c713: 报错日志与进度条展示冲突
## 3.5.0
### Minor Changes
- 5b97d29: 解析报错时不再中断后续转换
## 3.4.2
### Patch Changes
- cdf25c8: 主语言包文件改成自动生成
## 3.4.1
### Patch Changes
- 47eccc8: 依赖报错
## 3.4.0
### Minor Changes
- 4199f6b: 支持阿里云翻译
### Patch Changes
- Updated dependencies [4199f6b]
- @ifreeovo/translate-utils@1.2.0
## 3.3.6
### Patch Changes
- 126054e: 文本里的 unicode 编码解析问题
## 3.3.5
### Patch Changes
- 01b6b4b: 无法读取 js 格式语言包内容
## 3.3.4
### Patch Changes
- da9c43d: 修复 props 参数 default 为中文时,自定义 key 函数没生效
## 3.3.3
### Patch Changes
- 11e17ab: node 版本校验不准
## 3.3.2
### Patch Changes
- 9947150: 只进行提取操作且设置翻译文件类型为 js 时,生成的翻译文件内容和文件格式不一致
- 37de6fb: 读取配置文件报错
## 3.3.1
### Patch Changes
- 7a436dd: functionSnippets 重复插入
## 3.3.0
### Minor Changes
- eeb5ba5: 优化翻译算法
### Patch Changes
- eeb5ba5: 1.补充对 ObjectProperty 和 ObjectMethod 的处理 2.转换文件导入的 t 不对 3.避免 i18n 和 js 语法冲突
## 3.2.0
### Minor Changes
- d44c9e2: 支持自定义 vue 文件的标签顺序
### Patch Changes
- 733a1e5: vue 文件 style 标签属性问题
## 3.1.2
### Patch Changes
- 22206d5: 修复 mustache 依赖包下载失败问题
## 3.1.1
### Patch Changes
- 39f158b: 文本中含引号时,会导致字典 key 和转换后的标记不一致
## 3.1.0
### Minor Changes
- 24fc0a3: 支持百度翻译
## 3.0.5
### Patch Changes
- 2cb1677: 模版中含正则表达式会报错
## 3.0.4
### Patch Changes
- 4da8b80: 修复 vue 模版转换后中文缺失
## 3.0.3
### Patch Changes
- 21ee5a0: vue 模版里有忽略提取的注释标记时,被忽略的部分转换后会多出重复属性
## 3.0.2
### Patch Changes
- 39eee88: vue 模版里字符转义问题
- 2368c98: 修复字符串中回车替换问题
## 3.0.1
### Patch Changes
- 0d6f9cc: 去掉语言包 key 里面的回车
- 819444e: 批量翻译后,会缺失一些译文
## 3.0.0
### Major Changes
- b3fba08: 修改 vue 规则配置
- 4516e6d: 修改 js 类型语言包中的导出语句
- 20be77e: 翻译支持批量翻译 5000 个字符
## 2.7.1
### Patch Changes
- 2795c88: vue 模版注释节点后面,转换后会多一行
- 4032d0a: 转换后 vue 模版文本节点里的空格会丢失
## 2.7.0
### Minor Changes
- 39f8e47: 支持强制插入 importDeclaration 配置的内容
## 2.6.1
### Patch Changes
- a3dba41: 翻译文本中包含'|'符号时,页面展示会丢失'|'后面的内容
## 2.6.0
### Minor Changes
- 72c9dad: 支持关闭 prettier
### Patch Changes
- fe49810: 修复空行丢失问题
## 2.5.1
### Patch Changes
- aaa5cc3: 没有支持多层级语言包的翻译
- aaa5cc3: 没有支持多层级语言包的导入导出
- 384c3fd: vue 模版中{{!xxx}}语法转换后会丢失
## 2.5.0
### Minor Changes
- 864c72f: 提取失败的文件可以打印在日志中
- 1d67e33: 支持自定义提取结果的层级
## 2.4.4
### Patch Changes
- 1ff520b: 解析 vue 模版时,无法区分空字符串属性和布尔属性的问题
## 2.4.3
### Patch Changes
- 5b695c5: vue 模版标签属性为空字符串时,会生成错误的属性
- 9233c13: 文本节点里包含某些三元表达式会解析报错
## 2.4.2
### Patch Changes
- 14928bb: 解析@Component({})时报错
## 2.4.1
### Patch Changes
- 8cbef02: 中文提取后,表达式前面会多出一个分号
- e1ecfe7: vue 模版属性里有双引号时解析报错
- 57dfd05: 中文出现\n 时,执行命令会报错
## 2.4.0
### Minor Changes
- 11fd67f: 支持加载 excel
- 2bb91c6: 支持设置语言包文件类型
## 2.3.1
### Patch Changes
- d8deab0: 没有发生提取替换的文件不需要进行修改
- d8deab0: vue 文件中的非 export default 部分应该按 js 规则解析
## 2.3.0
### Minor Changes
- 970d49c: 支持自定义忽略的方法
## 2.2.1
### Patch Changes
- 9932fd9: 应该跳过 console.log 的转换
## 2.2.0
### Minor Changes
- 134ddbf: react 函数组件支持插入代码
## 2.1.3
### Patch Changes
- 752e918: 多语言文件的自定义 key 未生效
## 2.1.2
### Patch Changes
- a5e13da: 中文语言包存在的情况下进行翻译,其他语言包不会新增 key-value
## 2.1.1
### Patch Changes
- 94f6327: vue 模版属性值如果已经被转换过,不应该再进行二次转换
- 5232efb: 修复 replaceAll 方法报错
## 2.1.0
### Minor Changes
- 76f8bd2: 支持增量提取中文到中文语言包
## 2.0.0
### Major Changes
- 847d9b1: 修改命令行交互
## 1.3.13
### Patch Changes
- 5e11170: 修复字符串中的引号未转义
## 1.3.12
### Patch Changes
- 948b08e: 修复模板字符串里表达式存在大写单词会报错的问题
## 1.3.11
### Patch Changes
- d64b33f: 修复 vue 动态类名转换报错
## 1.3.10
### Patch Changes
- 429cb25: vue2 中@Component 的处理逻辑问题
## 1.3.9
### Patch Changes
- 62ecdc2: 进度条插件的安装报错
## 1.3.8
### Patch Changes
- 2df0472: 修复进度条插件的安装报错
## 1.3.7
### Patch Changes
- 0850fd2: 修复 vue 使用 ts 组件声明的转换问题
- 6906921: 修复转换会丢失文件注释
- 2fa4d9b: 修复 html 属性没有属性值的时不应该赋空字符串
## 1.3.6
### Patch Changes
- eae9fd0: 修复 vue 模板属性值为对象时的转换报错
- d3c6f65: 修复模板里{{!xxx}}这种情况会被忽略渲染
- 664b562: vue 模板里的 v-else 转换错误
## 1.3.5
### Patch Changes
- 7944fcc: 降低包的使用门槛.支持到 node12.x 以上
## 1.3.4
### Patch Changes
- be38073: 修复模板字符串含回车时报错
- f814072: 修复表达式语句中的中文正则匹配错误
- adb3f1e: 修复模板字符串缺了 MemberExpression 的场景
## 1.3.3
### Patch Changes
- 465da20: 修复 vue 的 props 属性,转换效果不符合预期
- 71adfc5: 修复 vue 模板里 html 属性值如果已经翻译了,不应该再翻译
- 54ea76f: 修复模版字符串的提取转换不全
## 1.3.2
### Patch Changes
- 697f3d1: 修复对象里有大写字母属性时,无法正确解析
## 1.3.1
### Patch Changes
- 8d1768d: 替换指令参数 translations 为 locales
## 1.3.0
### Minor Changes
- 3384b5d: 支持初始化配置
## 1.2.9
### Patch Changes
- a5c7d6e: 依赖问题
## 1.2.8
### Patch Changes
- 4e410a8: 依赖问题
## 1.2.7
### Patch Changes
- b43fc17: 打包依赖问题
## 1.2.6
### Patch Changes
- 74c8a4c: 修复依赖问题
## 1.2.5
### Patch Changes
- 73a2b9c: 不应该用语言包的 Key 作为翻译文本
- Updated dependencies [73a2b9c]
- @ifreeovo/translate-utils@1.0.0
## 1.2.4
### Patch Changes
- d277c3c: 修复已翻译的语言包没有被正确更新
## 1.2.3
### Patch Changes
- b8a3122: 修复翻译报错
================================================
FILE: packages/i18n-extract-cli/README.md
================================================
# 介绍
这是一款能够自动将代码里的中文转成 i18n 国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于 vue2、vue3 和 react
## 流程设计
见[掘金文章](https://juejin.cn/post/7174082242426175525)
## 功能 🎉
- 支持.mjs.cjs.js.ts.jsx.tsx.vue 后缀文件提取中文
- 支持 vue2.0,vue3.0,react 提取中文
- 支持通过/\*i18n-ignore\*/注释,忽略中文提取
- 支持将提取的中文以 key-value 形式存入\*.json 语言包里
- 支持 prettier 格式化代码
- 支持将中文语言包自动翻译成其他语言
- 支持将翻译结果导出成 excel
- 支持读取 excel 文件并转换成语言包
- 自定义语言包 key 的层级嵌套
- 自定义语言包的 key
- 自定义 i18n 工具的调用对象
- 自定义 i18n 工具的方法名
- 自定义 i18n 第三方包的导入
- 自定义忽略提取的方法
## 安装
```
npm i @ifreeovo/i18n-extract-cli -g
```
## 使用
在项目根目录执行下面命令
```
it
```
## 指令参数
| 参数 | 类型 | 默认值 | 描述 |
| ----------------- | ------- | ---------------------- | -------------------------------------------------------------------------------------- |
| -i, --input | String | 'src' | 指定待提取的文件目录。 |
| -o, --output | String | '' | 输出转换后文件路径。没有值时表示完成提取后自动覆盖原始文件。当有值时,会输出到指定目录 |
| -c, --config-file | String | '' | 指定命令行配置文件的所在路径(可以自定义更多功能) |
| --localePath | String | './locales/zh-CN.json' | 指定提取的中文语言包所存放的路径。 |
| -v,--verbose | Boolean | false | 控制台打印更多调试信息 |
| -h,--help | Boolean | false | 查看指令用法 |
| --skip-extract | Boolean | false | 跳过 i18n 转换阶段。 |
| --skip-translate | Boolean | false | 跳过中文翻译阶段。 |
| --locales | Array | ['en-US'] | 根据中文语言包自动翻译成其他语言。用法例子 --locales en zh-CHT |
| --exportExcel | Boolean | false | 开启后。导出所有翻译内容到 excel。 默认导出到当前目录下的 locales.xlsx |
| --excelPath | String | './locales.xlsx' | 指定导出的 excel 路径。 |
## 子命令
| 子命令 | 描述 |
| --------- | ----------------------------------------- |
| init | 在项目里初始化一个命令行配置 |
| loadExcel | 根据导入翻译文件的 excel 内容,生成语言包 |
## 命令行配置
如果有更多的定制需求,可以在项目根目录执行`it init`,创建`i18n.config.js`文件,按自身需求修改完配置后,再执行`it -c i18n.config.js`。(注意:配置文件里参数的优先级比指令参数高)
```js
// 以下为i18n.config.js默认的完整配置,所有属性均为可选,可以根据自身需要修改
module.exports = {
input: 'src', // 需要转换的文件目录或文件。形式可以是数组,例如['./a.js', 'src'],也可以是字符串,例如'a.js'
output: '', // 没有值时表示完成提取后自动覆盖原始文件
exclude: ['**/node_modules/**/*'], // 排除不需要提取的文件
localePath: './locales/zh-CN.json', // 中文语言包的存放位置
localeFileType: 'json', // 设置语言包的文件类型,支持js、json。默认为json
// rules每个属性对应的是不同后缀文件的处理方式
rules: {
js: {
caller: '', // 自定义this.$t('xxx')中的this。不填则默认没有调用对象
functionName: 't', // 自定义this.$t('xxx')中的$t
customizeKey: function (key, currentFilePath) {
return key
}, // 自定义this.$t('xxx')中的'xxx'部分的生成规则
customSlot: function (slotValue) {
return slotValue
}, // 自定义模版字段串里插槽部分,例如原文为`你好 ${name}`,转换后是t('你好 {name}'),其中转换后的{name}部分,可以通过这个函数定制
importDeclaration: 'import { t } from "i18n"', // 默认在文件里导入i18n包。不填则默认不导入i18n的包。由于i18n的npm包有很多,用户可根据项目自行修改导入语法
forceImport: false, // 即使文件没出现中文,也强行插入importDeclaration定义的语句
},
// ts,cjs,mjs,jsx,tsx配置方式同上
ts: {
caller: '',
functionName: 't',
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
forceImport: false,
},
cjs: {
caller: '',
functionName: 't',
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
importDeclaration: 'import { t } from "i18n"',
forceImport: false,
},
mjs: {
caller: '',
functionName: 't',
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
importDeclaration: 'import { t } from "i18n"',
forceImport: false,
},
jsx: {
caller: '',
functionName: 't',
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
importDeclaration: 'import { t } from "i18n"',
functionSnippets: '', // react函数组件里,全局加代码片段
forceImport: false,
},
tsx: {
caller: '',
functionName: 't',
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
importDeclaration: 'import { t } from "i18n"',
functionSnippets: '',
forceImport: false,
},
vue: {
caller: 'this',
functionNameInTemplate: '$t', // vue这里的配置,仅针对vue的template标签里面的内容生效
functionNameInScript: '$t', // vue这里的配置,仅针对vue的script部分export default里面的内容生效
customizeKey: function (key, currentFilePath) {
return key
},
customSlot: function (slotValue) {
return slotValue
},
importDeclaration: '',
forceImport: false,
tagOrder: ['template', 'script', 'style'], // 支持自定义vue文件的标签顺序
},
},
globalRule: {
ignoreMethods: [], // 忽略指定函数调用的中文提取。例如想忽略sensor.track('中文')的提取。这里就写['sensor.track']
},
// prettier配置,参考https://prettier.io/docs/en/options.html
prettier: {
semi: false,
singleQuote: true,
},
incremental: true, // 开启后。支持将文件中新提取到中文键值对,追加到原有的中文语言包
skipExtract: false, // 跳过提取中文阶段
// 以下是和翻译相关的配置,注意搭配使用
skipTranslate: true, // 跳过翻译语言包阶段。默认不翻译
translationTextMaxLength: 5000, // 每次请求翻译接口,接口携带参数里翻译原文的最大长度
locales: [], // 需要翻译的语言包。例如['en', 'zh-CHT'],会自动翻译英文和繁体
excelPath: './locales.xlsx', // excel存放路径
exportExcel: false, // 是否导出excel
// 参数:
// allKeyValue:已遍历的所有文件的key-value
// currentFileKeyMap: 当前文件提取到的key-value
// currentFilePath: 当前遍历的文件路径
adjustKeyMap(allKeyValue, currentFileKeyMap, currentFilePath) {
return allKeyValue
}, // 对提取结构进行二次处理
}
```
具体用法可以点击下方链接参考
- [react 项目实战例子](https://github.com/IFreeOvO/i18n-cli/tree/master/examples/react-demo)
- [vue 项目实战例子](https://github.com/IFreeOvO/i18n-cli/tree/master/examples/vue-demo)
## 举几个栗子 🌰
1. 跳过转换阶段,仅将中文语言包翻译成其他语言(例如英语、中文繁体等)
```bash
it --skip-extract --locales en zh-CHT
```
2. 跳过自动翻译阶段,仅进行 i18n 转换,并将提取到的 key-value 提取到中文语言包
```bash
it --skip-translate
```
3. 使用自定义配置进行 i18n 转换
```bash
it -c ./i18n.config.js
```
4. 指定需要自动翻译的语言(例如日语),并指定项目里中文语言包的位置(相对于命令的执行位置)。命令执行时会自动根据中文语言包,将日语翻译出来并存入到`ja.json`文件中
```bash
it --localePath ./locales/zh-CN.json --locales ja
```
5. 导入翻译的 excel 表格,并自动生成对应语言包的 json 文件
excel 的表头格式举例`['字典key', 'zh-CN', 'en-US']`
```bash
# 方式1,根据指令参数导入
it loadExcel --excelPath ./demo.xlsx --localePath ./locales/zh-CN.json
# 方式2,根据本地自定义配置导入
it loadExcel -c ./i18n.config.js
```
6. 将翻译结果导出到 excel 表格
```bash
# 方式1,根据指令参数
it --skip-extract --skip-translate --exportExcel --excelPath ./demo.xlsx
# 方式2,根据本地配置
it --skip-extract --skip-translate -c ./i18n.config.js
```
## 转换效果示例
#### react 转换示例
转换前
```jsx
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState('你好')
return (
<div>
<p title="标题">{msg + '呵呵'}</p>
<button onClick={() => setMsg(msg + '啊')}>点击</button>
</div>
)
}
export default Example
```
转换后
```jsx
import { t } from 'i18n'
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState(t('你好'))
return (
<div>
<p title={t('标题')}>{msg + t('呵呵')}</p>
<button onClick={() => setMsg(msg + t('啊'))}>{t('点击')}</button>
</div>
)
}
export default Example
```
#### vue 转换示例
转换前
```vue
<template>
<div :label="'标签'" :title="1 + '标题'">
<p title="测试注释">内容</p>
<button @click="handleClick('信息')">点击</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('点了')
},
},
}
</script>
```
转换后
```vue
<template>
<div :label="$t('标签')" :title="1 + $t('标题')">
<p :title="$t('测试注释')">{{ $t('内容') }}</p>
<button @click="handleClick($t('信息'))">{{ $t('点击') }}</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log(this.$t('点了'))
},
},
}
</script>
```
## 注意事项
- 自定义配置里的 js 规则,除了用于处理 js 文件,也会应用到 vue 的模版和 vue`script`标签的非`export default`部分。例如
```js
<script>import a from 'a.js' function b() {a('哈哈哈')}</script>
```
- 自定义配置里的 vue 的`functionNameInScript`规则,仅针对`script`标签的`export default`部分生效。例如
```vue
<script>
export default {
data: {
return {
a: '测试'
}
}
}
</script>
```
- 代码转换后,新插入的导入语句中`import { t } from "i18n"`的`i18n`是通过打包工具(如`webpack`)的别名`alias`功能实现的。开发者可以结合自身需求自己定义,通过别名把`i18n`文件指向一个绝对路径
- 导入语句中`import { t } from "i18n"`,其中的`i18n`文件内容要自己去封装实现
- 翻译后,命令行工具自动去掉提取汉字里的回车,这是因为回车会影响翻译准确度。所有原文里如果有回车,请自行校对,在语言包里手动补上回车
- 如果使用的 ts 枚举类型包含中文键,这种情况不支持自动转换,例如
```ts
enum typeEnum {
'测试',
}
```
需要手动加注释跳过转换
```ts
/* i18n-ignore */
enum typeEnum {
'测试',
}
```
- type 类型定义里的中文,也不支持自动转换。请用`/*i18n-ignore*/`忽略
```ts
/* i18n-ignore */
type TitleType = '测试'
```
## 开源许可证
[MIT](./LICENSE)
================================================
FILE: packages/i18n-extract-cli/bin/index.js
================================================
#!/usr/bin/env node
const semver = require('semver')
const packageJson = require('../package.json')
const chalk = require('chalk')
checkNodeVersion()
process.env.PACKAGE_NAME = packageJson.name
process.env.PACKAGE_VERSION = packageJson.version
require('../dist/index')
function checkNodeVersion() {
if (!semver.satisfies(process.version, packageJson.engines.node, { includePrerelease: true })) {
console.log(
chalk.red(
'You are using Node ' +
process.version +
', but this version of ' +
packageJson.name +
' requires Node ' +
packageJson.engines.node +
'.\nPlease upgrade your Node version.'
)
)
process.exit(1)
}
}
================================================
FILE: packages/i18n-extract-cli/package.json
================================================
{
"name": "@ifreeovo/i18n-extract-cli",
"version": "4.3.4",
"description": "这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于vue2、vue3和react",
"publishConfig": {
"access": "public"
},
"bin": {
"it": "bin/index.js"
},
"types": "types/index.d.ts",
"scripts": {
"dev": "tsc --watch",
"build": "rimraf dist && tsc --build",
"check": "tsc --noEmit",
"test": "vitest run"
},
"keywords": [
"i18n",
"intl",
"extract",
"intl-cli",
"i18n-cli",
"vue-i18n",
"react-i18n",
"i18n-translate",
"auto-translate",
"translate"
],
"engines": {
"node": ">=14.21.2"
},
"author": "IFreeOvO",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.2",
"@babel/generator": "^7.20.0",
"@babel/plugin-syntax-decorators": "^7.19.0",
"@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.0",
"@babel/types": "^7.20.0",
"@ifreeovo/translate-utils": "^1.2.1",
"@vue/compiler-sfc": "^3.2.39",
"chalk": "4.1.2",
"cli-progress": "^3.11.2",
"commander": "^9.4.1",
"ejs": "^3.1.8",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"htmlparser2": "^8.0.1",
"inquirer": "8.2.5",
"leven": "3.1.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"mustache": "^4.2.0",
"node-xlsx": "^0.21.0",
"prettier": "^2.7.1",
"semver": "^7.3.8",
"serialize-javascript": "^6.0.0",
"slash": "3.0.0"
},
"devDependencies": {
"@types/babel__core": "^7.1.20",
"@types/babel__generator": "^7.6.4",
"@types/babel__template": "^7.4.1",
"@types/babel__traverse": "^7.18.2",
"@types/cli-progress": "^3.11.0",
"@types/ejs": "^3.1.1",
"@types/fs-extra": "^9.0.13",
"@types/glob": "^8.0.0",
"@types/inquirer": "^9.0.3",
"@types/lodash": "^4.14.188",
"@types/minimist": "^1.2.2",
"@types/mustache": "^4.2.5",
"@types/node": "^25.3.5",
"@types/prettier": "^2.7.1",
"@types/serialize-javascript": "^5.0.2",
"rimraf": "^3.0.2",
"vitest": "^2"
},
"files": [
"bin",
"dist",
"types",
"README.md"
],
"repository": {
"type": "git",
"url": "git+https://github.com/IFreeOvO/i18n-cli.git",
"directory": "packages/i18n-extract-cli"
},
"bugs": {
"url": "https://github.com/IFreeOvO/i18n-cli/issues"
},
"homepage": "https://github.com/IFreeOvO/i18n-cli/tree/master/packages/i18n-extract-cli"
}
================================================
FILE: packages/i18n-extract-cli/src/__tests__/transformJs.test.ts
================================================
import { describe, it, expect, beforeEach } from 'vitest'
import transformJs from '../transformJs'
import { initParse } from '../parse'
import Collector from '../collector'
import StateManager from '../utils/stateManager'
import type { transformOptions } from '../../types'
function getDefaultOptions(): transformOptions {
StateManager.setCurrentSourcePath('test.ts')
return {
rule: {
caller: '',
functionName: 't',
customizeKey: (key: string) => key,
customSlot: (slotValue: string) => `{${slotValue}}`,
importDeclaration: 'import { t } from "i18n"',
},
parse: initParse(),
}
}
function resetCollector() {
Collector.setKeyMap({})
Collector.resetCountOfAdditions()
Collector.resetCurrentFileKeyMap()
Collector.setCurrentCollectorPath('')
}
describe('transformJs - TemplateLiteral', () => {
beforeEach(() => {
resetCollector()
})
it('应该正常提取普通模板字符串中的中文', () => {
const code = 'const msg = `你好${name}`'
const result = transformJs(code, getDefaultOptions())
expect(result.code).toContain("t('你好{name}'")
const keyMap = Collector.getKeyMap()
expect(keyMap).toHaveProperty('你好{name}')
})
it('应该跳过包含 HTML 标签的模板字符串 (issue #180)', () => {
const code = `
export const getAirspacePopup = (data) => {
const { areaType, lowerHeight, upperHeight, startTime, endTime } = data
const typeLabel = 'test'
return \`<div style="max-width: 320px;">
<div>类型:\${typeLabel}</div>
<div>高度:\${lowerHeight}-\${upperHeight}m</div>
<div>时间:\${startTime}-\${endTime}</div>
</div>\`
}
`
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
const keys = Object.keys(keyMap)
for (const key of keys) {
expect(key).not.toMatch(/<div/)
expect(key).not.toMatch(/<\/div>/)
}
})
it('应该跳过简单的 HTML 模板字符串', () => {
const code = 'const html = `<p>测试文本</p>`'
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
const keys = Object.keys(keyMap)
for (const key of keys) {
expect(key).not.toMatch(/<p>/)
expect(key).not.toMatch(/<\/p>/)
}
})
it('不应该跳过不包含 HTML 标签的模板字符串', () => {
const code = 'const msg = `测试${value}内容`'
const result = transformJs(code, getDefaultOptions())
expect(result.code).toContain("t('测试{value}内容'")
})
it('不应该跳过仅包含尖括号但非 HTML 标签的模板字符串', () => {
const code = 'const msg = `数量 > ${count} 且 < ${max}`'
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
expect(Object.keys(keyMap).length).toBeGreaterThan(0)
})
it('应该跳过自闭合 HTML 标签的模板字符串', () => {
const code = 'const html = `<img src="test.png" alt="图片" />`'
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
const keys = Object.keys(keyMap)
for (const key of keys) {
expect(key).not.toMatch(/<img/)
}
})
it('应该跳过带 MemberExpression 插值的 HTML 模板字符串', () => {
const code = `const html = \`<span>名称:\${obj.name}</span>\``
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
expect(Object.keys(keyMap).length).toBe(0)
})
it('应该跳过带复杂表达式插值的 HTML 模板字符串', () => {
const code = `const html = \`<div>结果:\${a + b}</div>\``
transformJs(code, getDefaultOptions())
const keyMap = Collector.getKeyMap()
expect(Object.keys(keyMap).length).toBe(0)
})
it('应该正常提取含中文但无 HTML 的多插值模板字符串', () => {
const code = 'const msg = `共${total}条,当前第${page}页`'
const result = transformJs(code, getDefaultOptions())
expect(result.code).toContain('t(')
const keyMap = Collector.getKeyMap()
expect(Object.keys(keyMap).length).toBe(1)
const key = Object.keys(keyMap)[0]
expect(key).toContain('共')
expect(key).toContain('页')
expect(key).not.toMatch(/</)
})
it('应该正常处理纯中文无插值的模板字符串', () => {
const code = 'const msg = `你好世界`'
const result = transformJs(code, getDefaultOptions())
expect(result.code).toContain("t('你好世界')")
})
})
describe('transformJs - StringLiteral', () => {
beforeEach(() => {
resetCollector()
})
it('应该正常提取普通字符串中的中文', () => {
const code = "const msg = '你好世界'"
const result = transformJs(code, getDefaultOptions())
expect(result.code).toContain("t('你好世界')")
})
it('不应该提取不包含中文的字符串', () => {
const code = "const msg = 'hello world'"
const result = transformJs(code, getDefaultOptions())
expect(result.code).not.toContain('t(')
const keyMap = Collector.getKeyMap()
expect(Object.keys(keyMap).length).toBe(0)
})
})
================================================
FILE: packages/i18n-extract-cli/src/collector.ts
================================================
import type { CustomizeKey, StringObject } from '../types'
import log from './utils/log'
import { removeLineBreaksInTag } from './utils/removeLineBreaksInTag'
import { escapeQuotes } from './utils/escapeQuotes'
class Collector {
private static _instance: Collector
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
static getInstance() {
if (!this._instance) {
this._instance = new Collector()
}
return this._instance
}
private keyMap: StringObject = {}
// 记录每个文件执行提取的次数
private countOfAdditions = 0
// 记录单个文件里提取的中文,键为自定义key,值为原始中文key
private currentFileKeyMap: Record<string, string> = {}
private currentFilePath = ''
setCurrentCollectorPath(path: string) {
this.currentFilePath = path
}
getCurrentCollectorPath() {
return this.currentFilePath
}
add(originalText: string, customizeKeyFn: CustomizeKey) {
const formattedText = removeLineBreaksInTag(originalText)
const translationKey = customizeKeyFn(escapeQuotes(formattedText), this.currentFilePath) // key中不能包含回车
log.verbose('提取中文:', formattedText)
// keyMap 的 value 使用原始文本(保留 \n 等换行符),确保写入 JSON 的值与源码一致
// formattedText 仅用于生成 key(key 中不能包含回车)
const valueText = originalText.replace('|', "{'|'}") // '|' 管道符在vue-i18n表示复数形式,需要特殊处理。见https://vue-i18n.intlify.dev/guide/essentials/pluralization.html
this.keyMap[translationKey] = valueText
this.countOfAdditions++
this.currentFileKeyMap[translationKey] = originalText
return translationKey
}
getCurrentFileKeyMap(): Record<string, string> {
return this.currentFileKeyMap
}
resetCurrentFileKeyMap() {
this.currentFileKeyMap = {}
}
getKeyMap(): StringObject {
return this.keyMap
}
setKeyMap(value: StringObject) {
this.keyMap = value
}
resetCountOfAdditions() {
this.countOfAdditions = 0
}
getCountOfAdditions(): number {
return this.countOfAdditions
}
}
export default Collector.getInstance()
================================================
FILE: packages/i18n-extract-cli/src/commands/init/index.ts
================================================
import fs from 'fs-extra'
import { getAbsolutePath } from '../../utils/getAbsolutePath'
import defaultConfig from '../../default.config'
import { CONFIG_FILE_NAME } from '../../utils/constants'
import { serializeCode } from '../../utils/serializeCode'
function execInit() {
const configPath = getAbsolutePath(process.cwd(), CONFIG_FILE_NAME)
const code = serializeCode(defaultConfig, false)
fs.outputFileSync(configPath, code)
}
export default execInit
================================================
FILE: packages/i18n-extract-cli/src/commands/loadExcel/index.ts
================================================
import xlsx from 'node-xlsx'
import type { StringObject } from '../../../types'
import { CommandOptions } from '../../../types/index'
import { getAbsolutePath } from '../../utils/getAbsolutePath'
import { getI18nConfig } from '../../utils/initConfig'
import { getLocaleDir } from '../../utils/getLocaleDir'
import StateManager from '../../utils/stateManager'
import { saveLocaleFile } from '../../utils/saveLocaleFile'
import log from '../../utils/log'
import { spreadObject } from '../../utils/spreadObject'
function getLangList(locales: string[], rows: string[][]): StringObject[] {
const langList: Record<string, string>[] = []
const result: StringObject[] = []
locales.forEach((locale, i) => {
// 创建一个对象,存储该语言的翻译
langList.push({})
rows.forEach((row) => {
const key = row[0]
const value = row[i + 1]
langList[i][key] = value
})
// 对象的key可能是xx.xx这种形式,需要转成{xx:{xx:1}}
result[i] = spreadObject(langList[i])
})
return result
}
function execLoadExcel(options: CommandOptions) {
log.info(`正在导入excel翻译文件`)
const i18nConfig = getI18nConfig(options)
// 全局缓存脚手架配置
StateManager.setToolConfig(i18nConfig)
const { excelPath } = i18nConfig
const xlsxData = xlsx.parse(getAbsolutePath(process.cwd(), excelPath))[0].data as string[][]
if (xlsxData.length === 0) {
return
}
// 获取待生成的语言
const locales = xlsxData[0].slice(1)
const rows = xlsxData.slice(1)
const langList: StringObject[] = getLangList(locales, rows)
// 将excel翻译内容更新到本地
locales.forEach((locale, i) => {
const localeDirPath = getLocaleDir()
const currentLocalePath = getAbsolutePath(
localeDirPath,
`${locale}.${i18nConfig.localeFileType}`
)
const localePack = langList[i]
log.verbose(`写入到指定文件:`, currentLocalePath)
saveLocaleFile(localePack, currentLocalePath)
})
log.success(`导入完毕!`)
}
export default execLoadExcel
================================================
FILE: packages/i18n-extract-cli/src/core.ts
================================================
import type { CommandOptions, FileExtension, TranslateConfig, PrettierConfig } from '../types'
import fs from 'fs-extra'
import chalk from 'chalk'
import inquirer from 'inquirer'
import path from 'path'
import prettier from 'prettier'
import cliProgress from 'cli-progress'
import glob from 'glob'
import merge from 'lodash/merge'
import cloneDeep from 'lodash/cloneDeep'
import isArray from 'lodash/isArray'
import transform from './transform'
import log from './utils/log'
import { getAbsolutePath } from './utils/getAbsolutePath'
import Collector from './collector'
import translate from './translate'
import getLang from './utils/getLang'
import { YOUDAO, GOOGLE, BAIDU, ALICLOUD } from './utils/constants'
import StateManager from './utils/stateManager'
import exportExcel from './exportExcel'
import { getI18nConfig } from './utils/initConfig'
import { saveLocaleFile } from './utils/saveLocaleFile'
import { isObject } from './utils/assertType'
import errorLogger from './utils/error-logger'
import isDirectory from './utils/isDirectory'
interface InquirerResult {
translator?: 'google' | 'youdao' | 'baidu' | 'alicloud'
key?: string
secret?: string
proxy?: string
}
function getPathFromInput(input: string, exclude: string[]) {
const resolvePath = getAbsolutePath(process.cwd(), input)
if (!fs.existsSync(resolvePath)) {
log.error(`路径${resolvePath}不存在,请重新设置input参数`)
process.exit(1)
}
if (isDirectory(resolvePath)) {
const paths = glob
.sync(`${resolvePath}/**/*.{cjs,mjs,js,ts,tsx,jsx,vue}`, {
ignore: exclude,
})
.filter((file) => fs.statSync(file).isFile())
return paths
} else {
return [resolvePath]
}
}
function getSourceFilePaths(input: string, exclude: string[]): string[] {
const filePaths: string[] = []
if (isArray(input)) {
input.forEach((item) => {
const paths = getPathFromInput(item, exclude)
filePaths.push(...paths)
})
} else {
const paths = getPathFromInput(input, exclude)
filePaths.push(...paths)
}
return filePaths
}
// TODO: 逻辑需要重写
function saveLocale(localePath: string) {
const keyMap = Collector.getKeyMap()
const localeAbsolutePath = getAbsolutePath(process.cwd(), localePath)
if (!fs.existsSync(localeAbsolutePath)) {
fs.ensureFileSync(localeAbsolutePath)
}
if (!fs.statSync(localeAbsolutePath).isFile()) {
log.error(`路径${localePath}不是一个文件,请重新设置localePath参数`)
process.exit(1)
}
saveLocaleFile(keyMap, localeAbsolutePath)
log.verbose(`输出中文语言包到指定位置:`, localeAbsolutePath)
}
function getPrettierParser(ext: string): string {
switch (ext) {
case 'vue':
return 'vue'
case 'ts':
case 'tsx':
return 'babel-ts'
default:
return 'babel'
}
}
function getOutputPath(input: string, output: string, sourceFilePath: string): string {
let outputPath
if (output) {
const filePath = sourceFilePath.replace(getAbsolutePath(process.cwd(), input) + '/', '')
outputPath = getAbsolutePath(process.cwd(), output, filePath)
fs.ensureFileSync(outputPath)
} else {
outputPath = getAbsolutePath(process.cwd(), sourceFilePath)
}
return outputPath
}
function formatInquirerResult(answers: InquirerResult): TranslateConfig {
if (answers.translator === YOUDAO) {
return {
translator: answers.translator,
youdao: {
key: answers.key,
secret: answers.secret,
},
}
} else if (answers.translator === BAIDU) {
return {
translator: answers.translator,
baidu: {
key: answers.key,
secret: answers.secret,
},
}
} else if (answers.translator === ALICLOUD) {
return {
translator: answers.translator,
alicloud: {
key: answers.key,
secret: answers.secret,
},
}
} else {
return {
translator: answers.translator,
google: {
proxy: answers.proxy,
},
}
}
}
async function getTranslationConfig() {
const cachePath = getAbsolutePath(__dirname, '../.cache/configCache.json')
fs.ensureFileSync(cachePath)
const cache = fs.readFileSync(cachePath, 'utf8') || '{}'
const oldConfigCache: InquirerResult = JSON.parse(cache)
const answers = await inquirer.prompt([
{
type: 'list',
name: 'translator',
message: '请选择翻译接口',
default: YOUDAO,
choices: [
{ name: '有道翻译', value: YOUDAO },
{ name: '谷歌翻译', value: GOOGLE },
{ name: '百度翻译', value: BAIDU },
{ name: '阿里云机器翻译', value: ALICLOUD },
],
when(answers) {
return !answers.skipTranslate
},
},
{
type: 'input',
name: 'proxy',
message: '使用谷歌服务需要翻墙,请输入代理地址(可选)',
default: oldConfigCache.proxy || '',
when(answers) {
return answers.translator === GOOGLE
},
},
{
type: 'input',
name: 'key',
message: '请输入有道翻译appKey',
default: oldConfigCache.key || '',
when(answers) {
return answers.translator === YOUDAO
},
validate(input) {
return input.length === 0 ? 'appKey不能为空' : true
},
},
{
type: 'input',
name: 'secret',
message: '请输入有道翻译appSecret',
default: oldConfigCache.secret || '',
when(answers) {
return answers.translator === YOUDAO
},
validate(input) {
return input.length === 0 ? 'appSecret不能为空' : true
},
},
{
type: 'input',
name: 'key',
message: '请输入百度翻译appId',
default: oldConfigCache.key || '',
when(answers) {
return answers.translator === BAIDU
},
validate(input) {
return input.length === 0 ? 'appKey不能为空' : true
},
},
{
type: 'input',
name: 'secret',
message: '请输入百度翻译appSecret',
default: oldConfigCache.secret || '',
when(answers) {
return answers.translator === BAIDU
},
validate(input) {
return input.length === 0 ? 'appSecret不能为空' : true
},
},
{
type: 'input',
name: 'key',
message: '请输入阿里云机器翻译accessKeyId',
default: oldConfigCache.key || '',
when(answers) {
return answers.translator === ALICLOUD
},
validate(input) {
return input.length === 0 ? 'accessKeyId不能为空' : true
},
},
{
type: 'input',
name: 'secret',
message: '请输入阿里云机器翻译accessKeySecret',
default: oldConfigCache.secret || '',
when(answers) {
return answers.translator === ALICLOUD
},
validate(input) {
return input.length === 0 ? 'accessKeySecret不能为空' : true
},
},
])
const newConfigCache = Object.assign(oldConfigCache, answers)
fs.writeFileSync(cachePath, JSON.stringify(newConfigCache), 'utf8')
const result = formatInquirerResult(answers)
return result
}
function formatCode(code: string, ext: string, prettierConfig: PrettierConfig): string {
let stylizedCode = code
if (isObject(prettierConfig)) {
stylizedCode = prettier.format(code, {
...prettierConfig,
parser: getPrettierParser(ext),
})
log.verbose(`格式化代码完成`)
}
return stylizedCode
}
export default async function (options: CommandOptions) {
let i18nConfig = getI18nConfig(options)
if (!i18nConfig.skipTranslate) {
const translationConfig = await getTranslationConfig()
i18nConfig = merge(i18nConfig, translationConfig)
}
// 全局缓存脚手架配置
StateManager.setToolConfig(i18nConfig)
const {
input,
exclude,
output,
rules,
localePath,
locales,
skipExtract,
skipTranslate,
adjustKeyMap,
localeFileType,
} = i18nConfig
log.debug(`命令行配置信息:`, i18nConfig)
let oldPrimaryLang: Record<string, string> = {}
const primaryLangPath = getAbsolutePath(process.cwd(), localePath)
if (!fs.existsSync(primaryLangPath)) {
saveLocaleFile({}, primaryLangPath)
}
oldPrimaryLang = getLang(primaryLangPath)
if (!skipExtract) {
log.info('正在转换中文,请稍等...')
const sourceFilePaths = getSourceFilePaths(input, exclude)
const bar = new cliProgress.SingleBar(
{
format: `${chalk.cyan('提取进度:')} [{bar}] {percentage}% {value}/{total}`,
},
cliProgress.Presets.shades_classic
)
const startTime = new Date().getTime()
bar.start(sourceFilePaths.length, 0)
sourceFilePaths.forEach((sourceFilePath) => {
StateManager.setCurrentSourcePath(sourceFilePath)
log.verbose(`正在提取文件中的中文:`, sourceFilePath)
errorLogger.setFilePath(sourceFilePath)
const sourceCode = fs.readFileSync(sourceFilePath, 'utf8')
const ext = path.extname(sourceFilePath).replace('.', '') as FileExtension
Collector.resetCountOfAdditions()
Collector.setCurrentCollectorPath(sourceFilePath)
// 跳过空文件
if (sourceCode.trim() === '') {
bar.increment()
return
}
const { code } = transform(sourceCode, ext, rules, sourceFilePath)
log.verbose(`完成中文提取和语法转换:`, sourceFilePath)
// 只有文件提取过中文,或文件规则forceImport为true时,才重新写入文件
if (Collector.getCountOfAdditions() > 0 || rules[ext].forceImport) {
const stylizedCode = formatCode(code, ext, i18nConfig.prettier)
if (isArray(input)) {
log.error('input为数组时,暂不支持设置dist参数')
return
}
const outputPath = getOutputPath(input, output, sourceFilePath)
fs.writeFileSync(outputPath, stylizedCode, 'utf8')
log.verbose(`生成文件:`, outputPath)
}
// 自定义当前文件的keyMap
if (adjustKeyMap) {
const newkeyMap = adjustKeyMap(
cloneDeep(Collector.getKeyMap()),
Collector.getCurrentFileKeyMap(),
sourceFilePath
)
Collector.setKeyMap(newkeyMap)
Collector.resetCurrentFileKeyMap()
}
bar.increment()
})
// 增量转换时,保留之前的提取的中文结果
if (i18nConfig.incremental) {
const newkeyMap = merge(oldPrimaryLang, Collector.getKeyMap())
Collector.setKeyMap(newkeyMap)
}
const extName = path.extname(localePath)
const savePath = localePath.replace(extName, `.${localeFileType}`)
saveLocale(savePath)
bar.stop()
const endTime = new Date().getTime()
log.info(`耗时${((endTime - startTime) / 1000).toFixed(2)}s`)
}
errorLogger.printErrors()
console.log('') // 空一行
if (!skipTranslate) {
await translate(localePath, locales, oldPrimaryLang, {
translator: i18nConfig.translator,
google: i18nConfig.google,
youdao: i18nConfig.youdao,
baidu: i18nConfig.baidu,
alicloud: i18nConfig.alicloud,
translationTextMaxLength: i18nConfig.translationTextMaxLength,
})
}
log.success('转换完毕!')
if (i18nConfig.exportExcel) {
log.info(`正在导出excel翻译文件`)
exportExcel()
log.success(`导出完毕!`)
}
}
================================================
FILE: packages/i18n-extract-cli/src/default.config.ts
================================================
import { Config, Rule } from '../types'
// 参数path,在生成配置文件时需要展示在文件里,所以这里去掉eslint校验
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getCustomizeKey(key: string, path?: string): string {
return key
}
function getCustomSlot(slotValue: string): string {
return `{${slotValue}}`
}
function getCommonRule(): Rule {
return {
caller: '',
functionName: 't',
customizeKey: getCustomizeKey,
customSlot: getCustomSlot,
importDeclaration: 'import { t } from "i18n"',
}
}
const config: Config = {
input: 'src',
output: '',
exclude: ['**/node_modules/**/*'],
rules: {
js: getCommonRule(),
ts: getCommonRule(),
cjs: getCommonRule(),
mjs: getCommonRule(),
jsx: {
...getCommonRule(),
functionSnippets: '',
},
tsx: {
...getCommonRule(),
functionSnippets: '',
},
vue: {
caller: 'this',
functionNameInTemplate: '$t',
functionNameInScript: '$t',
customizeKey: getCustomizeKey,
customSlot: getCustomSlot,
importDeclaration: '',
tagOrder: ['template', 'script', 'style'],
},
},
prettier: {
semi: false,
singleQuote: true,
},
incremental: true,
skipExtract: false,
localePath: './locales/zh-CN.json',
localeFileType: 'json',
excelPath: './locales.xlsx',
exportExcel: false,
skipTranslate: false,
translationTextMaxLength: 5000,
locales: ['en-US'],
globalRule: {
ignoreMethods: [],
},
// 参数currentFileKeyMap和currentFilePath,在生成配置文件时需要展示在文件里,所以这里去掉eslint校验
// eslint-disable-next-line @typescript-eslint/no-unused-vars
adjustKeyMap(allKeyValue, currentFileKeyMap, currentFilePath) {
return allKeyValue
},
}
export default config
================================================
FILE: packages/i18n-extract-cli/src/exportExcel.ts
================================================
import fs from 'fs-extra'
import type { StringObject } from '../types'
import StateManager from './utils/stateManager'
import { getExcelHeader, buildExcel } from './utils/excelUtil'
import { getAbsolutePath } from './utils/getAbsolutePath'
import getLang from './utils/getLang'
import log from './utils/log'
import { getLocaleDir } from './utils/getLocaleDir'
import { flatObjectDeep } from './utils/flatObjectDeep'
export default function exportExcel() {
const { localeFileType, excelPath } = StateManager.getToolConfig()
const headers = getExcelHeader()
const matchResult = excelPath.match(new RegExp(`([A-Za-z-]+.xlsx)`, 'g')) ?? []
const excelFileName = matchResult[0] ?? ''
// 获取语言包存放路径
const localeDirPath = getLocaleDir()
const locales = headers.slice(1)
// 遍历每个语言包,并组成excel的data
const data: string[][] = []
for (const locale of locales) {
const currentLocalePath = getAbsolutePath(localeDirPath, `${locale}.${localeFileType}`)
let lang: StringObject = {}
if (fs.existsSync(currentLocalePath)) {
lang = getLang(currentLocalePath)
} else {
log.error(`${locale}语言包不存在`)
break
}
// 遍历中文时,存入key和value,并创建数据行
if (locale === 'zh-CN') {
let rowIndex = 0
const keyValueMap = flatObjectDeep(lang)
Object.keys(keyValueMap).forEach((key) => {
data.push([])
data[rowIndex].push(key) // 放入字段key
data[rowIndex].push(keyValueMap[key]) // 放入中文翻译
rowIndex++
})
} else {
// 其他语言,按中文字典key,把对应的语言翻译结果填入表格
let rowIndex = 0
const keyValueMap = flatObjectDeep(lang)
Object.keys(keyValueMap).forEach((key) => {
if (data[rowIndex]) {
data[rowIndex].push(keyValueMap[key])
}
rowIndex++
})
}
}
const excelBuffer = buildExcel(headers, data, excelFileName)
const excelData = new Uint8Array(excelBuffer, excelBuffer.byteOffset, excelBuffer.length)
fs.writeFileSync(getAbsolutePath(process.cwd(), excelPath), excelData, 'utf8')
}
================================================
FILE: packages/i18n-extract-cli/src/index.ts
================================================
import type { Command } from 'commander'
import { program, Option } from 'commander'
import leven from 'leven'
import minimist from 'minimist'
import execCommand from './core'
const chalk = require('chalk')
program
.version(`${process.env.PACKAGE_NAME} ${process.env.PACKAGE_VERSION}`)
.usage('[command] [options]')
program
.option('-i, --input <path>', '输入文件路径')
.option('-o, --output <path>', '输出文件路径')
.option('-c, --config-file <path>', '配置文件所在路径')
.option('-v, --verbose', '控制台打印更多调试信息')
.option('--skip-extract', '跳过中文提取阶段')
.option('--skip-translate', '跳过中文翻译阶段')
.option('--locales <locales...>', '根据中文语言包自动翻译成其他语言')
.option('--localePath <path>', '指定提取的中文语言包所存放的路径')
.option('--excelPath <path>', '语言包excel的存放路径')
.option('--exportExcel', '将所有翻译导入到excel。用于人工校对翻译')
.action((options) => {
execCommand(options)
})
program
.command('init')
.description('在项目里初始化一个配置文件')
.action(() => {
require('./commands/init/index').default()
})
program
.command('loadExcel')
.description('导入翻译语言的excel')
.option('-v, --verbose', '控制台打印更多调试信息')
.option('-c, --config-file <path>', '配置文件所在路径')
.option('--localePath <path>', '指定提取的中文语言包所存放的路径')
.option('--excelPath <path>', '语言包excel的存放路径')
.action(() => {
// TODO: 不知道为什么,这里commander没有直接返回指令参数,先用minimist自己处理
const options = minimist(process.argv.slice(3))
if (options.c) {
options.configFile = options.c
}
require('./commands/loadExcel').default(options)
})
program.addOption(new Option('-d, --debug').hideHelp())
program.on('option:verbose', function () {
process.env.CLI_VERBOSE = program.opts().verbose
})
program.on('option:debug', function () {
process.env.CLI_DEBUG = program.opts().debug
})
enhanceErrorMessages()
program.parse(process.argv)
function enhanceErrorMessages() {
type CMD = Command & { Command: { prototype: { [key: string]: any } } }
;(program as CMD).Command.prototype['unknownOption'] = function (...options: any) {
const unknownOption = options[0]
this.outputHelp()
console.log()
console.log(` ` + chalk.red(`Unknown option ${chalk.yellow(unknownOption)}.`))
if (unknownOption.startsWith('--')) {
suggestCommands(unknownOption.slice(2, unknownOption.length))
}
console.log()
process.exit(1)
}
}
function suggestCommands(unknownOption: string) {
const availableOptions = ['input', 'output', 'config-file']
let suggestion: string | undefined
availableOptions.forEach((name) => {
const isBestMatch = leven(name, unknownOption) < leven(suggestion || '', unknownOption)
if (leven(name, unknownOption) < 3 && isBestMatch) {
suggestion = name
}
})
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(`--${suggestion}`)}?`))
}
}
================================================
FILE: packages/i18n-extract-cli/src/parse.ts
================================================
import path from 'node:path'
import StateManager from './utils/stateManager'
const babel = require('@babel/core')
const presetEnv = require('@babel/preset-env')
const presetReact = require('@babel/preset-react')
const typescriptPresets = require('@babel/preset-typescript')
const pluginSyntaxDecorators = require('@babel/plugin-syntax-decorators')
function langToExtension(lang = 'js') {
switch (lang.toLowerCase()) {
case 'js':
case 'mjs':
case 'javascript':
return '.js'
case 'ts':
case 'typescript':
return '.ts'
case 'jsx':
return '.jsx'
case 'tsx':
return '.tsx'
default:
throw new Error(`vue script标签里存在未知的lang属性值: ${lang}`)
}
}
function getSourceFileName() {
const sourcePath = StateManager.getCurrentSourcePath()
const lang = StateManager.getVueScriptLang()
const ext = path.extname(sourcePath)
const isVueFile = ext === '.vue'
const basename = path.basename(sourcePath, ext)
const filename = isVueFile ? basename + langToExtension(lang) : basename + ext
return filename
}
export function initParse() {
return function (code: string) {
return babel.parseSync(code, {
ast: true,
configFile: false,
filename: getSourceFileName(),
presets: [presetEnv, presetReact, typescriptPresets],
plugins: [[pluginSyntaxDecorators, { legacy: true }]],
})
}
}
================================================
FILE: packages/i18n-extract-cli/src/transform.ts
================================================
import chalk from 'chalk'
import type { Rules, FileExtension } from '../types'
import transformJs from './transformJs'
import transformVue from './transformVue'
import { initParse } from './parse'
function transform(
code: string,
ext: FileExtension,
rules: Rules,
filePath: string
): {
code: string
} {
switch (ext) {
case 'cjs':
case 'mjs':
case 'js':
case 'jsx':
return transformJs(code, {
rule: rules[ext],
parse: initParse(),
})
case 'ts':
case 'tsx':
return transformJs(code, {
rule: rules[ext],
parse: initParse(),
})
case 'vue':
// 规则functionName废弃掉,使用functionNameInScript代替
rules[ext].functionName = rules[ext].functionNameInScript ?? ''
return transformVue(code, {
rule: rules[ext],
filePath,
})
default:
throw new Error(chalk.red(`不支持对.${ext}后缀的文件进行提取`))
}
}
export default transform
================================================
FILE: packages/i18n-extract-cli/src/transformJs.ts
================================================
import { NodePath } from '@babel/traverse'
import type {
Comment,
StringLiteral,
TemplateLiteral,
ObjectProperty,
ObjectMethod,
SpreadElement,
JSXText,
JSXAttribute,
ImportDeclaration,
CallExpression,
ObjectExpression,
MemberExpression,
Expression,
ArrowFunctionExpression,
Node,
ReturnStatement,
FunctionExpression,
FunctionDeclaration,
Statement,
} from '@babel/types'
import type { GeneratorResult } from '@babel/generator'
import type { transformOptions } from '../types'
import { types as t } from '@babel/core'
import traverse from '@babel/traverse'
import babelGenerator from '@babel/generator'
import template from '@babel/template'
import isEmpty from 'lodash/isEmpty'
import Collector from './collector'
import { includeChinese } from './utils/includeChinese'
import { isObject } from './utils/assertType'
import { IGNORE_REMARK } from './utils/constants'
import StateManager from './utils/stateManager'
type TemplateParams = {
[k: string]:
| string
| {
isAstNode: true
value: Expression
}
}
interface Context {
hasImportI18n?: boolean
}
function getObjectExpression(obj: TemplateParams): ObjectExpression {
const ObjectPropertyArr: Array<ObjectMethod | ObjectProperty | SpreadElement> = []
Object.keys(obj).forEach((k) => {
const tempValue = obj[k]
let newValue
if (isObject(tempValue)) {
newValue = tempValue.value
} else {
newValue = t.identifier(tempValue)
}
ObjectPropertyArr.push(t.objectProperty(t.identifier(k), newValue))
})
const ast = t.objectExpression(ObjectPropertyArr)
return ast
}
// 判断节点是否是props属性的默认值
function isPropNode(path: NodePath<StringLiteral>): boolean {
const objWithProps = path.parentPath?.parentPath?.parentPath?.parentPath?.parent
const rootNode =
path.parentPath?.parentPath?.parentPath?.parentPath?.parentPath?.parentPath?.parent
let isMeetProp = false
let isMeetKey = false
let isMeetContainer = false
const propDecoratorNode = path.parentPath.parentPath?.parentPath?.node // @props节点
// 属性是否包含在props结构里
if (
objWithProps &&
objWithProps.type === 'ObjectProperty' &&
objWithProps.key.type === 'Identifier' &&
objWithProps.key.name === 'props'
) {
isMeetProp = true
} else if (
propDecoratorNode?.type === 'CallExpression' &&
(propDecoratorNode?.callee as any)?.name === 'prop'
) {
// TODO: 不严谨的处理。后期再改
// 属性是否包含在@props结构里
isMeetProp = true
}
// 对应key是否是default
if (
path.parent &&
path.parent.type === 'ObjectProperty' &&
path.parent.key.type === 'Identifier' &&
path.parent.key.name === 'default'
) {
isMeetKey = true
}
// 遍历到指定层数后是否是导出声明
if (rootNode && rootNode.type === 'ExportDefaultDeclaration') {
isMeetContainer = true
} else if (rootNode?.type === 'ClassDeclaration') {
/**
* ts导出类的情况
* @Component
* export default class MyComponent extends Vue
* */
isMeetContainer = true
}
return isMeetProp && isMeetKey && isMeetContainer
}
function getStringLiteral(value: string): StringLiteral {
return Object.assign(t.stringLiteral(value), {
extra: {
raw: `'${value}'`,
rawValue: value,
},
})
}
function nodeToCode(node: Node): string {
return babelGenerator(node).code
}
// 允许往react函数组件中加入自定义代码
function insertSnippets(node: ArrowFunctionExpression | FunctionExpression, snippets?: string) {
if (node.body.type === 'BlockStatement' && snippets) {
const returnStatement = node.body.body.find((node: Node) => node.type === 'ReturnStatement')
if (returnStatement) {
// TODO: 这里判断是否包含snippet,不严谨,推荐节点判断
const arg = (returnStatement as ReturnStatement).argument
const statements = template.statements(snippets)()
const source = nodeToCode(node.body).replace(/[\n\s]/g, '')
for (let i = 0; i < statements.length; i++) {
const statement = statements[i]
const snippet = nodeToCode(statement).replace(/[\n\s]/g, '')
// 只插入不存在的snippet
if (!source.includes(snippet)) {
pushStatement(node, arg, statement)
}
}
}
}
}
function pushStatement(
node: ArrowFunctionExpression | FunctionExpression,
arg: Expression | null | undefined,
statement: Statement
) {
// 函数是否是react函数组件
// 情况1: 返回的三元表达式包含JSXElement
// 情况2: 直接返回了JSXElement
const argType = arg?.type
const code = nodeToCode(node)
if (
argType === 'ConditionalExpression' &&
(arg.consequent.type === 'JSXElement' || arg.alternate.type === 'JSXElement')
) {
if (includeChinese(code) && node.body.type === 'BlockStatement') {
node.body.body.unshift(statement)
}
} else if (argType === 'JSXElement' && node.body.type === 'BlockStatement') {
node.body.body.unshift(statement)
}
}
function transformJs(
code: string,
options: transformOptions,
context: Context = {
hasImportI18n: false, // 文件是否导入过i18n
}
): GeneratorResult {
const { rule } = options
const {
caller,
functionName,
customizeKey,
customSlot,
importDeclaration,
functionSnippets,
forceImport,
} = rule
let hasTransformed = false // 文件里是否存在中文转换,有的话才有必要导入i18n
function getCallExpression(identifier: string, quote = "'"): string {
const callerName = caller ? caller + '.' : ''
const expression = `${callerName}${functionName}(${quote}${identifier}${quote})`
return expression
}
function getReplaceValue(translationKey: string, params?: TemplateParams) {
if (!functionName) {
throw new Error('functionName is required')
}
// 表达式结构 obj.fn('xx',{xx:xx})
let expression
// i18n标记有参数的情况
if (params) {
const keyLiteral = getStringLiteral(translationKey)
if (caller) {
return t.callExpression(
t.memberExpression(t.identifier(caller), t.identifier(functionName)),
[keyLiteral, getObjectExpression(params)]
)
} else {
return t.callExpression(t.identifier(functionName), [
keyLiteral,
getObjectExpression(params),
])
}
} else {
// i18n标记没参数的情况
expression = getCallExpression(translationKey)
return template.expression(expression)()
}
}
function transformAST(code: string, options: transformOptions) {
function getTraverseOptions() {
return {
enter(path: NodePath) {
const leadingComments = path.node.leadingComments
if (leadingComments) {
// 是否跳过翻译
let isSkipTransform = false
leadingComments.every((comment: Comment) => {
if (comment.value.includes(IGNORE_REMARK)) {
isSkipTransform = true
return false
}
return true
})
if (isSkipTransform) {
path.skip()
}
}
},
StringLiteral(path: NodePath<StringLiteral>) {
// raw可以拿到未转义的原始文本。例如\u4E00,用raw获取时是'\u4E00'。用value获取的是'一'
const value = path.node.extra
? (path.node.extra.raw as string).slice(1, -1)
: path.node.value
// 处理vue props里的中文
if (includeChinese(value) && options.isJsInVue && isPropNode(path)) {
const translationKey = Collector.add(value, customizeKey)
const expression = `function() {
return ${getCallExpression(translationKey)}
}`
path.replaceWith(template.expression(expression)())
path.skip()
return
}
if (includeChinese(value)) {
hasTransformed = true
const translationKey = Collector.add(value, customizeKey)
path.replaceWith(getReplaceValue(translationKey))
}
path.skip()
},
TemplateLiteral(path: NodePath<TemplateLiteral>) {
const { node } = path
const templateMembers = [...node.quasis, ...node.expressions]
templateMembers.sort((a, b) => (a.start as number) - (b.start as number))
const containsHtml = node.quasis.some((node) =>
/<\/?[a-zA-Z][a-zA-Z0-9]*[\s>/]/.test(node.value.raw)
)
if (containsHtml) {
return
}
const shouldReplace = node.quasis.some((node) => includeChinese(node.value.raw))
if (shouldReplace) {
let value = ''
let slotIndex = 1
const params: TemplateParams = {}
templateMembers.forEach(function (node) {
if (node.type === 'Identifier') {
value += customSlot(node.name)
params[node.name] = node.name
} else if (node.type === 'TemplateElement') {
value += node.value.raw.replace(/[\r\n]/g, '') // 用raw防止字符串中出现 /n
} else if (node.type === 'MemberExpression') {
const key = `slot${slotIndex++}`
value += customSlot(key)
params[key] = {
isAstNode: true,
value: node as MemberExpression,
}
} else {
// 处理${}内容为表达式的情况。例如`测试${a + b}`,把 a+b 这个语法树作为params的值, 并自定义params的键为slot加数字的形式
const key = `slot${slotIndex++}`
value += customSlot(key)
const expression = babelGenerator(node).code
const tempAst = transformAST(expression, options) as any
const expressionAst = tempAst.program.body[0].expression
params[key] = {
isAstNode: true,
value: expressionAst,
}
}
})
hasTransformed = true
const translationKey = Collector.add(value, customizeKey)
const slotParams = isEmpty(params) ? undefined : params
path.replaceWith(getReplaceValue(translationKey, slotParams))
}
},
JSXText(path: NodePath<JSXText>) {
const value = path.node.value
if (includeChinese(value)) {
hasTransformed = true
const translationKey = Collector.add(value.trim(), customizeKey)
path.replaceWith(t.jSXExpressionContainer(getReplaceValue(translationKey)))
}
path.skip()
},
JSXAttribute(path: NodePath<JSXAttribute>) {
const node = path.node as NodePath<JSXAttribute>['node']
const valueType = node.value?.type
if (valueType === 'StringLiteral' && node.value && includeChinese(node.value.value)) {
const value = node.value.value
const translationKey = Collector.add(value, customizeKey)
const jsxIdentifier = t.jsxIdentifier(node.name.name as string)
const jsxContainer = t.jSXExpressionContainer(getReplaceValue(translationKey))
hasTransformed = true
path.replaceWith(t.jsxAttribute(jsxIdentifier, jsxContainer))
path.skip()
}
},
CallExpression(path: NodePath<CallExpression>) {
const { node } = path
const callee = node.callee
// 根据全局配置,跳过不需要提取的函数
const globalRule = StateManager.getToolConfig().globalRule
const code = nodeToCode(node)
globalRule.ignoreMethods.forEach((ignoreRule) => {
if (code.startsWith(ignoreRule)) {
path.skip()
return
}
})
// 跳过console.log的提取
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'console'
) {
path.skip()
return
}
// 无调用对象的情况,例如$t('xx')
if (callee.type === 'Identifier' && callee.name === functionName) {
path.skip()
return
}
// 有调用对象的情况,例如this.$t('xx')、i18n.$t('xx)
if (callee.type === 'MemberExpression') {
if (callee.property && callee.property.type === 'Identifier') {
if (callee.property.name === functionName) {
// 处理形如i18n.$t('xx)的情况
if (callee.object.type === 'Identifier' && callee.object.name === caller) {
path.skip()
return
}
// 处理形如this.$t('xx')的情况
if (callee.object.type === 'ThisExpression' && caller === 'this') {
path.skip()
return
}
}
}
}
},
ImportDeclaration(path: NodePath<ImportDeclaration>) {
const res = importDeclaration.match(/from ["'](.*)["']/)
const packageName = res ? res[1] : ''
if (path.node.source.value === packageName) {
context.hasImportI18n = true
}
// 不在遍历 import 过程中插入,改由 traverse 结束后统一处理(第 485 行)
// 避免在尚未遍历到已有的 i18n import 时就提前插入,导致 "Duplicate declaration" 错误
},
ArrowFunctionExpression(path: NodePath<ArrowFunctionExpression>) {
const { node } = path
// 函数组件必须在代码最外层
if (path.parentPath.scope.block.type !== 'Program') {
return
}
// 允许往react函数组件中加入自定义代码
insertSnippets(node, functionSnippets)
},
FunctionDeclaration(path: NodePath<FunctionDeclaration>) {
const { node } = path
// 函数组件必须在代码最外层
if (path.parentPath.scope.block.type !== 'Program') {
return
}
// 允许往react函数组件中加入自定义代码
insertSnippets(node as any, functionSnippets)
},
FunctionExpression(path: NodePath<FunctionExpression>) {
const { node } = path
// 函数组件必须在代码最外层
if (path.parentPath.scope.block.type !== 'Program') {
return
}
// 允许往react函数组件中加入自定义代码
insertSnippets(node, functionSnippets)
},
ObjectProperty(path: NodePath<ObjectProperty>) {
if (t.isStringLiteral(path.node.key)) {
if (includeChinese(path.node.key.value)) {
hasTransformed = true
const translationKey = Collector.add(path.node.key.value, customizeKey)
path.replaceWith(
t.objectProperty(
getReplaceValue(translationKey),
path.node.value,
true,
path.node.shorthand,
path.node.decorators
)
)
}
}
},
ObjectMethod(path: NodePath<ObjectMethod>) {
if (t.isStringLiteral(path.node.key)) {
if (includeChinese(path.node.key.value)) {
hasTransformed = true
const translationKey = Collector.add(path.node.key.value, customizeKey)
path.replaceWith(
t.objectMethod(
path.node.kind,
getReplaceValue(translationKey),
path.node.params,
path.node.body,
true,
path.node.generator,
path.node.async
)
)
}
}
},
}
}
const ast = options.parse(code)
traverse(ast, getTraverseOptions())
return ast
}
const ast = transformAST(code, options)
const result = babelGenerator(ast, {
compact: false,
retainLines: true,
})
// 文件里没有出现任何导入语句的情况
if (!context.hasImportI18n && hasTransformed) {
result.code = `${importDeclaration}\n${result.code}`
}
// 有forceImport时,即使没发生中文提取,也要在文件里加入i18n导入语句
if (!context.hasImportI18n && !hasTransformed && forceImport) {
result.code = `${importDeclaration}\n${result.code}`
}
return result
}
export default transformJs
================================================
FILE: packages/i18n-extract-cli/src/transformVue.ts
================================================
import type {
SFCScriptBlock,
SFCStyleBlock,
SFCTemplateBlock,
SFCDescriptor,
} from '@vue/compiler-sfc'
import { parse } from '@vue/compiler-sfc'
import * as htmlparser2 from 'htmlparser2'
import prettier from 'prettier'
import mustache from 'mustache'
import ejs from 'ejs'
import type { Rule, TagOrder, transformOptions } from '../types'
import { includeChinese } from './utils/includeChinese'
import log from './utils/log'
import transformJs from './transformJs'
import { initParse } from './parse'
import Collector from './collector'
import { IGNORE_REMARK } from './utils/constants'
import StateManager from './utils/stateManager'
import errorLogger from './utils/error-logger'
type Handler = (source: string, rule: Rule) => string
const COMMENT_TYPE = '!'
function parseJsSyntax(source: string, rule: Rule): string {
// html属性有可能是{xx:xx}这种对象形式,直接解析会报错,需要特殊处理。
// 先处理成temp = {xx:xx} 让babel解析,解析完再还原成{xx:xx}
let isObjectStruct = false
if (source.startsWith('{') && source.endsWith('}')) {
isObjectStruct = true
source = `temp=${source}`
}
const { code } = transformJs(source, {
rule: {
...rule,
functionName: rule.functionNameInTemplate,
caller: '',
importDeclaration: '',
},
parse: initParse(),
})
let stylizedCode = prettier.format(code, {
singleQuote: true,
semi: false,
parser: 'babel',
})
// pretter格式化后有时会多出分号
if (stylizedCode.startsWith(';')) {
stylizedCode = stylizedCode.slice(1)
}
if (isObjectStruct) {
stylizedCode = stylizedCode.replace('temp = ', '')
}
return stylizedCode.endsWith('\n') ? stylizedCode.slice(0, stylizedCode.length - 1) : stylizedCode
}
// 判断表达式是否已经转换成i18n
function hasTransformed(code: string, functionNameInTemplate: string): boolean {
if (!functionNameInTemplate) return false
// 转义函数名中的特殊正则字符,避免 \t 被解析为 tab 等问题
const escaped = functionNameInTemplate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return new RegExp(`(?:^|[^\\w])${escaped}\\(`, 'g').test(code)
}
// TODO: 需要优化,传参方式太挫
function parseTextNode(
text: string,
rule: Rule,
getReplaceValue: (translationKey: string) => string,
customizeKey: (key: string) => string
) {
let str = ''
let tokens: mustache.TemplateSpans = []
try {
tokens = mustache.parse(text)
} catch (err: any) {
errorLogger.reportTemplateError(text, err)
return text
}
for (const token of tokens) {
const type = token[0]
const value = token[1]
if (includeChinese(value)) {
if (type === 'text') {
const translationKey = Collector.add(value, customizeKey)
str += `{{${getReplaceValue(translationKey)}}}`
} else if (type === 'name') {
const source = parseJsSyntax(value, rule)
str += `{{${source}}}`
} else if (type === COMMENT_TYPE) {
// 形如{{!xxxx}}这种形式,在mustache里属于注释语法
const source = parseJsSyntax(`!${value}`, rule)
str += `{{${source}}}`
}
} else {
if (type === 'text') {
str += value
} else if (type === 'name') {
str += `{{${value}}}`
} else if (type === COMMENT_TYPE) {
// 形如{{!xxxx}}这种形式,在mustache里属于注释语法
str += `{{!${value}}}`
}
}
}
return str
}
function handleTemplate(code: string, rule: Rule): string {
let htmlString = ''
const { functionNameInTemplate, customizeKey } = rule
function getReplaceValue(translationKey: string): string {
// 表达式结构 $t('xx')
return `${functionNameInTemplate}('${translationKey}')`
}
function parseIgnoredTagAttribute(attributes: Record<string, string | undefined>): string {
let attrs = ''
for (const key in attributes) {
const attrValue = attributes[key]
if (attrValue === undefined) {
attrs += ` ${key} `
} else {
attrs += ` ${key}="${attrValue}" `
}
}
return attrs
}
function parseTagAttribute(attributes: Record<string, string | undefined>): string {
let attrs = ''
for (const key in attributes) {
const attrValue = attributes[key]
const isVueDirective = key.startsWith(':') || key.startsWith('@') || key.startsWith('v-')
if (attrValue === undefined) {
attrs += ` ${key} `
} else if (includeChinese(attrValue) && isVueDirective) {
// 如果属性值已经是 t('xxx') 或 functionName('xxx') 形式,直接保留不处理
if (hasTransformed(attrValue, functionNameInTemplate ?? '')) {
attrs += ` ${key}="${attrValue}" `
} else {
const source = parseJsSyntax(attrValue, rule)
// 处理属性类似于:xx="'xx'",这种属性值不是js表达式的情况。attrValue === source即属性值不是js表达式
// !hasTransformed()是为了排除,类似:xx="$t('xx')"这种已经转化过的情况。这种情况不需要二次处理
if (attrValue === source && !hasTransformed(source, functionNameInTemplate ?? '')) {
const translationKey = Collector.add(removeQuotes(attrValue), customizeKey)
const expression = getReplaceValue(translationKey)
attrs += ` ${key}="${expression}" `
} else {
attrs += ` ${key}="${source}" `
}
}
} else if (includeChinese(attrValue) && !isVueDirective) {
const translationKey = Collector.add(attrValue, (key, path) => {
// 属性里的$t('')转成$t(``),并把双引号转成单引号
key = key.replace(/'/g, '`').replace(/"/g, "'")
return customizeKey(key, path)
})
const expression = getReplaceValue(translationKey)
attrs += ` :${key}="${expression}" `
} else if (attrValue === '') {
// 这里key=''是因为之后还会被pretttier处理一遍,所以写死单引号没什么影响
attrs += `${key}='' `
} else {
attrs += ` ${key}="${attrValue}" `
}
}
return attrs
}
// 转义特殊字符
function escapeSpecialChar(text: string): string {
text = text.replace(/ /g, ' ')
text = text.replace(/</g, '<')
text = text.replace(/>/g, '>')
text = text.replace(/"/g, '"')
text = text.replace(/&/g, '&')
return text
}
let shouldIgnore = false // 是否忽略提取
let textNodeCache = '' // 缓存当前文本节点内容
let attrsCache: Record<string, string | undefined> = {} // 缓存当前标签的属性
const ignoreTags: string[] = [] // 记录忽略提取的标签名
const parser = new htmlparser2.Parser(
{
onopentag(tagName) {
// 处理文本节点没有被标签包裹的情况
// 如果这个标签没被忽略提取,那么就进行文本节点解析
if (!shouldIgnore) {
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey)
htmlString += text
textNodeCache = ''
}
let attrs = ''
const attributes = attrsCache
if (shouldIgnore) {
ignoreTags.push(tagName)
attrs = parseIgnoredTagAttribute(attributes)
// 重置属性缓存
attrsCache = {}
htmlString += `<${tagName} ${attrs}>`
return
}
attrs = parseTagAttribute(attributes)
// 重置属性缓存
attrsCache = {}
htmlString += `<${tagName} ${attrs}>`
},
onattribute(name, value, quote) {
if (value) {
attrsCache[name] = value
} else {
if (quote === undefined) {
attrsCache[name] = undefined
} else {
attrsCache[name] = value
}
}
},
ontext(text) {
text = escapeSpecialChar(text)
if (shouldIgnore) {
htmlString += text
return
}
textNodeCache += text
},
onclosetag(tagName, isImplied) {
// 处理文本被标签包裹的情况
// 如果这个标签没被忽略提取,那么就进行文本节点解析
if (!shouldIgnore) {
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey)
htmlString += text
textNodeCache = ''
}
// 判断是否可以取消忽略提取
if (ignoreTags.length === 0) {
shouldIgnore = false
} else {
if (ignoreTags[ignoreTags.length - 1] === tagName) {
ignoreTags.pop()
if (ignoreTags.length === 0) {
shouldIgnore = false
}
}
}
// 如果是自闭合标签
if (isImplied) {
htmlString = htmlString.slice(0, htmlString.length - 2) + '/>'
return
}
htmlString += `</${tagName}>`
},
oncomment(comment) {
// 如果注释前有文本节点,就拼接
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey)
htmlString += text
textNodeCache = ''
if (comment.includes(IGNORE_REMARK)) {
shouldIgnore = true
}
htmlString += `<!--${comment}-->`
},
},
{
lowerCaseTags: false,
recognizeSelfClosing: true,
lowerCaseAttributeNames: false,
decodeEntities: false,
}
)
parser.write(code)
parser.end()
return htmlString
}
function getComponentDecoratorPosition(source: string): number {
return source.indexOf('@Component')
}
function getExportDefaultPosition(source: string): number {
return source.indexOf('export default')
}
function handleScript(source: string, rule: Rule): string {
const lang = StateManager.getVueScriptLang().toLowerCase()
if (['ts', 'typescript', 'tsx'].includes(lang)) {
// 如果vue用了ts,按@Component装饰器进行分割
const startIndex = getComponentDecoratorPosition(source)
return combineVueScript(source.slice(0, startIndex), source.slice(startIndex), rule)
} else {
const startIndex = getExportDefaultPosition(source)
return combineVueScript(source.slice(0, startIndex), source.slice(startIndex), rule)
}
}
function combineVueScript(nonComponentCode: string, componentCode: string, rule: Rule) {
const transformOptions = {
rule: {
...rule,
functionName: rule.functionNameInScript,
},
isJsInVue: true, // 标记处理vue里的js
parse: initParse(),
}
const lang = StateManager.getVueScriptLang().toLowerCase()
const scriptContext = {}
const transformedNonComponentCode = transformJs(
nonComponentCode,
{
...transformOptions,
rule: ['ts', 'typescript', 'tsx'].includes(lang)
? StateManager.getToolConfig().rules.ts
: StateManager.getToolConfig().rules.js,
},
scriptContext
).code
const transformedComponentCode = transformJs(componentCode, transformOptions, scriptContext).code
if (transformedNonComponentCode) {
return '\n' + transformedNonComponentCode + '\n' + transformedComponentCode + '\n'
} else {
return transformedComponentCode + '\n'
}
}
function mergeCode(
tagOrder: TagOrder,
tagMap: {
template: string
script: string
style: string
}
): string {
const sourceCode = tagOrder.reduce((code, tagName) => {
return code + tagMap[tagName]
}, '')
return sourceCode
}
function removeQuotes(value: string): string {
if (['"', "'"].includes(value.charAt(0)) && ['"', "'"].includes(value.charAt(value.length - 1))) {
value = value.substring(1, value.length - 1)
}
return value
}
function getWrapperTemplate(sfcBlock: SFCTemplateBlock | SFCScriptBlock | SFCStyleBlock): string {
const { type, lang, attrs } = sfcBlock
let template = `<${type}`
if (lang) {
template += ` lang="${lang}"`
}
if ((sfcBlock as SFCScriptBlock).setup) {
template += ` setup`
}
if ((sfcBlock as SFCStyleBlock).scoped) {
template += ` scoped`
}
for (const attr in attrs) {
if (!['lang', 'scoped', 'setup'].includes(attr)) {
if (attrs[attr] === true) {
template += attr
} else {
template += ` ${attr}="${attrs[attr]}"`
}
}
}
template += `><%- code %></${type}>`
return template
}
function generateSource(
sfcBlock: SFCTemplateBlock | SFCScriptBlock,
handler: Handler,
rule: Rule
): string {
const wrapperTemplate = getWrapperTemplate(sfcBlock)
let source
try {
source = handler(sfcBlock.content, rule)
} catch (err: any) {
source = sfcBlock.content
errorLogger.reportFileError(err.message)
}
return ejs.render(wrapperTemplate, {
code: source,
})
}
function removeSnippet(
source: string,
sfcBlock: SFCTemplateBlock | SFCScriptBlock | SFCStyleBlock | null
): string {
return sfcBlock ? source.replace(sfcBlock.content, '') : source
}
// 提取文件头注释
// TODO: 这里投机取巧了一下,把标签内容清空再匹配注释。避免匹配错了。后期有好的方案再替换
function getFileComment(descriptor: SFCDescriptor): string {
const { template, script, scriptSetup, styles } = descriptor
let source = descriptor.source
source = removeSnippet(source, template)
source = removeSnippet(source, script)
source = removeSnippet(source, scriptSetup)
if (styles) {
for (const style of styles) {
source = removeSnippet(source, style)
}
}
const result = source.match(/<!--[\s\S]*?-->/)
return result ? result[0] : ''
}
function transformVue(
code: string,
options: Omit<transformOptions, 'parse'>
): {
code: string
} {
const { rule, filePath } = options
const { descriptor, errors } = parse(code)
if (errors.length > 0) {
const line = (errors[0] as any).loc.start.line
log.error(`源文件${filePath}第${line}行附近解析出现错误:`, errors[0].toString())
return {
code,
}
}
const { template, script, scriptSetup, styles } = descriptor
let templateCode = ''
let scriptCode = ''
let stylesCode = ''
const fileComment = getFileComment(descriptor)
if (template) {
templateCode = generateSource(template, handleTemplate, rule)
}
if (script) {
StateManager.setVueScriptLang(script.lang)
scriptCode = generateSource(script, handleScript, rule)
}
if (scriptSetup) {
StateManager.setVueScriptLang(scriptSetup?.lang)
scriptCode = generateSource(scriptSetup, handleScript, rule)
}
if (styles) {
for (const style of styles) {
const wrapperTemplate = getWrapperTemplate(style)
const source = style.content
stylesCode +=
ejs.render(wrapperTemplate, {
code: source,
}) + '\n'
}
}
const tagMap = {
template: templateCode,
script: scriptCode,
style: stylesCode,
}
const tagOrder = StateManager.getToolConfig().rules.vue.tagOrder
code = mergeCode(tagOrder, tagMap)
if (fileComment) {
code = fileComment + code
}
return {
code,
}
}
export default transformVue
================================================
FILE: packages/i18n-extract-cli/src/translate.ts
================================================
import fs from 'fs-extra'
import {
googleTranslate,
youdaoTranslate,
baiduTranslate,
alicloudTranslate,
} from '@ifreeovo/translate-utils'
import type { TranslateConfig, StringObject, translatorType } from '../types'
import { getAbsolutePath } from './utils/getAbsolutePath'
import log from './utils/log'
import { GOOGLE, YOUDAO, BAIDU, ALICLOUD } from './utils/constants'
import getLang from './utils/getLang'
import StateManager from './utils/stateManager'
import { saveLocaleFile } from './utils/saveLocaleFile'
import { flatObjectDeep } from './utils/flatObjectDeep'
import { spreadObject } from './utils/spreadObject'
async function translateByGoogle(
word: string,
locale: string,
options: TranslateConfig
): Promise<string> {
try {
return await googleTranslate(word, 'zh-CN', locale, options.google?.proxy)
} catch (e: any) {
if (e.name === 'TooManyRequestsError') {
log.error('翻译失败,请求超过谷歌api调用次数限制')
} else {
log.error('谷歌翻译请求出错', e)
}
return ''
}
}
async function translateByYoudao(
word: string,
locale: string,
options: TranslateConfig
): Promise<string> {
if (!options.youdao || !options.youdao?.key || !options.youdao?.secret) {
log.error('翻译失败,当前翻译器为有道,请完善youdao配置参数')
process.exit(1)
}
try {
return await youdaoTranslate(word, 'zh-CN', locale, options.youdao)
} catch (e) {
log.error('有道翻译请求出错', e)
return ''
}
}
async function translateByBaidu(
word: string,
locale: string,
options: TranslateConfig
): Promise<string> {
if (!options.baidu || !options.baidu?.key || !options.baidu?.secret) {
log.error('翻译失败,当前翻译器为百度,请完善baidu配置参数')
process.exit(1)
}
try {
return await baiduTranslate(word, 'zh', locale, options.baidu)
} catch (e) {
log.error('百度翻译请求出错', e)
return ''
}
}
async function translateByAlicloud(
word: string,
locale: string,
options: TranslateConfig
): Promise<string> {
if (!options.alicloud || !options.alicloud?.key || !options.alicloud?.secret) {
log.error('翻译失败,当前翻译器为阿里云机器翻译,请完善alicloud配置参数')
process.exit(1)
}
try {
return await alicloudTranslate(word, 'zh', locale, options.alicloud)
} catch (e) {
log.error('阿里云机器翻译请求出错', JSON.stringify(e))
return ''
}
}
export default async function (
localePath: string,
locales: string[],
oldPrimaryLang: StringObject,
options: TranslateConfig
) {
if (![GOOGLE, YOUDAO, BAIDU, ALICLOUD].includes(options.translator || '')) {
log.error('翻译失败,请确认translator参数是否配置正确')
process.exit(1)
}
log.verbose('当前使用的翻译器:', options.translator)
const primaryLangPath = getAbsolutePath(process.cwd(), localePath)
const newPrimaryLang = flatObjectDeep(getLang(primaryLangPath))
const localeFileType = StateManager.getToolConfig().localeFileType
for (const targetLocale of locales) {
log.info(`正在翻译${targetLocale}语言包`)
const reg = new RegExp(`/[A-Za-z-]+.${localeFileType}`, 'g')
const targetPath = localePath.replace(reg, `/${targetLocale}.${localeFileType}`)
const targetLocalePath = getAbsolutePath(process.cwd(), targetPath)
let oldTargetLangPack: Record<string, string> = {}
let newTargetLangPack: Record<string, string> = {}
if (fs.existsSync(targetLocalePath)) {
oldTargetLangPack = flatObjectDeep(getLang(targetLocalePath))
} else {
// 创建空的翻译文件
saveLocaleFile({}, targetLocalePath)
}
const keyList = Object.keys(newPrimaryLang)
const willTranslateText: Record<string, string> = {}
for (const key of keyList) {
// 主语言同一个key的value不变,就复用原有的翻译结果
const oldLang = flatObjectDeep(oldPrimaryLang)
const isNotChanged = oldLang[key] === newPrimaryLang[key]
if (isNotChanged && oldTargetLangPack[key]) {
newTargetLangPack[key] = oldTargetLangPack[key]
} else {
willTranslateText[key] = newPrimaryLang[key]
}
}
// 翻译新增键值对内容
const translator = new Translator({
provider: options.translator || YOUDAO,
targetLocale,
providerOptions: options,
})
const incrementalTranslation = await translator.translate(willTranslateText)
newTargetLangPack = {
...newTargetLangPack,
...incrementalTranslation,
}
const fileContent = spreadObject(newTargetLangPack)
saveLocaleFile(fileContent, targetLocalePath)
log.info(`完成${targetLocale}语言包翻译`)
}
}
type tranlateFunction = (
word: string,
locale: string,
options: TranslateConfig
) => Promise<string | Array<{ src: string; dst: string }>>
interface TranslatorConstructor {
provider: translatorType
targetLocale: string
providerOptions: TranslateConfig
}
class Translator {
#provider: tranlateFunction
#targetLocale: string
#providerOptions: TranslateConfig
#textLengthLimit = 5000
#separator = '\n' // 翻译文本拼接用的分隔符
constructor({ provider, targetLocale, providerOptions }: TranslatorConstructor) {
switch (provider) {
case YOUDAO:
this.#provider = translateByYoudao
break
case GOOGLE:
this.#provider = translateByGoogle
break
case BAIDU:
this.#provider = translateByBaidu
break
case ALICLOUD:
this.#provider = translateByAlicloud
break
}
this.#targetLocale = targetLocale
this.#providerOptions = providerOptions
this.#textLengthLimit = providerOptions.translationTextMaxLength || 5000
}
async translate(dictionary: Record<string, string>): Promise<Record<string, string>> {
const allTextArr = Object.keys(dictionary).map((key) => dictionary[key])
let restTextBundleArr = allTextArr
let startIndex = 0
const result: string[] = []
// 每轮循环,先判断key-value的字符数量
// 如果字符小于#textLengthLimit,以两倍速度递增,扩大翻译行数,以尽可能翻译更多的行数
// 如果字符大于#textLengthLimit,以两倍倍速度递减,扩小翻译行数,以尽可能翻译更多的行数
// 确定了行数后开始翻译,一直循环到翻译完所有行
while (startIndex < allTextArr.length && restTextBundleArr.length > 0) {
const maxTranslationCount = this.getMaxTranslationCount(restTextBundleArr)
const textBundleArr = allTextArr.slice(startIndex, startIndex + maxTranslationCount)
restTextBundleArr = allTextArr.slice(startIndex + maxTranslationCount)
startIndex = startIndex + maxTranslationCount
const [res] = await Promise.all([
this.#provider(
textBundleArr.join(this.#separator), // 文本中可能有逗号,为了防止后面分割字符出错,使用\\$代替逗号
this.#targetLocale,
this.#providerOptions
),
new Promise((resolve) => setTimeout(resolve, 1000)), // 有道翻译接口限制每秒1次请求
])
let resArr: string[]
if (typeof res === 'object') {
resArr = res.map((item) => item.dst)
} else {
resArr = res.split(this.#separator)
}
result.push(...resArr)
}
const incrementalTranslation: Record<string, string> = {}
Object.keys(dictionary).forEach((key, index) => {
// 翻译后有可能字符串前后会多出一个空格,这里做一下过滤
let translatedText = result[index] || ''
if (!dictionary[key].startsWith(' ') && translatedText.startsWith(' ')) {
translatedText = translatedText.slice(1)
}
if (!dictionary[key].endsWith(' ') && translatedText.endsWith(' ')) {
translatedText = translatedText.slice(0, -1)
}
incrementalTranslation[key] = translatedText
})
return incrementalTranslation
}
// 二分法查找最大翻译行数,不使用递归,避免异常情况下栈溢出
getMaxTranslationCount(textArr: string[]): number {
const textNum = textArr.length
let upper = textNum
let lower = 1
let pointer = 1
while (upper - lower > 1) {
pointer = Math.floor((upper + lower) / 2)
const textBundleArr = textArr.slice(0, pointer)
const textBundleLength = textBundleArr.join(this.#separator).length
if (textBundleLength <= this.#textLengthLimit) {
lower = Math.max(lower, pointer)
} else {
upper = Math.min(upper, pointer)
}
}
return Math.max(pointer, 1)
}
}
================================================
FILE: packages/i18n-extract-cli/src/utils/assertType.ts
================================================
export function isObject(obj: unknown): obj is object {
return Object.prototype.toString.call(obj) === '[object Object]'
}
================================================
FILE: packages/i18n-extract-cli/src/utils/constants.ts
================================================
export const IGNORE_REMARK = 'i18n-ignore'
export const GOOGLE = 'google'
export const YOUDAO = 'youdao'
export const BAIDU = 'baidu'
export const CONFIG_FILE_NAME = 'i18n.config.js'
export const ALICLOUD = 'alicloud'
================================================
FILE: packages/i18n-extract-cli/src/utils/error-logger.ts
================================================
import log from './log'
class ErrorLogger {
private _filerPath = ''
private _errorList: string[] = []
private _delimiter = '\n=======================================\n'
setFilePath(path: string) {
this._filerPath = path
}
reportFileError(errorMessage: string) {
this._errorList.push(
`${this._delimiter}解析${this._filerPath}文件时遇到未知错误:\n${errorMessage}\n\n已跳过文件转换,请手动处理${this._delimiter}`
)
}
reportTemplateError(originSource: string, errorMessage: string) {
this._errorList.push(
`${this._delimiter}解析${this._filerPath}文件里的模版内容\n${originSource}\n时遇到未知错误:\n${errorMessage}\n\n已跳过此段模版转换,请手动处理${this._delimiter}`
)
}
printErrors() {
if (this._errorList.length === 0) {
return
}
this._errorList.forEach((err) => {
log.error(err)
})
log.error(
`总计出现 ${this._errorList.length} 处错误,需要手动处理。建议去issue里反馈相关问题https://github.com/IFreeOvO/i18n-cli/issues,以协助作者完善代码♪(・ω・)ノ`
)
}
}
export default new ErrorLogger()
================================================
FILE: packages/i18n-extract-cli/src/utils/escapeQuotes.ts
================================================
export function escapeQuotes(value: string): string {
return value.replace(/'/g, '_#_').replace(/"/g, '_##_')
}
================================================
FILE: packages/i18n-extract-cli/src/utils/excelUtil.ts
================================================
import xlsx from 'node-xlsx'
import StateManager from './stateManager'
export function getExcelHeader(): string[] {
const { locales } = StateManager.getToolConfig()
const header = ['字典key', 'zh-CN']
for (const locale of locales) {
header.push(locale)
}
return header
}
export function buildExcel(headers: string[], data: string[][], name: string): Buffer {
const sheetOptions: Record<string, any> = {}
sheetOptions['!cols'] = []
headers.forEach(() => {
sheetOptions['!cols'].push({
wch: 50, // 表格列宽
})
})
data.unshift(headers)
const buffer = xlsx.build([{ options: {}, name, data }], { sheetOptions })
return buffer
}
================================================
FILE: packages/i18n-extract-cli/src/utils/flatObjectDeep.ts
================================================
import type { StringObject } from '../../types'
import { isObject } from './assertType'
/**
* @example
* 将{a: {bb: 1}} 转成 {'a.bb': 1}
*/
export function flatObjectDeep(data: StringObject): Record<string, string> {
const keyValueMap: Record<string, string> = {}
function collectMap(obj: StringObject, upperKey?: string) {
Object.keys(obj).forEach((key) => {
const currentKey = upperKey ? `${upperKey}.${key}` : key
const value = obj[key]
if (isObject(value)) {
collectMap(value, currentKey)
} else {
keyValueMap[currentKey] = value
}
})
}
collectMap(data)
return keyValueMap
}
================================================
FILE: packages/i18n-extract-cli/src/utils/getAbsolutePath.ts
================================================
import path from 'path'
import slash from 'slash'
export function getAbsolutePath(...paths: string[]) {
return slash(path.resolve(...paths))
}
================================================
FILE: packages/i18n-extract-cli/src/utils/getLang.ts
================================================
import fs from 'fs-extra'
import StateManager from './stateManager'
import log from './log'
function getLang(langPath: string): Record<string, string> {
const localeFileType = StateManager.getToolConfig().localeFileType
try {
if (localeFileType === 'json') {
// json文件直接require拿不到文件内容,故改成下面写法
const content = fs.readFileSync(langPath).toString()
if (!content) {
return {}
}
return JSON.parse(content)
} else {
if (!fs.existsSync(langPath)) {
log.error(`文件${langPath}不存在`)
return {}
}
// TODO: 因为默认生成的是esm的js文件,先简单处理下。后期还是兼容esm和commonjs比较好
const str = fs.readFileSync(langPath).toString().replace('export default', 'return')
const content = new Function(str)()
return content
}
} catch (e) {
log.error(`读取文件路径${langPath}出错:`, e)
return {}
}
}
export default getLang
================================================
FILE: packages/i18n-extract-cli/src/utils/getLocaleDir.ts
================================================
import StateManager from './stateManager'
export function getLocaleDir(): string {
const { localeFileType, localePath } = StateManager.getToolConfig()
const reg = new RegExp(`/[A-Za-z-]+.${localeFileType}`, 'g')
const localeDirPath = localePath.replace(reg, '')
return localeDirPath
}
================================================
FILE: packages/i18n-extract-cli/src/utils/includeChinese.ts
================================================
export function includeChinese(code: string) {
return new RegExp('[\u{4E00}-\u{9FFF}]', 'g').test(code)
}
================================================
FILE: packages/i18n-extract-cli/src/utils/initConfig.ts
================================================
import fs from 'fs-extra'
import merge from 'lodash/merge'
import { deepPartial, CommandOptions, Config } from '../../types/index'
import defaultConfig from '../default.config'
import { getAbsolutePath } from './getAbsolutePath'
import log from './log'
function getUserConfig(configFile?: string): deepPartial<Config> {
if (configFile) {
const configPath = getAbsolutePath(process.cwd(), configFile)
if (!fs.existsSync(configPath)) {
log.warning('配置文件路径不存在,请重新设置指令参数 -c 或 --config-file 的值')
return {}
} else {
const config = require(configPath)
// prettier为true时删除,是为了走默认的配置
if (config.prettier === true) {
delete config.prettier
}
return config
}
} else {
return {}
}
}
export function getI18nConfig(options: CommandOptions): Config {
const userConfig = getUserConfig(options.configFile)
const config = merge(defaultConfig, options, userConfig)
return config
}
================================================
FILE: packages/i18n-extract-cli/src/utils/isDirectory.ts
================================================
import { statSync } from 'node:fs'
export default function isDirectory(filePath: string): boolean {
return statSync(filePath).isDirectory()
}
================================================
FILE: packages/i18n-extract-cli/src/utils/log.ts
================================================
import chalk from 'chalk'
const log = {
info: (msg: string) => console.log('\n' + chalk.cyan(msg)),
warning: (msg: string) => console.log('\n' + chalk.yellow(msg)),
success: (msg: string) => console.log('\n' + chalk.green(msg)),
error: (msg1: unknown, msg2: unknown = '') =>
console.log('\n' + chalk.red(msg1), chalk.red(msg2)),
verbose: (label: string, msg: unknown = '') =>
process.env.CLI_VERBOSE && console.log('\n' + chalk.gray(label), msg),
debug: (label: string, msg: unknown = '') =>
process.env.CLI_DEBUG && console.log('\n' + chalk.magenta(label), msg),
}
export default log
================================================
FILE: packages/i18n-extract-cli/src/utils/removeLineBreaksInTag.ts
================================================
export function removeLineBreaksInTag(str: string): string {
return str.replace(/([\r\n]+\s*)+/g, '')
}
================================================
FILE: packages/i18n-extract-cli/src/utils/saveLocaleFile.ts
================================================
import fs from 'fs-extra'
import type { StringObject } from '../../types'
import StateManager from './stateManager'
import { serializeCode } from './serializeCode'
export function saveLocaleFile(locale: StringObject, path: string) {
const { localeFileType } = StateManager.getToolConfig()
if (!fs.existsSync(path)) {
fs.ensureFileSync(path)
}
if (localeFileType === 'json') {
fs.writeFileSync(path, JSON.stringify(locale, null, 2), 'utf8')
} else {
fs.writeFileSync(path, serializeCode(locale), 'utf8')
}
}
================================================
FILE: packages/i18n-extract-cli/src/utils/serializeCode.ts
================================================
import prettier from 'prettier'
import serialize from 'serialize-javascript'
export function serializeCode(source: unknown, isESModule = true) {
const exportStatement = isESModule ? 'export default' : 'module.exports ='
const code = `
${exportStatement} ${serialize(source, {
unsafe: true,
})}
`
const stylizedCode = prettier.format(code, {
semi: false,
singleQuote: true,
parser: 'babel',
})
return stylizedCode
}
================================================
FILE: packages/i18n-extract-cli/src/utils/spreadObject.ts
================================================
import type { StringObject } from '../../types'
import set from 'lodash/set'
export function spreadObject(obj: Record<string, string>): StringObject {
const newObject: StringObject = {}
Object.keys(obj).forEach((key) => {
const keyList = key.split('.')
set(newObject, keyList, obj[key])
})
return newObject
}
================================================
FILE: packages/i18n-extract-cli/src/utils/stateManager.ts
================================================
import type { Config } from '../../types'
import defaultConfig from '../default.config'
class StateManager {
private static _instance: StateManager
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
private toolConfig: Config = defaultConfig
private currentSourcePath = ''
private vueScriptLang = 'js'
static getInstance() {
if (!this._instance) {
this._instance = new StateManager()
}
return this._instance
}
setToolConfig(config: Config): void {
this.toolConfig = config
}
getToolConfig(): Config {
return this.toolConfig
}
setCurrentSourcePath(path: string): void {
this.currentSourcePath = path
}
getCurrentSourcePath(): string {
return this.currentSourcePath
}
setVueScriptLang(lang?: string): void {
this.vueScriptLang = lang || 'js'
}
getVueScriptLang(): string {
return this.vueScriptLang
}
}
export default StateManager.getInstance()
================================================
FILE: packages/i18n-extract-cli/tsconfig.json
================================================
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true,
"sourceMap": true
},
"include": ["src"]
}
================================================
FILE: packages/i18n-extract-cli/types/index.d.ts
================================================
import type { ParseResult } from '@babel/core'
import type { Options } from 'prettier'
export type deepPartial<T extends object> = {
[K in keyof T]?: T[K] extends object ? deepPartial<T[K]> : T[K]
}
export interface GlobalRule {
ignoreMethods: string[]
}
export interface transformOptions {
rule: Rule
parse: (code: string) => ParseResult
isJsInVue?: boolean
filePath?: string
}
export type CustomizeKey = (key: string, path?: string) => string
export type GetCustomSlot = (slotValue: string) => string
export interface Rule {
caller: string
functionName?: string
importDeclaration: string
customizeKey: CustomizeKey
customSlot: GetCustomSlot
// TODO: 可优化成根据范型动态生成规则
functionSnippets?: string
forceImport?: boolean
functionNameInTemplate?: string
functionNameInScript?: string
}
export type Rules = {
[k in keyof Config['rules']]: Config['rules'][k]
}
export type FileExtension = 'js' | 'ts' | 'cjs' | 'mjs' | 'jsx' | 'tsx' | 'vue'
export type StringObject = {
[key: string]: string | StringObject
}
export type AdjustKeyMap = (
allKeyValue: StringObject,
currentPathKeyValue: Record<string, string>,
path
) => StringObject
export interface YoudaoConfig {
key?: string
secret?: string
}
export interface BaiduConfig {
key?: string
secret?: string
}
interface AlicloudConfig {
key?: string
secret?: string
}
export type translatorType = 'google' | 'youdao' | 'baidu' | 'alicloud'
export interface TranslateConfig {
translator?: translatorType
google?: {
proxy?: string
}
youdao?: YoudaoConfig
baidu?: BaiduConfig
alicloud?: AlicloudConfig
translationTextMaxLength?: number
}
export type PrettierConfig = Options | boolean
export type TagOrder = Array<'template' | 'script' | 'style'>
export type Config = {
input: string
output: string
localePath: string
localeFileType: string
locales: string[]
exclude: string[]
rules: {
js: Rule
ts: Rule
cjs: Rule
mjs: Rule
tsx: Rule & {
functionSnippets: string
}
jsx: Rule & {
functionSnippets: string
}
vue: Rule & {
tagOrder: TagOrder
}
}
prettier: PrettierConfig
skipExtract: boolean
skipTranslate: boolean
translationTextMaxLength: number
incremental: boolean
globalRule: GlobalRule
excelPath: string
exportExcel: boolean
adjustKeyMap?: AdjustKeyMap
} & TranslateConfig
export interface CommandOptions {
input?: string
output?: string
localePath?: string
configFile?: string
locales?: string[]
verbose?: boolean
skipExtract?: boolean
skipTranslate?: boolean
excelPath?: string
exportExcel?: boolean
}
================================================
FILE: packages/translate-utils/CHANGELOG.md
================================================
# @ifreeovo/translate-utils
## 1.2.1
### Patch Changes
- 19f80ae: 阿里云机器翻译语言代码未兼容
## 1.2.0
### Minor Changes
- 4199f6b: 支持阿里云翻译
## 1.1.0
### Minor Changes
- ee2bcec: 支持百度翻译
## 1.0.0
### Major Changes
- 73a2b9c: 支持有道翻译和谷歌翻译
================================================
FILE: packages/translate-utils/README.md
================================================
# @ifreeovo/translate-utils
一个翻译工具函数库。支持有道,谷歌。
## install
```
npm i @ifreeovo/translate-utils
```
## api
### googleTranslate
```ts
declare function googleTranslate(
word: string, // 待翻译文本
originLang: string, // 源语言
targetLang: string, // 目标语言
proxy: string | undefined // 代理地址
): Promise<string>
```
例子
```js
const res = await googleTranslate('翻译内容', 'zh-CN', 'en-US', 'socks://127.0.0.1:1080')
```
### youdaoTranslate
```ts
interface YoudaoConfig {
key?: string // 有道词典appKey
secret?: string // 有道词典appSecret
}
declare function youdaoTranslate(
word: string, // 待翻译文本
originLang: string, // 源语言
targetLang: string, // 目标语言
option: YoudaoConfig // 有道词典配置
): Promise<string>
```
例子
```js
const res = await googleTranslate('翻译内容', 'zh-CN', 'en-US', {
key: '2d8e89a6fd072117',
secret: 'HiX7rGmYRad3ISMLYexRLfpkJi2taMPh',
})
```
================================================
FILE: packages/translate-utils/index.d.ts
================================================
interface YoudaoConfig {
key?: string
secret?: string
}
interface BaiduConfig {
key?: string
secret?: string
}
export interface AlicloudConfig {
key?: string
secret?: string
}
declare namespace TranslateUtils {
export declare function googleTranslate(
word: string,
originLang: string,
targetLang: string,
proxy: string | undefined
): Promise<string>
export declare function youdaoTranslate(
word: string,
originLang: string,
targetLang: string,
option: YoudaoConfig
): Promise<string>
export declare function baiduTranslate(
word: string,
originLang: string,
targetLang: string,
option: BaiduConfig
): Promise<string>
export declare function alicloudTranslate(
word: string,
originLang: string,
targetLang: string,
option: AlicloudConfig
): Promise<string>
}
export = TranslateUtils
================================================
FILE: packages/translate-utils/package.json
================================================
{
"name": "@ifreeovo/translate-utils",
"version": "1.2.1",
"description": "翻译工具函数,支持有道,谷歌,百度,阿里机器翻译",
"publishConfig": {
"access": "public"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "index.d.ts",
"scripts": {
"dev:cjs": "tsc --watch -p ./tsconfig.cjs.json",
"dev:esm": "tsc --watch -p ./tsconfig.esm.json",
"build": "rimraf dist && tsc --build ./tsconfig.cjs.json && tsc --build ./tsconfig.esm.json"
},
"keywords": [
"translate"
],
"author": "IFreeOvO",
"license": "MIT",
"files": [
"dist",
"index.d.ts",
"README.md"
],
"dependencies": {
"@alicloud/alimt20181012": "^1.3.0",
"@alicloud/openapi-client": "^0.4.9",
"@alicloud/tea-util": "^1.4.8",
"@vitalets/google-translate-api": "9.0.0",
"got": "11.8.5",
"md5": "^2.3.0",
"proxy-agent": "^5.0.0",
"qs": "^6.11.0"
},
"devDependencies": {
"@types/md5": "^2.3.2",
"@types/qs": "^6.9.7",
"rimraf": "^3.0.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/IFreeOvO/i18n-cli.git",
"directory": "packages/translate-utils"
},
"bugs": {
"url": "https://github.com/IFreeOvO/i18n-cli/issues"
},
"homepage": "https://github.com/IFreeOvO/i18n-cli/tree/master/packages/translate-utils"
}
================================================
FILE: packages/translate-utils/src/alicloud.ts
================================================
import alimt20181012, * as $alimt20181012 from '@alicloud/alimt20181012'
import { Config } from '@alicloud/openapi-client'
import { RuntimeOptions } from '@alicloud/tea-util'
export interface AlicloudConfig {
key?: string
secret?: string
}
export function createClient(key = '', secret = ''): alimt20181012 {
const config = new Config({
accessKeyId: key,
accessKeySecret: secret,
endpoint: 'mt.aliyuncs.com',
})
return new alimt20181012(config)
}
function toAlicloudLangCode(locale: string): string {
const lower = locale.toLowerCase()
if (lower === 'zh-tw' || lower === 'zh-cht' || lower === 'zh-hant') {
return 'zht'
}
return lower.split('-')[0]
}
export async function alicloudTranslate(
word: string,
originLang: string,
targetLang: string,
option: AlicloudConfig
): Promise<string> {
const key = option.key
const secret = option.secret
const client = createClient(key, secret)
const translateGeneralRequest = new $alimt20181012.TranslateGeneralRequest({
formatType: 'text',
sourceLanguage: toAlicloudLangCode(originLang),
targetLanguage: toAlicloudLangCode(targetLang),
scene: 'general',
sourceText: word,
})
const runtime = new RuntimeOptions({})
return new Promise((resolve, reject) => {
client
.translateGeneralWithOptions(translateGeneralRequest, runtime)
.then((resp) => {
if (resp.statusCode === 200) {
if (resp.body?.code === 200) {
resolve(resp.body?.data?.translated ?? '')
} else {
reject(resp.body)
}
} else {
reject(resp.body)
}
})
.catch((e: any) => {
reject(e)
})
})
}
================================================
FILE: packages/translate-utils/src/baidu.ts
================================================
import md5 from 'md5'
import qs from 'qs'
import { Response } from 'got/dist/source/core/index'
const got = require('got')
export interface BaiduConfig {
key?: string
secret?: string
}
export async function baiduTranslate(
word: string,
originLang: string,
targetLang: string,
option: BaiduConfig
): Promise<Array<{ src: string; dst: string }>> {
const key = option.key
const secret = option.secret
const salt = Math.random()
const sign = md5(key + word + salt + secret)
const baseUrl = 'https://fanyi-api.baidu.com/api/trans/vip/translate'
const params = {
from: originLang,
to: targetLang.split('-')[0],
appid: key,
salt,
sign,
q: word,
}
const url = `${baseUrl}?${qs.stringify(params)}`
return new Promise((resolve, reject) => {
got
.get(url)
.then(({ body }: Response<string>) => {
const res = JSON.parse(body)
if (!res.error_code) {
resolve(res.trans_result)
} else {
reject(body)
}
})
.catch((e: any) => {
reject(e)
})
})
}
================================================
FILE: packages/translate-utils/src/google.ts
================================================
import ProxyAgent from 'proxy-agent'
import { translate } from '@vitalets/google-translate-api'
export async function googleTranslate(
word: string,
originLang: string,
targetLang: string,
proxy: string | undefined
): Promise<string> {
return new Promise((resolve, reject) => {
const agent = ProxyAgent(proxy)
translate(word, {
fetchOptions: { agent },
from: originLang,
to: targetLang,
})
.then((res) => {
resolve(res.text || '')
})
.catch((e: any) => {
reject(e)
})
})
}
================================================
FILE: packages/translate-utils/src/index.ts
================================================
export { googleTranslate } from './google'
export { youdaoTranslate } from './youdao'
export { baiduTranslate } from './baidu'
export { alicloudTranslate } from './alicloud'
================================================
FILE: packages/translate-utils/src/youdao.ts
================================================
import md5 from 'md5'
import qs from 'qs'
import { Response } from 'got/dist/source/core/index'
const got = require('got')
export interface YoudaoConfig {
key?: string
secret?: string
}
export async function youdaoTranslate(
word: string,
originLang: string,
targetLang: string,
option: YoudaoConfig
): Promise<string> {
const key = option.key
const secret = option.secret
const salt = Math.random()
const sign = md5(key + word + salt + secret)
const baseUrl = 'https://openapi.youdao.com/api'
const params = {
from: originLang,
to: targetLang,
appKey: key,
salt,
sign,
q: word,
}
const url = `${baseUrl}?${qs.stringify(params)}`
return new Promise((resolve, reject) => {
got
.get(url)
.then(({ body }: Response<string>) => {
const res = JSON.parse(body)
if (res.errorCode === '0') {
resolve(res.translation[0])
} else {
reject(body)
}
})
.catch((e: any) => {
reject(e)
})
})
}
================================================
FILE: packages/translate-utils/tsconfig.cjs.json
================================================
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/cjs",
"rootDir": "./src",
"removeComments": true
},
"include": ["src", "index.d.ts"],
"module": "commonjs"
}
================================================
FILE: packages/translate-utils/tsconfig.esm.json
================================================
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/esm",
"rootDir": "./src",
"removeComments": true
},
"include": ["src"],
"module": "es6"
}
================================================
FILE: patches/mustache@4.2.0.patch
================================================
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index b1f72d0a37364c5a4b6d5ec394ca69a9191421ab..0000000000000000000000000000000000000000
diff --git a/mustache.js b/mustache.js
index 62fac4d632620f0f407d7dbf76c3d36d14468b45..387e42a6f26f8d2a47d39d0efc9a05f3f9d031a8 100644
--- a/mustache.js
+++ b/mustache.js
@@ -84,7 +84,7 @@
var spaceRe = /\s+/;
var equalsRe = /\s*=/;
var curlyRe = /\s*\}/;
- var tagRe = /#|\^|\/|>|\{|&|=|!/;
+ var tagRe = /#|\^|>|\{|&|=|!/;
/**
* Breaks up the given `template` string into a tree of tokens. If the `tags`
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'packages/*'
================================================
FILE: tsconfig.base.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"declaration": true,
"skipLibCheck": true,
"incremental": true,
"lib": ["esnext", "dom"]
},
"exclude": ["**/node_modules", "**/examples", "**/dist", "**/*.test.ts"]
}
================================================
FILE: turbo.json
================================================
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev:cjs": {
"cache": false,
"persistent": true
},
"dev:esm": {
"cache": false,
"persistent": true
},
"dev": {
"dependsOn": ["dev:cjs", "dev:esm"],
"cache": false,
"persistent": true
},
"test": {}
},
"globalDependencies": ["tsconfig.base.json", "tsconfig.json"]
}
gitextract_3o6dmje6/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── issue-close.yml │ ├── issue-inactive.yml │ ├── issue-remove-inactive.yml │ └── release.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── commitlint.config.js ├── examples/ │ ├── react-demo/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── .umirc.ts │ │ ├── README.md │ │ ├── i18n.config.js │ │ ├── mock/ │ │ │ └── .gitkeep │ │ ├── package.json │ │ ├── src/ │ │ │ ├── pages/ │ │ │ │ └── index.tsx │ │ │ └── utils/ │ │ │ └── i18n.ts │ │ ├── tsconfig.json │ │ └── typings.d.ts │ └── vue-demo/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── style.css │ │ ├── utils/ │ │ │ └── i18n.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages/ │ ├── i18n-extract-cli/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── bin/ │ │ │ └── index.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── transformJs.test.ts │ │ │ ├── collector.ts │ │ │ ├── commands/ │ │ │ │ ├── init/ │ │ │ │ │ └── index.ts │ │ │ │ └── loadExcel/ │ │ │ │ └── index.ts │ │ │ ├── core.ts │ │ │ ├── default.config.ts │ │ │ ├── exportExcel.ts │ │ │ ├── index.ts │ │ │ ├── parse.ts │ │ │ ├── transform.ts │ │ │ ├── transformJs.ts │ │ │ ├── transformVue.ts │ │ │ ├── translate.ts │ │ │ └── utils/ │ │ │ ├── assertType.ts │ │ │ ├── constants.ts │ │ │ ├── error-logger.ts │ │ │ ├── escapeQuotes.ts │ │ │ ├── excelUtil.ts │ │ │ ├── flatObjectDeep.ts │ │ │ ├── getAbsolutePath.ts │ │ │ ├── getLang.ts │ │ │ ├── getLocaleDir.ts │ │ │ ├── includeChinese.ts │ │ │ ├── initConfig.ts │ │ │ ├── isDirectory.ts │ │ │ ├── log.ts │ │ │ ├── removeLineBreaksInTag.ts │ │ │ ├── saveLocaleFile.ts │ │ │ ├── serializeCode.ts │ │ │ ├── spreadObject.ts │ │ │ └── stateManager.ts │ │ ├── tsconfig.json │ │ └── types/ │ │ └── index.d.ts │ └── translate-utils/ │ ├── CHANGELOG.md │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src/ │ │ ├── alicloud.ts │ │ ├── baidu.ts │ │ ├── google.ts │ │ ├── index.ts │ │ └── youdao.ts │ ├── tsconfig.cjs.json │ └── tsconfig.esm.json ├── patches/ │ └── mustache@4.2.0.patch ├── pnpm-workspace.yaml ├── tsconfig.base.json └── turbo.json
SYMBOL INDEX (143 symbols across 39 files)
FILE: examples/react-demo/src/pages/index.tsx
function HomePage (line 4) | function HomePage() {
FILE: examples/react-demo/src/utils/i18n.ts
function t (line 2) | function t(key: string, params: Record<string, string> = {}) {
FILE: packages/i18n-extract-cli/bin/index.js
function checkNodeVersion (line 11) | function checkNodeVersion() {
FILE: packages/i18n-extract-cli/src/__tests__/transformJs.test.ts
function getDefaultOptions (line 8) | function getDefaultOptions(): transformOptions {
function resetCollector (line 22) | function resetCollector() {
FILE: packages/i18n-extract-cli/src/collector.ts
class Collector (line 6) | class Collector {
method constructor (line 9) | private constructor() {}
method getInstance (line 11) | static getInstance() {
method setCurrentCollectorPath (line 25) | setCurrentCollectorPath(path: string) {
method getCurrentCollectorPath (line 29) | getCurrentCollectorPath() {
method add (line 33) | add(originalText: string, customizeKeyFn: CustomizeKey) {
method getCurrentFileKeyMap (line 46) | getCurrentFileKeyMap(): Record<string, string> {
method resetCurrentFileKeyMap (line 50) | resetCurrentFileKeyMap() {
method getKeyMap (line 54) | getKeyMap(): StringObject {
method setKeyMap (line 58) | setKeyMap(value: StringObject) {
method resetCountOfAdditions (line 62) | resetCountOfAdditions() {
method getCountOfAdditions (line 66) | getCountOfAdditions(): number {
FILE: packages/i18n-extract-cli/src/commands/init/index.ts
function execInit (line 7) | function execInit() {
FILE: packages/i18n-extract-cli/src/commands/loadExcel/index.ts
function getLangList (line 12) | function getLangList(locales: string[], rows: string[][]): StringObject[] {
function execLoadExcel (line 31) | function execLoadExcel(options: CommandOptions) {
FILE: packages/i18n-extract-cli/src/core.ts
type InquirerResult (line 27) | interface InquirerResult {
function getPathFromInput (line 34) | function getPathFromInput(input: string, exclude: string[]) {
function getSourceFilePaths (line 52) | function getSourceFilePaths(input: string, exclude: string[]): string[] {
function saveLocale (line 68) | function saveLocale(localePath: string) {
function getPrettierParser (line 84) | function getPrettierParser(ext: string): string {
function getOutputPath (line 96) | function getOutputPath(input: string, output: string, sourceFilePath: st...
function formatInquirerResult (line 108) | function formatInquirerResult(answers: InquirerResult): TranslateConfig {
function getTranslationConfig (line 143) | async function getTranslationConfig() {
function formatCode (line 255) | function formatCode(code: string, ext: string, prettierConfig: PrettierC...
FILE: packages/i18n-extract-cli/src/default.config.ts
function getCustomizeKey (line 5) | function getCustomizeKey(key: string, path?: string): string {
function getCustomSlot (line 9) | function getCustomSlot(slotValue: string): string {
function getCommonRule (line 13) | function getCommonRule(): Rule {
method adjustKeyMap (line 68) | adjustKeyMap(allKeyValue, currentFileKeyMap, currentFilePath) {
FILE: packages/i18n-extract-cli/src/exportExcel.ts
function exportExcel (line 11) | function exportExcel() {
FILE: packages/i18n-extract-cli/src/index.ts
function enhanceErrorMessages (line 64) | function enhanceErrorMessages() {
function suggestCommands (line 79) | function suggestCommands(unknownOption: string) {
FILE: packages/i18n-extract-cli/src/parse.ts
function langToExtension (line 10) | function langToExtension(lang = 'js') {
function getSourceFileName (line 28) | function getSourceFileName() {
function initParse (line 39) | function initParse() {
FILE: packages/i18n-extract-cli/src/transform.ts
function transform (line 7) | function transform(
FILE: packages/i18n-extract-cli/src/transformJs.ts
type TemplateParams (line 36) | type TemplateParams = {
type Context (line 45) | interface Context {
function getObjectExpression (line 49) | function getObjectExpression(obj: TemplateParams): ObjectExpression {
function isPropNode (line 66) | function isPropNode(path: NodePath<StringLiteral>): boolean {
function getStringLiteral (line 114) | function getStringLiteral(value: string): StringLiteral {
function nodeToCode (line 123) | function nodeToCode(node: Node): string {
function insertSnippets (line 128) | function insertSnippets(node: ArrowFunctionExpression | FunctionExpressi...
function pushStatement (line 151) | function pushStatement(
function transformJs (line 174) | function transformJs(
FILE: packages/i18n-extract-cli/src/transformVue.ts
type Handler (line 22) | type Handler = (source: string, rule: Rule) => string
constant COMMENT_TYPE (line 24) | const COMMENT_TYPE = '!'
function parseJsSyntax (line 26) | function parseJsSyntax(source: string, rule: Rule): string {
function hasTransformed (line 62) | function hasTransformed(code: string, functionNameInTemplate: string): b...
function parseTextNode (line 70) | function parseTextNode(
function handleTemplate (line 114) | function handleTemplate(code: string, rule: Rule): string {
function getComponentDecoratorPosition (line 295) | function getComponentDecoratorPosition(source: string): number {
function getExportDefaultPosition (line 299) | function getExportDefaultPosition(source: string): number {
function handleScript (line 303) | function handleScript(source: string, rule: Rule): string {
function combineVueScript (line 315) | function combineVueScript(nonComponentCode: string, componentCode: strin...
function mergeCode (line 344) | function mergeCode(
function removeQuotes (line 358) | function removeQuotes(value: string): string {
function getWrapperTemplate (line 366) | function getWrapperTemplate(sfcBlock: SFCTemplateBlock | SFCScriptBlock ...
function generateSource (line 392) | function generateSource(
function removeSnippet (line 410) | function removeSnippet(
function getFileComment (line 419) | function getFileComment(descriptor: SFCDescriptor): string {
function transformVue (line 434) | function transformVue(
FILE: packages/i18n-extract-cli/src/translate.ts
function translateByGoogle (line 18) | async function translateByGoogle(
function translateByYoudao (line 35) | async function translateByYoudao(
function translateByBaidu (line 52) | async function translateByBaidu(
function translateByAlicloud (line 69) | async function translateByAlicloud(
type tranlateFunction (line 148) | type tranlateFunction = (
type TranslatorConstructor (line 154) | interface TranslatorConstructor {
class Translator (line 159) | class Translator {
method constructor (line 166) | constructor({ provider, targetLocale, providerOptions }: TranslatorCon...
method translate (line 186) | async translate(dictionary: Record<string, string>): Promise<Record<st...
method getMaxTranslationCount (line 235) | getMaxTranslationCount(textArr: string[]): number {
FILE: packages/i18n-extract-cli/src/utils/assertType.ts
function isObject (line 1) | function isObject(obj: unknown): obj is object {
FILE: packages/i18n-extract-cli/src/utils/constants.ts
constant IGNORE_REMARK (line 1) | const IGNORE_REMARK = 'i18n-ignore'
constant GOOGLE (line 2) | const GOOGLE = 'google'
constant YOUDAO (line 3) | const YOUDAO = 'youdao'
constant BAIDU (line 4) | const BAIDU = 'baidu'
constant CONFIG_FILE_NAME (line 5) | const CONFIG_FILE_NAME = 'i18n.config.js'
constant ALICLOUD (line 6) | const ALICLOUD = 'alicloud'
FILE: packages/i18n-extract-cli/src/utils/error-logger.ts
class ErrorLogger (line 2) | class ErrorLogger {
method setFilePath (line 9) | setFilePath(path: string) {
method reportFileError (line 13) | reportFileError(errorMessage: string) {
method reportTemplateError (line 19) | reportTemplateError(originSource: string, errorMessage: string) {
method printErrors (line 25) | printErrors() {
FILE: packages/i18n-extract-cli/src/utils/escapeQuotes.ts
function escapeQuotes (line 1) | function escapeQuotes(value: string): string {
FILE: packages/i18n-extract-cli/src/utils/excelUtil.ts
function getExcelHeader (line 4) | function getExcelHeader(): string[] {
function buildExcel (line 13) | function buildExcel(headers: string[], data: string[][], name: string): ...
FILE: packages/i18n-extract-cli/src/utils/flatObjectDeep.ts
function flatObjectDeep (line 8) | function flatObjectDeep(data: StringObject): Record<string, string> {
FILE: packages/i18n-extract-cli/src/utils/getAbsolutePath.ts
function getAbsolutePath (line 4) | function getAbsolutePath(...paths: string[]) {
FILE: packages/i18n-extract-cli/src/utils/getLang.ts
function getLang (line 5) | function getLang(langPath: string): Record<string, string> {
FILE: packages/i18n-extract-cli/src/utils/getLocaleDir.ts
function getLocaleDir (line 3) | function getLocaleDir(): string {
FILE: packages/i18n-extract-cli/src/utils/includeChinese.ts
function includeChinese (line 1) | function includeChinese(code: string) {
FILE: packages/i18n-extract-cli/src/utils/initConfig.ts
function getUserConfig (line 9) | function getUserConfig(configFile?: string): deepPartial<Config> {
function getI18nConfig (line 28) | function getI18nConfig(options: CommandOptions): Config {
FILE: packages/i18n-extract-cli/src/utils/isDirectory.ts
function isDirectory (line 3) | function isDirectory(filePath: string): boolean {
FILE: packages/i18n-extract-cli/src/utils/removeLineBreaksInTag.ts
function removeLineBreaksInTag (line 1) | function removeLineBreaksInTag(str: string): string {
FILE: packages/i18n-extract-cli/src/utils/saveLocaleFile.ts
function saveLocaleFile (line 6) | function saveLocaleFile(locale: StringObject, path: string) {
FILE: packages/i18n-extract-cli/src/utils/serializeCode.ts
function serializeCode (line 4) | function serializeCode(source: unknown, isESModule = true) {
FILE: packages/i18n-extract-cli/src/utils/spreadObject.ts
function spreadObject (line 4) | function spreadObject(obj: Record<string, string>): StringObject {
FILE: packages/i18n-extract-cli/src/utils/stateManager.ts
class StateManager (line 4) | class StateManager {
method constructor (line 7) | private constructor() {}
method getInstance (line 15) | static getInstance() {
method setToolConfig (line 22) | setToolConfig(config: Config): void {
method getToolConfig (line 25) | getToolConfig(): Config {
method setCurrentSourcePath (line 29) | setCurrentSourcePath(path: string): void {
method getCurrentSourcePath (line 33) | getCurrentSourcePath(): string {
method setVueScriptLang (line 37) | setVueScriptLang(lang?: string): void {
method getVueScriptLang (line 41) | getVueScriptLang(): string {
FILE: packages/i18n-extract-cli/types/index.d.ts
type deepPartial (line 4) | type deepPartial<T extends object> = {
type GlobalRule (line 8) | interface GlobalRule {
type transformOptions (line 12) | interface transformOptions {
type CustomizeKey (line 19) | type CustomizeKey = (key: string, path?: string) => string
type GetCustomSlot (line 21) | type GetCustomSlot = (slotValue: string) => string
type Rule (line 23) | interface Rule {
type Rules (line 36) | type Rules = {
type FileExtension (line 40) | type FileExtension = 'js' | 'ts' | 'cjs' | 'mjs' | 'jsx' | 'tsx' | 'vue'
type StringObject (line 42) | type StringObject = {
type AdjustKeyMap (line 46) | type AdjustKeyMap = (
type YoudaoConfig (line 52) | interface YoudaoConfig {
type BaiduConfig (line 57) | interface BaiduConfig {
type AlicloudConfig (line 62) | interface AlicloudConfig {
type translatorType (line 67) | type translatorType = 'google' | 'youdao' | 'baidu' | 'alicloud'
type TranslateConfig (line 68) | interface TranslateConfig {
type PrettierConfig (line 79) | type PrettierConfig = Options | boolean
type TagOrder (line 81) | type TagOrder = Array<'template' | 'script' | 'style'>
type Config (line 83) | type Config = {
type CommandOptions (line 116) | interface CommandOptions {
FILE: packages/translate-utils/index.d.ts
type YoudaoConfig (line 1) | interface YoudaoConfig {
type BaiduConfig (line 6) | interface BaiduConfig {
type AlicloudConfig (line 11) | interface AlicloudConfig {
FILE: packages/translate-utils/src/alicloud.ts
type AlicloudConfig (line 5) | interface AlicloudConfig {
function createClient (line 10) | function createClient(key = '', secret = ''): alimt20181012 {
function toAlicloudLangCode (line 19) | function toAlicloudLangCode(locale: string): string {
function alicloudTranslate (line 27) | async function alicloudTranslate(
FILE: packages/translate-utils/src/baidu.ts
type BaiduConfig (line 6) | interface BaiduConfig {
function baiduTranslate (line 11) | async function baiduTranslate(
FILE: packages/translate-utils/src/google.ts
function googleTranslate (line 4) | async function googleTranslate(
FILE: packages/translate-utils/src/youdao.ts
type YoudaoConfig (line 6) | interface YoudaoConfig {
function youdaoTranslate (line 11) | async function youdaoTranslate(
Condensed preview — 99 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (153K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 271,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@2.2.0/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".editorconfig",
"chars": 187,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
},
{
"path": ".eslintignore",
"chars": 27,
"preview": "node_modules\ndist\nexamples\n"
},
{
"path": ".eslintrc.js",
"chars": 504,
"preview": "module.exports = {\n env: {\n es2021: true,\n node: true,\n },\n extends: [\n 'eslint:recommended',\n 'plugin:@t"
},
{
"path": ".gitattributes",
"chars": 26,
"preview": ".husky/* linguist-vendored"
},
{
"path": ".github/workflows/issue-close.yml",
"chars": 583,
"preview": "name: Issue Close\n\non:\n schedule:\n # GMT+8 03:00\n - cron: '0 19 * * *'\n\njobs:\n close-issues:\n runs-on: ubuntu"
},
{
"path": ".github/workflows/issue-inactive.yml",
"chars": 294,
"preview": "name: Issue Inactive\n\non:\n schedule:\n # GMT+8 03:00\n - cron: '0 19 * * *'\n\njobs:\n check-inactive:\n runs-on: u"
},
{
"path": ".github/workflows/issue-remove-inactive.yml",
"chars": 520,
"preview": "name: Issue Remove Inactive\n\non:\n issues:\n types: [edited]\n issue_comment:\n types: [created, edited]\n\njobs:\n is"
},
{
"path": ".github/workflows/release.yml",
"chars": 974,
"preview": "name: deployment\non:\n push:\n branches:\n - main\n\npermissions:\n contents: write\n pull-requests: write\n id-toke"
},
{
"path": ".gitignore",
"chars": 78,
"preview": "node_modules/\n.DS_Store\n*.log\ndist\n.turbo\n.cache\n.vscode\ntsconfig.tsbuildinfo\n"
},
{
"path": ".husky/commit-msg",
"chars": 91,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit $1\n"
},
{
"path": ".husky/pre-commit",
"chars": 82,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no-install lint-staged\n"
},
{
"path": ".npmrc",
"chars": 87,
"preview": "registry=https://registry.npmjs.org/\nengine-strict=true\nstrict-peer-dependencies=false\n"
},
{
"path": ".nvmrc",
"chars": 4,
"preview": "v18\n"
},
{
"path": ".prettierignore",
"chars": 18,
"preview": "node_modules\ndist\n"
},
{
"path": ".prettierrc",
"chars": 65,
"preview": "{\n \"printWidth\": 100,\n \"semi\": false,\n \"singleQuote\": true\n}\n\n"
},
{
"path": "LICENSE",
"chars": 1082,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2022-present IFreeOvO\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 2207,
"preview": "\n[.UserConfig} */\n\nmodule.exports = {\n extends: ['@commitlint/config-conventional'],\n rules: "
},
{
"path": "examples/react-demo/.editorconfig",
"chars": 245,
"preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_tr"
},
{
"path": "examples/react-demo/.gitignore",
"chars": 281,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/npm"
},
{
"path": "examples/react-demo/.prettierignore",
"chars": 80,
"preview": "**/*.md\n**/*.svg\n**/*.ejs\n**/*.html\npackage.json\n.umi\n.umi-production\n.umi-test\n"
},
{
"path": "examples/react-demo/.prettierrc",
"chars": 174,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"all\",\n \"printWidth\": 80,\n \"overrides\": [\n {\n \"files\": \".prettierr"
},
{
"path": "examples/react-demo/.umirc.ts",
"chars": 220,
"preview": "import { defineConfig } from 'umi';\n\nexport default defineConfig({\n nodeModulesTransform: {\n type: 'none',\n },\n ro"
},
{
"path": "examples/react-demo/README.md",
"chars": 325,
"preview": "# 介绍\nreact项目模板,用于测试翻译效果\n\n## 安装\n装项目依赖\n```\nyarn i\n```\n\n装命令行工具\n```\nyarn i -g @ovointl/i18n-extract-cli\n```\n\n## 运行项目\n在当前目录(r"
},
{
"path": "examples/react-demo/i18n.config.js",
"chars": 159,
"preview": "module.exports = {\n localePath: './src/locales/zh-CN.json',\n rules: {\n tsx: {\n importDeclaration: 'import { t "
},
{
"path": "examples/react-demo/mock/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "examples/react-demo/package.json",
"chars": 878,
"preview": "{\n \"private\": true,\n \"scripts\": {\n \"start\": \"umi dev\",\n \"build\": \"umi build\",\n \"postinstall\": \"umi generate t"
},
{
"path": "examples/react-demo/src/pages/index.tsx",
"chars": 572,
"preview": "import { setLocale, getLocale } from 'umi';\nimport { useState } from 'react';\n\nexport default function HomePage() {\n le"
},
{
"path": "examples/react-demo/src/utils/i18n.ts",
"chars": 203,
"preview": "import { useIntl } from 'umi';\nexport function t(key: string, params: Record<string, string> = {}) {\n const intl = useI"
},
{
"path": "examples/react-demo/tsconfig.json",
"chars": 653,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"node\",\n \"resolveJso"
},
{
"path": "examples/react-demo/typings.d.ts",
"chars": 244,
"preview": "declare module '*.css';\ndeclare module '*.less';\ndeclare module '*.png';\ndeclare module '*.svg' {\n export function Reac"
},
{
"path": "examples/vue-demo/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "examples/vue-demo/README.md",
"chars": 307,
"preview": "# 介绍\n\nvue 项目模板,用于测试翻译效果\n\n## 安装\n\n装项目依赖\n\n```\nnpm i\n```\n\n装命令行工具\n\n```\nnpm i -g @ovointl/i18n-extract-cli\n```\n\n## 运行项目\n\n在当前目录"
},
{
"path": "examples/vue-demo/index.html",
"chars": 362,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "examples/vue-demo/package.json",
"chars": 411,
"preview": "{\n \"name\": \"vue-demo\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n "
},
{
"path": "examples/vue-demo/src/App.vue",
"chars": 745,
"preview": "<template>\n <div>\n <a href=\"https://vitejs.dev\" target=\"_blank\">\n <img src=\"/vite.svg\" class=\"logo\" alt=\"Vite l"
},
{
"path": "examples/vue-demo/src/components/HelloWorld.vue",
"chars": 680,
"preview": "\n<template>\n <h1>{{ msg + '---组合' }}</h1>\n\n <div class=\"card\">\n <button type=\"button\" @click=\"count++\">数量为 {{ count"
},
{
"path": "examples/vue-demo/src/main.ts",
"chars": 152,
"preview": "import { createApp } from 'vue'\nimport './style.css'\nimport App from './App.vue'\nimport i18n from './utils/i18n'\ncreateA"
},
{
"path": "examples/vue-demo/src/style.css",
"chars": 1319,
"preview": ":root {\n font-family: Inter, Avenir, Helvetica, Arial, sans-serif;\n font-size: 16px;\n line-height: 24px;\n font-weigh"
},
{
"path": "examples/vue-demo/src/utils/i18n.ts",
"chars": 309,
"preview": "import { createI18n } from 'vue-i18n'\nimport zh from '../../locales/zh-CN.json'\nimport en from '../../locales/en-US.json"
},
{
"path": "examples/vue-demo/src/vite-env.d.ts",
"chars": 186,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n import type { DefineComponent } from 'vue'\n const com"
},
{
"path": "examples/vue-demo/tsconfig.json",
"chars": 488,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"modul"
},
{
"path": "examples/vue-demo/tsconfig.node.json",
"chars": 184,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSynthe"
},
{
"path": "examples/vue-demo/vite.config.ts",
"chars": 157,
"preview": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\n// https://vitejs.dev/config/\nexport default d"
},
{
"path": "package.json",
"chars": 1537,
"preview": "{\n \"private\": true,\n \"version\": \"1.0.0\",\n \"description\": \"提取项目里的中文,并替换为i18n函数\",\n \"scripts\": {\n \"preinstall\": \"onl"
},
{
"path": "packages/i18n-extract-cli/CHANGELOG.md",
"chars": 5922,
"preview": "# @ifreeovo/i18n-extract-cli\n\n## 4.3.4\n\n### Patch Changes\n\n- 7e331d5: 修复解析时会将 html 错误塞入 key\n\n## 4.3.3\n\n### Patch Changes"
},
{
"path": "packages/i18n-extract-cli/README.md",
"chars": 9597,
"preview": "# 介绍\n\n这是一款能够自动将代码里的中文转成 i18n 国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于 vue2、vue3 和 react\n\n## 流程设计\n\n见[掘金文章](https://juej"
},
{
"path": "packages/i18n-extract-cli/bin/index.js",
"chars": 716,
"preview": "#!/usr/bin/env node\nconst semver = require('semver')\nconst packageJson = require('../package.json')\nconst chalk = requir"
},
{
"path": "packages/i18n-extract-cli/package.json",
"chars": 2561,
"preview": "{\n \"name\": \"@ifreeovo/i18n-extract-cli\",\n \"version\": \"4.3.4\",\n \"description\": \"这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也"
},
{
"path": "packages/i18n-extract-cli/src/__tests__/transformJs.test.ts",
"chars": 4553,
"preview": "import { describe, it, expect, beforeEach } from 'vitest'\nimport transformJs from '../transformJs'\nimport { initParse } "
},
{
"path": "packages/i18n-extract-cli/src/collector.ts",
"chars": 1992,
"preview": "import type { CustomizeKey, StringObject } from '../types'\nimport log from './utils/log'\nimport { removeLineBreaksInTag "
},
{
"path": "packages/i18n-extract-cli/src/commands/init/index.ts",
"chars": 462,
"preview": "import fs from 'fs-extra'\nimport { getAbsolutePath } from '../../utils/getAbsolutePath'\nimport defaultConfig from '../.."
},
{
"path": "packages/i18n-extract-cli/src/commands/loadExcel/index.ts",
"chars": 1898,
"preview": "import xlsx from 'node-xlsx'\nimport type { StringObject } from '../../../types'\nimport { CommandOptions } from '../../.."
},
{
"path": "packages/i18n-extract-cli/src/core.ts",
"chars": 10755,
"preview": "import type { CommandOptions, FileExtension, TranslateConfig, PrettierConfig } from '../types'\nimport fs from 'fs-extra'"
},
{
"path": "packages/i18n-extract-cli/src/default.config.ts",
"chars": 1723,
"preview": "import { Config, Rule } from '../types'\n\n// 参数path,在生成配置文件时需要展示在文件里,所以这里去掉eslint校验\n// eslint-disable-next-line @typescri"
},
{
"path": "packages/i18n-extract-cli/src/exportExcel.ts",
"chars": 2013,
"preview": "import fs from 'fs-extra'\nimport type { StringObject } from '../types'\nimport StateManager from './utils/stateManager'\ni"
},
{
"path": "packages/i18n-extract-cli/src/index.ts",
"chars": 2795,
"preview": "import type { Command } from 'commander'\nimport { program, Option } from 'commander'\nimport leven from 'leven'\nimport mi"
},
{
"path": "packages/i18n-extract-cli/src/parse.ts",
"chars": 1379,
"preview": "import path from 'node:path'\nimport StateManager from './utils/stateManager'\n\nconst babel = require('@babel/core')\nconst"
},
{
"path": "packages/i18n-extract-cli/src/transform.ts",
"chars": 945,
"preview": "import chalk from 'chalk'\nimport type { Rules, FileExtension } from '../types'\nimport transformJs from './transformJs'\ni"
},
{
"path": "packages/i18n-extract-cli/src/transformJs.ts",
"chars": 15834,
"preview": "import { NodePath } from '@babel/traverse'\nimport type {\n Comment,\n StringLiteral,\n TemplateLiteral,\n ObjectProperty"
},
{
"path": "packages/i18n-extract-cli/src/transformVue.ts",
"chars": 14114,
"preview": "import type {\n SFCScriptBlock,\n SFCStyleBlock,\n SFCTemplateBlock,\n SFCDescriptor,\n} from '@vue/compiler-sfc'\nimport "
},
{
"path": "packages/i18n-extract-cli/src/translate.ts",
"chars": 7897,
"preview": "import fs from 'fs-extra'\nimport {\n googleTranslate,\n youdaoTranslate,\n baiduTranslate,\n alicloudTranslate,\n} from '"
},
{
"path": "packages/i18n-extract-cli/src/utils/assertType.ts",
"chars": 125,
"preview": "export function isObject(obj: unknown): obj is object {\n return Object.prototype.toString.call(obj) === '[object Object"
},
{
"path": "packages/i18n-extract-cli/src/utils/constants.ts",
"chars": 218,
"preview": "export const IGNORE_REMARK = 'i18n-ignore'\nexport const GOOGLE = 'google'\nexport const YOUDAO = 'youdao'\nexport const BA"
},
{
"path": "packages/i18n-extract-cli/src/utils/error-logger.ts",
"chars": 1000,
"preview": "import log from './log'\nclass ErrorLogger {\n private _filerPath = ''\n\n private _errorList: string[] = []\n\n private _d"
},
{
"path": "packages/i18n-extract-cli/src/utils/escapeQuotes.ts",
"chars": 114,
"preview": "export function escapeQuotes(value: string): string {\n return value.replace(/'/g, '_#_').replace(/\"/g, '_##_')\n}\n"
},
{
"path": "packages/i18n-extract-cli/src/utils/excelUtil.ts",
"chars": 663,
"preview": "import xlsx from 'node-xlsx'\nimport StateManager from './stateManager'\n\nexport function getExcelHeader(): string[] {\n c"
},
{
"path": "packages/i18n-extract-cli/src/utils/flatObjectDeep.ts",
"chars": 644,
"preview": "import type { StringObject } from '../../types'\nimport { isObject } from './assertType'\n\n/**\n * @example\n * 将{a: {bb: 1}"
},
{
"path": "packages/i18n-extract-cli/src/utils/getAbsolutePath.ts",
"chars": 146,
"preview": "import path from 'path'\nimport slash from 'slash'\n\nexport function getAbsolutePath(...paths: string[]) {\n return slash("
},
{
"path": "packages/i18n-extract-cli/src/utils/getLang.ts",
"chars": 885,
"preview": "import fs from 'fs-extra'\nimport StateManager from './stateManager'\nimport log from './log'\n\nfunction getLang(langPath: "
},
{
"path": "packages/i18n-extract-cli/src/utils/getLocaleDir.ts",
"chars": 294,
"preview": "import StateManager from './stateManager'\n\nexport function getLocaleDir(): string {\n const { localeFileType, localePath"
},
{
"path": "packages/i18n-extract-cli/src/utils/includeChinese.ts",
"chars": 108,
"preview": "export function includeChinese(code: string) {\n return new RegExp('[\\u{4E00}-\\u{9FFF}]', 'g').test(code)\n}\n"
},
{
"path": "packages/i18n-extract-cli/src/utils/initConfig.ts",
"chars": 947,
"preview": "import fs from 'fs-extra'\nimport merge from 'lodash/merge'\n\nimport { deepPartial, CommandOptions, Config } from '../../t"
},
{
"path": "packages/i18n-extract-cli/src/utils/isDirectory.ts",
"chars": 145,
"preview": "import { statSync } from 'node:fs'\n\nexport default function isDirectory(filePath: string): boolean {\n return statSync(f"
},
{
"path": "packages/i18n-extract-cli/src/utils/log.ts",
"chars": 611,
"preview": "import chalk from 'chalk'\n\nconst log = {\n info: (msg: string) => console.log('\\n' + chalk.cyan(msg)),\n warning: (msg: "
},
{
"path": "packages/i18n-extract-cli/src/utils/removeLineBreaksInTag.ts",
"chars": 106,
"preview": "export function removeLineBreaksInTag(str: string): string {\n return str.replace(/([\\r\\n]+\\s*)+/g, '')\n}\n"
},
{
"path": "packages/i18n-extract-cli/src/utils/saveLocaleFile.ts",
"chars": 532,
"preview": "import fs from 'fs-extra'\nimport type { StringObject } from '../../types'\nimport StateManager from './stateManager'\nimpo"
},
{
"path": "packages/i18n-extract-cli/src/utils/serializeCode.ts",
"chars": 448,
"preview": "import prettier from 'prettier'\nimport serialize from 'serialize-javascript'\n\nexport function serializeCode(source: unkn"
},
{
"path": "packages/i18n-extract-cli/src/utils/spreadObject.ts",
"chars": 326,
"preview": "import type { StringObject } from '../../types'\nimport set from 'lodash/set'\n\nexport function spreadObject(obj: Record<s"
},
{
"path": "packages/i18n-extract-cli/src/utils/stateManager.ts",
"chars": 975,
"preview": "import type { Config } from '../../types'\nimport defaultConfig from '../default.config'\n\nclass StateManager {\n private "
},
{
"path": "packages/i18n-extract-cli/tsconfig.json",
"chars": 185,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist\",\n \"rootDir\": \"./src\",\n \"a"
},
{
"path": "packages/i18n-extract-cli/types/index.d.ts",
"chars": 2645,
"preview": "import type { ParseResult } from '@babel/core'\nimport type { Options } from 'prettier'\n\nexport type deepPartial<T extend"
},
{
"path": "packages/translate-utils/CHANGELOG.md",
"chars": 234,
"preview": "# @ifreeovo/translate-utils\n\n## 1.2.1\n\n### Patch Changes\n\n- 19f80ae: 阿里云机器翻译语言代码未兼容\n\n## 1.2.0\n\n### Minor Changes\n\n- 4199"
},
{
"path": "packages/translate-utils/README.md",
"chars": 862,
"preview": "# @ifreeovo/translate-utils\n\n一个翻译工具函数库。支持有道,谷歌。\n\n## install\n\n```\nnpm i @ifreeovo/translate-utils\n```\n\n## api\n\n### google"
},
{
"path": "packages/translate-utils/index.d.ts",
"chars": 883,
"preview": "interface YoudaoConfig {\n key?: string\n secret?: string\n}\n\ninterface BaiduConfig {\n key?: string\n secret?: string\n}\n"
},
{
"path": "packages/translate-utils/package.json",
"chars": 1317,
"preview": "{\n \"name\": \"@ifreeovo/translate-utils\",\n \"version\": \"1.2.1\",\n \"description\": \"翻译工具函数,支持有道,谷歌,百度,阿里机器翻译\",\n \"publishCo"
},
{
"path": "packages/translate-utils/src/alicloud.ts",
"chars": 1702,
"preview": "import alimt20181012, * as $alimt20181012 from '@alicloud/alimt20181012'\nimport { Config } from '@alicloud/openapi-clien"
},
{
"path": "packages/translate-utils/src/baidu.ts",
"chars": 1084,
"preview": "import md5 from 'md5'\nimport qs from 'qs'\nimport { Response } from 'got/dist/source/core/index'\nconst got = require('got"
},
{
"path": "packages/translate-utils/src/google.ts",
"chars": 556,
"preview": "import ProxyAgent from 'proxy-agent'\nimport { translate } from '@vitalets/google-translate-api'\n\nexport async function g"
},
{
"path": "packages/translate-utils/src/index.ts",
"chars": 174,
"preview": "export { googleTranslate } from './google'\nexport { youdaoTranslate } from './youdao'\nexport { baiduTranslate } from './"
},
{
"path": "packages/translate-utils/src/youdao.ts",
"chars": 1031,
"preview": "import md5 from 'md5'\nimport qs from 'qs'\nimport { Response } from 'got/dist/source/core/index'\nconst got = require('got"
},
{
"path": "packages/translate-utils/tsconfig.cjs.json",
"chars": 211,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist/cjs\",\n \"rootDir\": \"./src\",\n "
},
{
"path": "packages/translate-utils/tsconfig.esm.json",
"chars": 192,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist/esm\",\n \"rootDir\": \"./src\",\n "
},
{
"path": "patches/mustache@4.2.0.patch",
"chars": 581,
"preview": "diff --git a/CHANGELOG.md b/CHANGELOG.md\ndeleted file mode 100644\nindex b1f72d0a37364c5a4b6d5ec394ca69a9191421ab..000000"
},
{
"path": "pnpm-workspace.yaml",
"chars": 27,
"preview": "packages:\n - 'packages/*'\n"
},
{
"path": "tsconfig.base.json",
"chars": 430,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"target\": \"es2018\",\n \"module\": \"commonjs\",\n \"moduleResolution\": \""
},
{
"path": "turbo.json",
"chars": 488,
"preview": "{\n \"$schema\": \"https://turborepo.org/schema.json\",\n \"pipeline\": {\n \"build\": {\n \"dependsOn\": [\"^build\"],\n "
}
]
About this extraction
This page contains the full source code of the IFreeOvO/i18n-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 99 files (123.7 KB), approximately 40.6k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.