cc-translate 0.4.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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "cc-translate",
3
+ "version": "0.4.0",
4
+ "description": "A language translation tool based on OpenAI",
5
+ "author": "maybe",
6
+ "packageManager": "yarn@1.22.22",
7
+ "types": "./index.d.ts",
8
+ "main": "./index.mjs",
9
+ "module": "./index.mjs",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [
14
+ "translate",
15
+ "openai"
16
+ ],
17
+ "license": "ISC",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/cctip/cwallet-translate-script.git"
21
+ },
22
+ "homepage": "https://github.com/cctip/cwallet-translate-script",
23
+ "bugs": "https://github.com/xxx/cwallet-translate-script/issues",
24
+ "type": "module",
25
+ "scripts": {
26
+ "publish": "npx tsc && npm publish",
27
+ "compile": "tsc"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "import": "./index.js",
32
+ "require": "./index.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "ansi-colors": "^4.1.3",
37
+ "cli-progress": "^3.12.0",
38
+ "openai": "^4.68.1",
39
+ "ts-node": "^10.9.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/cli-progress": "^3.11.6",
43
+ "@types/node": "^22.7.9",
44
+ "typescript": "^5.6.3"
45
+ }
46
+ }
File without changes
package/src/index.ts ADDED
@@ -0,0 +1,361 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import colors from "ansi-colors";
4
+ import { OpenAI } from "openai";
5
+ import cliProgress from "cli-progress";
6
+ import {
7
+ ICwalletTranslateParams,
8
+ IJson,
9
+ IOpenaiConfig,
10
+ IOutputLanguageFile,
11
+ ISingleTranslate,
12
+ ITranslateChat,
13
+ ITranslateChatResponse,
14
+ SupportLanguageType,
15
+ } from "./types";
16
+ import {
17
+ chunkArray,
18
+ getRandomNumber,
19
+ notExistsToCreateFile,
20
+ readFileOfDirSync,
21
+ readJsonFileSync,
22
+ } from "./lib/utils.js";
23
+ import {
24
+ getCacheFileSync,
25
+ registerLanguageCacheFile,
26
+ translateJSONDiffToJson,
27
+ } from "./lib/cache/index.js";
28
+ import { logErrorToFile } from "./lib/log/index.js";
29
+ import { SUPPORT_LANGUAGE_MAP } from "./lib/support.js";
30
+
31
+ const __dirname = process.cwd();
32
+
33
+ const DEFAULT_OPENAI_CONFIG: IOpenaiConfig = {
34
+ model: "gpt-4o",
35
+ };
36
+
37
+ export class CwalletTranslate {
38
+ /** open ai api key */
39
+ private OPENAI_KEY: string;
40
+ /** */
41
+ CACHE_ROOT_PATH: string;
42
+ ENTRY_ROOT_PATH: string;
43
+ /** default en */
44
+ SOURCE_LANGUAGE: SupportLanguageType;
45
+ OUTPUT_ROOT_PATH: string | undefined;
46
+ languages: SupportLanguageType[];
47
+ client: OpenAI | null = null;
48
+ /** default model gpt-4o */
49
+ openaiConfig: IOpenaiConfig;
50
+ fineTune: string[];
51
+
52
+ constructor(params: ICwalletTranslateParams) {
53
+ this.OPENAI_KEY = params.key;
54
+ this.CACHE_ROOT_PATH = params.cacheFileRootPath;
55
+ this.ENTRY_ROOT_PATH = params.fileRootPath;
56
+ this.openaiConfig = params.openaiConfig ?? DEFAULT_OPENAI_CONFIG;
57
+ this.SOURCE_LANGUAGE = params.sourceLanguage ?? "en";
58
+ this.OUTPUT_ROOT_PATH = params.outputRootPath;
59
+ this.fineTune = params.fineTune;
60
+ this.languages = params.languages ?? [];
61
+ this.createOpenAIClient();
62
+ }
63
+
64
+ get supportLanguages() {
65
+ return Object.entries(SUPPORT_LANGUAGE_MAP)
66
+ .map(([key, val]) => val)
67
+ .filter(
68
+ ({ code }) =>
69
+ this.languages.includes(code) || code === this.SOURCE_LANGUAGE
70
+ );
71
+ }
72
+
73
+ get outputPath() {
74
+ return this.OUTPUT_ROOT_PATH ?? this.ENTRY_ROOT_PATH;
75
+ }
76
+
77
+ searchLanguage(code: SupportLanguageType) {
78
+ return this.supportLanguages.find((item) => item.code === code);
79
+ }
80
+
81
+ createOpenAIClient = () => {
82
+ /** 初始化openAi */
83
+ const client = new OpenAI({
84
+ apiKey: this.OPENAI_KEY,
85
+ });
86
+
87
+ this.client = client;
88
+ };
89
+ /**
90
+ * 翻译入口文件的所有支持的语言文件夹和其中的文件
91
+ */
92
+ translate = async () => {
93
+ console.log("🚀 开始翻译");
94
+ console.log(`🚀 使用的模型: ${this.openaiConfig.model} 🚀`);
95
+ console.log(`🚀 微调: ${this.fineTune} 🚀`);
96
+
97
+ const translateFolderPath = path.join(
98
+ this.ENTRY_ROOT_PATH,
99
+ this.SOURCE_LANGUAGE
100
+ );
101
+ // 翻译源语言问价夹下的所有json文件
102
+ const translateFolders = await readFileOfDirSync(translateFolderPath);
103
+ console.log("🚀 ~ 需要翻译语言的文件:", translateFolders);
104
+ // 创建进度条
105
+ const multiBar = new cliProgress.MultiBar(
106
+ {
107
+ clearOnComplete: false,
108
+ hideCursor: true,
109
+ format:
110
+ colors.cyan("{bar}") +
111
+ "| {percentage}% || {filename} {value}/{total} ",
112
+ },
113
+ cliProgress.Presets.legacy
114
+ );
115
+
116
+ let promises = [];
117
+ const arr: (() => Promise<void>)[] = [];
118
+
119
+ for (const item of this.supportLanguages) {
120
+ // 源语言不翻译
121
+ if (item.code === this.SOURCE_LANGUAGE) continue;
122
+ for (const fileName of translateFolders) {
123
+ const translateJson = await this.getTranslateContent(
124
+ item.code,
125
+ fileName
126
+ );
127
+ if (!translateJson) {
128
+ console.log(`${item.code}:${fileName} 没有需要翻译的内容`);
129
+ continue;
130
+ }
131
+ arr.push(() =>
132
+ this.singleTranslate({
133
+ language: item.code,
134
+ fileName,
135
+ multiBar,
136
+ translateJson,
137
+ })
138
+ );
139
+ }
140
+ }
141
+
142
+ promises = chunkArray(arr, 8);
143
+
144
+ for (const chunk of promises) {
145
+ await Promise.all(chunk.map((fn) => fn()));
146
+ }
147
+
148
+ multiBar.stop();
149
+ console.log("🚀 翻译完毕");
150
+ };
151
+ /**
152
+ * 翻译单个文件
153
+ * @param params
154
+ * @returns
155
+ */
156
+ singleTranslate = async (params: ISingleTranslate) => {
157
+ const {
158
+ /** 待翻译的语言 */
159
+ language,
160
+ /** 待翻译的文件名 */
161
+ fileName,
162
+ translateJson,
163
+ multiBar,
164
+ callback,
165
+ } = params;
166
+
167
+ try {
168
+ // 等待翻译的数组
169
+ const jsonMap: IJson = {};
170
+
171
+ // 生成chat循环代码
172
+ const promiseList = Object.entries(translateJson).map(
173
+ ([key, value], index) =>
174
+ () =>
175
+ this.translateChat({
176
+ key,
177
+ value,
178
+ language,
179
+ index,
180
+ fileName,
181
+ })
182
+ );
183
+
184
+ const progressBar = multiBar.create(promiseList.length, 0);
185
+
186
+ for (const fn of promiseList) {
187
+ const result = await fn();
188
+ jsonMap[result.key] = result.value;
189
+ progressBar.update(result.index + 1, {
190
+ filename: `${language}:${fileName}`,
191
+ });
192
+ }
193
+ this.outputLanguageFile({
194
+ jsonMap,
195
+ folderName: language,
196
+ fileName,
197
+ });
198
+ callback && callback();
199
+ } catch (error) {
200
+ logErrorToFile({
201
+ error: error as Error,
202
+ language,
203
+ fileName,
204
+ key: "",
205
+ });
206
+ return;
207
+ }
208
+ };
209
+
210
+ /**
211
+ * 使用open ai 进行翻译
212
+ * @param {string} key
213
+ * @param {string} value
214
+ * @param {OpenAI} client
215
+ * @param {string} language
216
+ * @returns
217
+ */
218
+ translateChat = (params: ITranslateChat): Promise<ITranslateChatResponse> => {
219
+ return new Promise((resolve) => {
220
+ const { key, value, language, index, fileName } = params;
221
+ try {
222
+ if (!this.client) throw new Error("Connection failed");
223
+ const targetLanguage = this.searchLanguage(language);
224
+ const originLanguage = this.searchLanguage(this.SOURCE_LANGUAGE);
225
+
226
+ if (!targetLanguage) {
227
+ throw new Error(`不支持的语言:${language}`);
228
+ }
229
+
230
+ if (!originLanguage) {
231
+ throw new Error(`不支持的语言:${this.SOURCE_LANGUAGE}`);
232
+ }
233
+
234
+ setTimeout(async () => {
235
+ const chatCompletion = await this.client!.chat.completions.create({
236
+ model: this.openaiConfig?.model,
237
+ messages: [
238
+ ...this.fineTune.map(
239
+ (val) =>
240
+ ({
241
+ role: "system",
242
+ content: val,
243
+ } as OpenAI.Chat.Completions.ChatCompletionMessageParam)
244
+ ),
245
+ {
246
+ role: "system",
247
+ content: `请将${originLanguage!.name}翻译成${
248
+ targetLanguage!.name
249
+ }`,
250
+ },
251
+ {
252
+ role: "system",
253
+ content: `翻译完成直接输出后对应意思的内容不要携带任何无关内容`,
254
+ },
255
+ {
256
+ role: "user",
257
+ content: value,
258
+ },
259
+ ],
260
+ });
261
+ resolve({
262
+ key,
263
+ value: chatCompletion?.choices[0]?.message.content ?? value,
264
+ index,
265
+ });
266
+ }, getRandomNumber(200, 300));
267
+ } catch (error) {
268
+ logErrorToFile({ error: error as Error, key, fileName, language });
269
+ resolve({
270
+ key,
271
+ value,
272
+ index,
273
+ error: error as Error,
274
+ });
275
+ }
276
+ });
277
+ };
278
+ /**
279
+ * 对比缓存文件 获取需要翻译的内容
280
+ * @param language
281
+ * @param fileName
282
+ * @returns
283
+ */
284
+ getTranslateContent = async (
285
+ language: SupportLanguageType,
286
+ fileName: string
287
+ ): Promise<IJson | undefined> => {
288
+ const translateFilePath = path.join(
289
+ this.ENTRY_ROOT_PATH,
290
+ this.SOURCE_LANGUAGE,
291
+ fileName
292
+ );
293
+ /** 缓存文件路径 */
294
+ const cacheFilePath = path.join(this.CACHE_ROOT_PATH, language, fileName);
295
+ if (!fs.existsSync(translateFilePath)) {
296
+ console.dir(`File not found: ${translateFilePath}`);
297
+ return;
298
+ }
299
+ const translateFileObject = await readJsonFileSync(translateFilePath);
300
+ const cacheObject = await getCacheFileSync(cacheFilePath);
301
+ const diffObject = translateJSONDiffToJson(
302
+ cacheObject,
303
+ translateFileObject
304
+ );
305
+ if (Object.values(diffObject).length === 0) {
306
+ if (Object.keys(translateFileObject).length === 0) {
307
+ this.outputLanguageFile({
308
+ jsonMap: {},
309
+ folderName: language,
310
+ fileName,
311
+ });
312
+ }
313
+ return;
314
+ }
315
+ return diffObject;
316
+ };
317
+
318
+ /**
319
+ * 输出语言文件
320
+ * @param {Object} jsonMap
321
+ */
322
+ outputLanguageFile = async (params: IOutputLanguageFile) => {
323
+ const { folderName, fileName, jsonMap } = params;
324
+ const outputFilePath = path.join(this.outputPath, folderName, fileName);
325
+ //创建输出文件夹
326
+ notExistsToCreateFile(this.outputPath);
327
+ //创建输出的语言文件夹
328
+ notExistsToCreateFile(`${this.outputPath}/${folderName}`);
329
+ let oldJsonData: string = "";
330
+ // 检查是否存在文件
331
+ if (!fs.existsSync(outputFilePath)) {
332
+ oldJsonData = await fs.readFileSync(
333
+ path.join(this.ENTRY_ROOT_PATH, this.SOURCE_LANGUAGE, fileName),
334
+ "utf8"
335
+ );
336
+ } else {
337
+ oldJsonData = await fs.readFileSync(outputFilePath, "utf8");
338
+ }
339
+ const oldJsonMap: IJson = JSON.parse(oldJsonData);
340
+ const newJsonMap: IJson = Object.assign(oldJsonMap, jsonMap);
341
+
342
+ await fs.writeFileSync(
343
+ path.resolve(outputFilePath),
344
+ JSON.stringify(newJsonMap, null, 2),
345
+ "utf8"
346
+ );
347
+
348
+ // 注册缓存
349
+ registerLanguageCacheFile({
350
+ sourceFilePath: path.join(
351
+ this.ENTRY_ROOT_PATH,
352
+ this.SOURCE_LANGUAGE,
353
+ fileName
354
+ ),
355
+ jsonMap: newJsonMap,
356
+ fileName,
357
+ language: folderName,
358
+ folderName: this.CACHE_ROOT_PATH,
359
+ });
360
+ };
361
+ }
@@ -0,0 +1,70 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { notExistsToCreateFile, readJsonFileSync } from "../utils.js";
4
+ import { IJson, IRegisterLanguageCacheFile } from "../../types";
5
+
6
+ /**
7
+ * 和翻译缓存json文件对比 返回存在更改的json文件
8
+ * @param {object} cacheObject 已经缓存的对象
9
+ * @param {object} translateObject 需要翻译的对象
10
+ * @returns {object} 存在修改的对象
11
+ */
12
+ export const translateJSONDiffToJson = (
13
+ cacheObject: IJson,
14
+ translateObject: IJson
15
+ ) => {
16
+ if (Object.values(cacheObject).length === 0) return translateObject;
17
+ // json文件内容diff
18
+ const pendingTranslateMap: IJson = {};
19
+ Object.entries(translateObject).forEach(([key, value]) => {
20
+ //不存在该key 是新增的key
21
+ if (!cacheObject[key]) {
22
+ pendingTranslateMap[key] = value;
23
+ }
24
+ // 存在缓存key但是内容不一样 需要重新翻译
25
+ else if (translateObject[key] !== cacheObject[key]) {
26
+ pendingTranslateMap[key] = value;
27
+ }
28
+ });
29
+
30
+ return pendingTranslateMap;
31
+ };
32
+
33
+ /**
34
+ * 获取缓存文件
35
+ * @param {string} filePath 缓存文件路径
36
+ * @returns {Promise<{key:value}>}
37
+ */
38
+ export const getCacheFileSync = async (filePath: string): Promise<IJson> => {
39
+ if (fs.existsSync(filePath)) {
40
+ return await readJsonFileSync(filePath);
41
+ } else {
42
+ return {};
43
+ }
44
+ };
45
+
46
+ /**
47
+ * 注册语言缓存文件
48
+ * @param {string} language
49
+ */
50
+ export const registerLanguageCacheFile = async (
51
+ params: IRegisterLanguageCacheFile
52
+ ) => {
53
+ const { jsonMap, sourceFilePath, fileName, language, folderName } = params;
54
+ const cacheFilePath = path.join(folderName, language, fileName);
55
+ const sourceObject = await readJsonFileSync(sourceFilePath);
56
+ const cacheObject = await readJsonFileSync(cacheFilePath);
57
+
58
+ Object.entries(jsonMap).forEach(([key, value]) => {
59
+ cacheObject[key] = sourceObject[key];
60
+ });
61
+
62
+ if (Object.values(jsonMap).length === 0) return;
63
+ notExistsToCreateFile(folderName);
64
+ notExistsToCreateFile(`${folderName}/${language}`);
65
+ await fs.writeFileSync(
66
+ cacheFilePath,
67
+ JSON.stringify(cacheObject, null, 2),
68
+ "utf8"
69
+ );
70
+ };
@@ -0,0 +1,19 @@
1
+ import { ITranslateLogError } from "../../types";
2
+ import fs from "fs";
3
+
4
+ export function logErrorToFile(params: ITranslateLogError) {
5
+ const { error, key, language, fileName } = params;
6
+ const logMessage = `${new Date().toISOString()} - Error: ${error.message}
7
+ \nLanguage: ${language}
8
+ \nFileName: ${fileName}
9
+ \nkey:${key}
10
+ \nStack: ${error.stack}\n\n`;
11
+
12
+ fs.appendFile("error.log", logMessage, (err) => {
13
+ if (err) {
14
+ console.error("无法写入日志文件:", err);
15
+ } else {
16
+ console.log("错误日志已写入 error.log 文件");
17
+ }
18
+ });
19
+ }
@@ -0,0 +1,104 @@
1
+ import { SupportLanguageMap } from "../types";
2
+
3
+ export const SUPPORT_LANGUAGE_MAP: SupportLanguageMap = {
4
+ en: {
5
+ code: "en",
6
+ name: "英语",
7
+ },
8
+ ["zh-CN"]: {
9
+ code: "zh-CN",
10
+ name: "简体中文",
11
+ },
12
+ ["zh-TW"]: {
13
+ code: "zh-TW",
14
+ name: "繁体中文",
15
+ },
16
+ ja: {
17
+ code: "ja",
18
+ name: "日语",
19
+ },
20
+ ar: {
21
+ code: "ar",
22
+ name: "阿拉伯语",
23
+ },
24
+ bn: {
25
+ code: "bn",
26
+ name: "孟加拉语",
27
+ },
28
+ de: {
29
+ code: "de",
30
+ name: "德语",
31
+ },
32
+ ["es-ES"]: {
33
+ code: "es-ES",
34
+ name: "西班牙语",
35
+ },
36
+ fr: {
37
+ code: "fr",
38
+ name: "法语",
39
+ },
40
+ hi: {
41
+ code: "hi",
42
+ name: "印地语",
43
+ },
44
+ id: {
45
+ code: "id",
46
+ name: "印度尼西亚语",
47
+ },
48
+ it: {
49
+ code: "it",
50
+ name: "意大利语",
51
+ },
52
+ ko: {
53
+ code: "ko",
54
+ name: "韩语",
55
+ },
56
+ ms: {
57
+ code: "ms",
58
+ name: "马来语",
59
+ },
60
+ my: {
61
+ code: "my",
62
+ name: "缅甸语",
63
+ },
64
+ ["ne-NP"]: {
65
+ code: "ne-NP",
66
+ name: "尼泊尔语",
67
+ },
68
+ nl: {
69
+ code: "nl",
70
+ name: "荷兰语",
71
+ },
72
+ pl: {
73
+ code: "pl",
74
+ name: "波兰语",
75
+ },
76
+ ["pt-PT"]: {
77
+ code: "pt-PT",
78
+ name: "葡萄牙语",
79
+ },
80
+ ru: {
81
+ code: "ru",
82
+ name: "俄罗斯语",
83
+ },
84
+ tl: {
85
+ code: "tl",
86
+ name: "菲律宾语",
87
+ },
88
+ tr: {
89
+ code: "tr",
90
+ name: "土耳其语",
91
+ },
92
+ vi: {
93
+ code: "vi",
94
+ name: "越南语",
95
+ },
96
+ uk: {
97
+ code: "uk",
98
+ name: "乌克兰语",
99
+ },
100
+ ["ur-PK"]: {
101
+ code: "ur-PK",
102
+ name: "乌尔都语",
103
+ },
104
+ };
@@ -0,0 +1,74 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { ICreateJsonFileParams } from "../types";
4
+
5
+ /**
6
+ * 不存在的文件夹则创建
7
+ * @param {string} path
8
+ */
9
+ export const notExistsToCreateFile = (path: string) => {
10
+ if (fs.existsSync(path)) return;
11
+ fs.mkdirSync(path);
12
+ };
13
+
14
+ /**
15
+ * 获取json文件 并返回 [[key,value],[key,value]...]]
16
+ * @param {*} path
17
+ * @returns
18
+ */
19
+ export const readJsonFileSync = async (path: string) => {
20
+ try {
21
+ if (!fs.existsSync(path)) return {};
22
+ const jsonStr = await fs.readFileSync(path, "utf8");
23
+ // 将JSON字符串解析为对象
24
+ return JSON.parse(jsonStr);
25
+ } catch (error) {
26
+ console.error("解析JSON时出错:", error);
27
+ return {};
28
+ }
29
+ };
30
+
31
+ /**
32
+ * 创建json文件
33
+ * @param {string} fileName 文件名
34
+ * @param {string} folderName 文件夹名
35
+ * @param {string} language 语言环境
36
+ * @param {object} jsonData 文件数据
37
+ */
38
+ export const createJsonFile = (params: ICreateJsonFileParams) => {
39
+ const { fileName, folderName, jsonData, language } = params;
40
+ notExistsToCreateFile(folderName);
41
+ notExistsToCreateFile(path.resolve(`${folderName}/${language}`));
42
+ fs.writeFileSync(
43
+ path.resolve(`${folderName}/${language}/${fileName}`),
44
+ JSON.stringify(jsonData, null, 2),
45
+ "utf8"
46
+ );
47
+ };
48
+
49
+ export const getRandomNumber = (min: number, max: number) => {
50
+ return Math.floor(Math.random() * (max - min + 1)) + min;
51
+ };
52
+
53
+ export const isDirectoryPath = (path: string) => {
54
+ if (!fs.existsSync(path)) return false;
55
+ return fs.statSync(path).isDirectory();
56
+ };
57
+
58
+ export const readFileOfDirSync = (dirPath: string) => {
59
+ if (!isDirectoryPath(dirPath)) return [];
60
+ const files = fs.readdirSync(dirPath);
61
+ // 筛选出所有文件夹
62
+ return files.filter((file) => path.extname(file) === ".json");
63
+ };
64
+
65
+ export function chunkArray<T extends object>(array: T[], chunkSize: number) {
66
+ const result = [];
67
+ let index = 0;
68
+
69
+ while (index < array.length) {
70
+ result.push(array.slice(index, index + chunkSize));
71
+ index += chunkSize;
72
+ }
73
+ return result;
74
+ }