@zzclub/z-cli 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v0.5.0
5
+
6
+ [compare changes](https://github.com/aatrooox/z-cli/compare/v0.4.5...v0.5.0)
7
+
8
+ ### 🚀 Enhancements
9
+
10
+ - 增加i18n命令;提取vue中的国际化配置 ([9777990](https://github.com/aatrooox/z-cli/commit/9777990))
11
+
12
+ ### 📖 Documentation
13
+
14
+ - 更新i18n说明 ([141d8f3](https://github.com/aatrooox/z-cli/commit/141d8f3))
15
+
16
+ ### ❤️ Contributors
17
+
18
+ - Aatrox <gnakzz@qq.com>
19
+
4
20
  ## v0.4.5
5
21
 
6
22
  [compare changes](https://github.com/aatrooox/z-cli/compare/v0.4.4...v0.4.5)
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  ## 功能一览
10
10
 
11
+ - `i18n`: 提取 `.vue` 文件中的 `i18n` 语法
11
12
  - `translate`: 国际化文件翻译 (英译中)
12
13
  - `tiny`: 压缩文件。基于sharpjs, sharp支持的文件都可以压缩
13
14
  - `picgo`: 通过 Picgo 上传到图床
@@ -28,6 +29,15 @@
28
29
  npm i -g @zzclub/z-cli
29
30
  ```
30
31
 
32
+ ## i18n 规则说明
33
+
34
+ 1. 只提取 `.vue` 文件
35
+ 2. 匹配规则,如:$t('i18n.module-name.placeholder.month') => `/\$t\(['"]i18n\.([^'"]+)['"]\)/g`
36
+ 1. 默认忽略 common。如 $t('i18n.common.placeholder.month') 会被忽略
37
+ 2. 以逗号分割,第一位 i18n 是固定的。 第二位为 文件名。 最后一位为属性key。 第二至最后之间,为 object 的 key
38
+ 3. 最后的文件名称为 `module-name.js` 内容为 `{ placeholder: { month: "month"}} `
39
+ 4. 保存位置如果没传。就会保存在 `.vue` 同级目录下
40
+
31
41
  ## 翻译功能配置说明
32
42
  ### 初始化翻译平台appId和key
33
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzclub/z-cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "all-in-one 工具箱,专为提升日常及工作效率而生",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,261 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+
6
+ const i18nCmd = {
7
+ name: "i18n",
8
+ aliases: ["i"],
9
+ description: "从 Vue 文件中生成国际化配置文件",
10
+ options: [
11
+ {
12
+ flags: "-f, --file <file>",
13
+ description: "转换文件的路径",
14
+ defaultValue: null,
15
+ },
16
+ {
17
+ flags: "-d, --dir <dirpath>",
18
+ description: "转换文件夹的路径",
19
+ defaultValue: null,
20
+ },
21
+ {
22
+ flags: "-o, --output <output>",
23
+ description: "输出目录",
24
+ defaultValue: "",
25
+ },
26
+ ],
27
+ action: async (option) => {
28
+ let filePath = option.file;
29
+ let dirPath = option.dir;
30
+ let outputDir = option.output;
31
+ let file_spinner = ora();
32
+
33
+ if (!filePath && !dirPath) {
34
+ file_spinner.fail('请指定文件或目录')
35
+ process.exit(1);
36
+ }
37
+
38
+ // 有文件夹路径时忽略文件
39
+ if (dirPath) {
40
+ dirPath = path.resolve(process.cwd(), dirPath);
41
+ let stat;
42
+ try {
43
+ stat = fs.statSync(dirPath);
44
+ } catch (err) {
45
+ file_spinner.fail(`${chalk.red(dirPath)}不存在!`);
46
+ return;
47
+ }
48
+
49
+ if (!stat.isDirectory()) {
50
+ file_spinner.fail(`${chalk.red(dirPath)}不是一个文件夹!`);
51
+ return;
52
+ } else {
53
+ let filePaths = [];
54
+ file_spinner.succeed(`开始检索${chalk.red(dirPath)}`);
55
+
56
+ // 获取所有 .vue 文件
57
+ getAllVueFiles(dirPath, filePaths);
58
+
59
+ if (filePaths.length) {
60
+ file_spinner.succeed(
61
+ `共找到${chalk.red(filePaths.length)}个要处理的文件`
62
+ );
63
+ file_spinner.start();
64
+
65
+ // 处理所有文件
66
+ const i18nMap = {};
67
+
68
+ for (const file of filePaths) {
69
+ const content = fs.readFileSync(file, 'utf-8');
70
+ const fileName = path.basename(file, '.vue');
71
+ const i18nKeys = extractI18nKeys(content);
72
+
73
+ if (i18nKeys.length > 0) {
74
+ file_spinner.succeed(`从 ${chalk.yellow(fileName)} 中提取了 ${chalk.green(i18nKeys.length)} 个国际化键值`);
75
+
76
+ // 处理每个提取的键值
77
+ for (const key of i18nKeys) {
78
+ await processI18nKey(key, i18nMap);
79
+ }
80
+ }
81
+ }
82
+
83
+ // 如果没有指定输出目录,使用处理的第一个文件所在目录
84
+ if (!outputDir && filePaths.length > 0) {
85
+ outputDir = path.dirname(filePaths[0]);
86
+ }
87
+
88
+ // 生成国际化文件
89
+ await generateI18nFiles(i18nMap, outputDir, file_spinner);
90
+
91
+ file_spinner.succeed('国际化文件生成完成');
92
+ file_spinner.stop();
93
+ } else {
94
+ file_spinner.warn(
95
+ `未找到任何 .vue 文件`
96
+ );
97
+ }
98
+ }
99
+ } else {
100
+ filePath = path.resolve(process.cwd(), filePath);
101
+ file_spinner.succeed(`正在处理${chalk.yellowBright(filePath)}`);
102
+ file_spinner.start();
103
+
104
+ // 处理单个 vue 文件
105
+ if (!filePath.endsWith('.vue')) {
106
+ file_spinner.fail('只能处理 .vue 文件');
107
+ return;
108
+ }
109
+
110
+ const content = fs.readFileSync(filePath, 'utf-8');
111
+ const fileName = path.basename(filePath, '.vue');
112
+ const i18nKeys = extractI18nKeys(content);
113
+
114
+ if (i18nKeys.length > 0) {
115
+ const i18nMap = {};
116
+
117
+ file_spinner.succeed(`从 ${chalk.yellow(fileName)} 中提取了 ${chalk.green(i18nKeys.length)} 个国际化键值`);
118
+
119
+ // 处理每个提取的键值
120
+ for (const key of i18nKeys) {
121
+ await processI18nKey(key, i18nMap);
122
+ }
123
+
124
+ // 如果没有指定输出目录,使用当前处理的文件所在目录
125
+ if (!outputDir) {
126
+ outputDir = path.dirname(filePath);
127
+ }
128
+
129
+ // 生成国际化文件
130
+ await generateI18nFiles(i18nMap, outputDir, file_spinner);
131
+
132
+ file_spinner.succeed('国际化文件生成完成');
133
+ } else {
134
+ file_spinner.warn(`未从 ${chalk.yellow(fileName)} 中找到任何国际化键值`);
135
+ }
136
+
137
+ file_spinner.stop();
138
+ }
139
+ },
140
+ };
141
+
142
+ /**
143
+ * 递归获取所有 .vue 文件
144
+ * @param {string} dirPath 目录路径
145
+ * @param {Array} filePaths 文件路径数组
146
+ */
147
+ function getAllVueFiles(dirPath, filePaths) {
148
+ const files = fs.readdirSync(dirPath);
149
+
150
+ files.forEach(file => {
151
+ const fullPath = path.join(dirPath, file);
152
+ const stat = fs.statSync(fullPath);
153
+
154
+ if (stat.isDirectory()) {
155
+ getAllVueFiles(fullPath, filePaths);
156
+ } else if (file.endsWith('.vue')) {
157
+ filePaths.push(fullPath);
158
+ }
159
+ });
160
+ }
161
+
162
+ /**
163
+ * 从 Vue 文件内容中提取国际化键值
164
+ * @param {string} content 文件内容
165
+ * @param {string} ignorePrefix 要忽略的前缀
166
+ * @returns {Array} 提取的键值数组
167
+ */
168
+ function extractI18nKeys(content, ignorePrefix = "common") {
169
+ const regex = /\$t\(['"]i18n\.([^'"]+)['"]\)/g;
170
+ const keys = [];
171
+ let match;
172
+
173
+ while ((match = regex.exec(content)) !== null) {
174
+ const key = match[1];
175
+ // 如果设置了忽略前缀且键以该前缀开头,则跳过
176
+ if (ignorePrefix && key.startsWith(`${ignorePrefix}.`)) {
177
+ continue;
178
+ }
179
+ keys.push(key);
180
+ }
181
+
182
+ return [...new Set(keys)]; // 去重
183
+ }
184
+
185
+ function unquoteKeys(json) {
186
+ return json.replace(/"(\\[^]|[^\\"])*"\s*:?/g, function (match) {
187
+ if (/:$/.test(match)) {
188
+ return match.replace(/^"|"(?=\s*:$)/g, "");
189
+ } else {
190
+ return match;
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * 处理单个国际化键值
197
+ * @param {string} key 键值
198
+ * @param {Object} i18nMap 国际化映射对象
199
+ */
200
+ async function processI18nKey(key, i18nMap) {
201
+ // 解析键值 fee-forecast-maintain.placeholder.yearNum
202
+ const parts = key.split('.');
203
+
204
+ if (parts.length < 2) return;
205
+
206
+ const fileKey = parts[0]; // fee-forecast-maintain
207
+ const lastKey = parts[parts.length - 1]; // yearNum
208
+ const objKeys = parts.slice(1, parts.length - 1); // ['placeholder']
209
+
210
+ // 确保文件键存在,并且是一个对象
211
+ if (!i18nMap[fileKey] || typeof i18nMap[fileKey] !== 'object') {
212
+ i18nMap[fileKey] = {};
213
+ }
214
+
215
+ // 构建嵌套对象
216
+ let current = i18nMap[fileKey];
217
+ for (const objKey of objKeys) {
218
+ if (!current[objKey] || typeof current[objKey] !== 'object') {
219
+ current[objKey] = {};
220
+ }
221
+ current = current[objKey];
222
+ }
223
+
224
+ // 设置最终的键值
225
+ if (!current[lastKey]) {
226
+ // 使用键名作为默认值
227
+ current[lastKey] = lastKey;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 生成国际化文件
233
+ * @param {Object} i18nMap 国际化映射对象
234
+ * @param {string} outputDir 输出目录
235
+ * @param {Object} spinner 加载指示器
236
+ */
237
+ async function generateI18nFiles(i18nMap, outputDir, spinner) {
238
+ // 确保输出目录存在
239
+ if (!outputDir) {
240
+ spinner.fail('未指定输出目录');
241
+ return;
242
+ }
243
+
244
+ // 确保输出目录存在
245
+ outputDir = path.resolve(process.cwd(), outputDir);
246
+ if (!fs.existsSync(outputDir)) {
247
+ fs.mkdirSync(outputDir, { recursive: true });
248
+ }
249
+
250
+ // 为每个文件键生成文件
251
+ for (const fileKey in i18nMap) {
252
+ const filePath = path.join(outputDir, `${fileKey}.js`);
253
+ // 使用 unquoteKeys 去掉对象键的引号
254
+ const content = `export default ${unquoteKeys(JSON.stringify(i18nMap[fileKey], null, 2))}`;
255
+
256
+ fs.writeFileSync(filePath, content);
257
+ spinner.succeed(`生成文件: ${chalk.green(filePath)}`);
258
+ }
259
+ }
260
+
261
+ export { i18nCmd };
@@ -9,6 +9,10 @@ const __dirname = dirname(__filename);
9
9
  export const registerCommand = (program, config) => {
10
10
  let app = program.command(config.name).description(config.description);
11
11
 
12
+ if (config.alias) {
13
+ app.alias(config.alias);
14
+ }
15
+
12
16
  config.options.forEach((option) => {
13
17
  app.option(option.flags, option.description, option.defaultValue);
14
18
  });
@@ -5,12 +5,10 @@ import { translate } from "../translate-api/index.js";
5
5
  import { readJsonFile } from "../utils/file.js";
6
6
  import { writeFileContent, getLocalConfig } from "../utils/common.js";
7
7
  import ora from "ora";
8
- // const { log } = require("../utils/common");
9
8
 
10
- // let config: { translate: any } = await getLocalConfig();
11
- // const translateConfig = config.translate;
12
9
  const translateCmd = {
13
10
  name: "translate",
11
+ alias: "trans",
14
12
  description: "中译英功能,支持批量和单个文件翻译",
15
13
  // options: ['-l, --language <language>', '转换为什么语言, 支持[zh]和[en]', 'en'],
16
14
  options: [
@@ -33,11 +31,12 @@ const translateCmd = {
33
31
  action: async (option) => {
34
32
  let filePath = option.file;
35
33
  let dirPath = option.dir;
36
-
34
+ let file_spinner = ora();
37
35
  if (!filePath && !dirPath) {
36
+ file_spinner.fail('请指定文件或目录')
38
37
  process.exit(1);
39
38
  }
40
- let file_spinner = ora();
39
+
41
40
  let config = await getLocalConfig();
42
41
  const translateConfig = config.translate;
43
42
  if (!translateConfig.account.appId || !translateConfig.account.key) {
package/src/index.js CHANGED
@@ -8,7 +8,7 @@ import { setCmd } from "./command/set.js";
8
8
  import { tinyCmd } from "./command/tiny.js";
9
9
  import { picgoCmd } from "./command/picgo.js";
10
10
  import { checkUpdate } from './utils/common.js'
11
-
11
+ import { i18nCmd } from "./command/i18n.js";
12
12
  const program = new Command();
13
13
 
14
14
  initProgram(program, async () => {
@@ -21,6 +21,6 @@ initProgram(program, async () => {
21
21
 
22
22
  registerCommand(program, tinyCmd);
23
23
  registerCommand(program, picgoCmd);
24
-
24
+ registerCommand(program, i18nCmd)
25
25
  program.parse(process.argv);
26
26
  });