translate.cjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. require('dotenv').config({ path: ".env" });
  2. require('dotenv').config({ path: ".env.local" });
  3. const fs = require('fs');
  4. const path = require('path');
  5. const API_URL = 'https://api.deepseek.com/v1/chat/completions';
  6. const API_KEY = process.env.DEEPSEEK_API_KEY;
  7. const SOURCE_LANG = 'zh-cn';
  8. const LOCALE_FILE = path.resolve(__dirname, '../lang/index.json');
  9. // 引入进度条库
  10. const cliProgress = require('cli-progress');
  11. const colors = require('colors');
  12. async function translateBatch(texts, targetLang) {
  13. // 构建批量内容:每行以[index]开头
  14. const batchContent = texts.map((text, index) => `[${index}] ${text}`).join('\n');
  15. const response = await fetch(API_URL, {
  16. method: 'POST',
  17. headers: {
  18. 'Content-Type': 'application/json',
  19. Authorization: `Bearer ${API_KEY}`,
  20. },
  21. body: JSON.stringify({
  22. model: 'deepseek-chat',
  23. messages: [
  24. {
  25. role: 'system',
  26. content: `作为企业软件系统专业翻译,严格遵守以下铁律:
  27. ■ 核心原则
  28. 1. 严格逐符号翻译(${SOURCE_LANG}→${targetLang})
  29. 2. 禁止添加/删除/改写任何内容
  30. 3. 保持批量翻译的编号格式
  31. ■ 符号保留规则
  32. ! 所有符号必须原样保留:
  33. • 编程符号:\${ } <% %> @ # & |
  34. • UI占位符:{0} %s [ ]
  35. • 货币单位:¥100.00 kg cm²
  36. • 中文符号:【 】 《 》 :
  37. ■ 中文符号位置规范
  38. # 三级处理机制:
  39. 1. 成对符号必须保持完整结构:
  40. ✓ 正确:【Warning】Text
  41. ✗ 禁止:Warning【 】Text
  42. 2. 独立符号位置:
  43. • 优先句尾 → Text】?
  44. • 次选句首 → 】Text?
  45. • 禁止句中 → Text】Text?
  46. 3. 跨字符串符号处理:
  47. • 前段含【时 → 保留在段尾("Synchronize【")
  48. • 后段含】时 → 保留在段首("】authorization data?")
  49. • 符号后接字母时添加空格:】 Authorization
  50. ■ 语法规范
  51. • 外文 → 被动语态("Item was created")
  52. • 中文 → 主动语态("已创建项目")
  53. • 禁止推测上下文(只翻译当前字符串内容)
  54. ■ 错误预防(绝对禁止)
  55. ✗ 将中文符号改为西式符号(】→])
  56. ✗ 移动非中文符号位置
  57. ✗ 添加原文不存在的内容
  58. ✗ 合并/拆分原始字符串
  59. ■ 批量处理
  60. ▸ 严格保持原始JSON结构
  61. ▸ 语言键名精确匹配(zh-cn/en/it等)`
  62. },
  63. {
  64. role: 'user',
  65. content: batchContent,
  66. },
  67. ],
  68. temperature: 0.3,
  69. max_tokens: 4000,
  70. }),
  71. });
  72. const data = await response.json();
  73. if (!response.ok || !data.choices || !data.choices[0]?.message?.content) {
  74. const errorMsg = data.error?.message || `HTTP ${response.status}: ${response.statusText}`;
  75. throw new Error(`翻译API返回错误:${errorMsg}`);
  76. }
  77. // 解析批量响应
  78. const batchResult = data.choices[0].message.content.trim();
  79. const translations = {};
  80. // 按行分割结果
  81. const lines = batchResult.split('\n');
  82. for (const line of lines) {
  83. // 使用更精确的匹配模式
  84. const match = line.match(/^\[(\d+)\]\s*(.+)/);
  85. if (match) {
  86. const index = parseInt(match[1]);
  87. translations[index] = match[2].trim();
  88. }
  89. }
  90. return translations;
  91. }
  92. function extractTargetLangs(localeData) {
  93. const allLangs = new Set();
  94. for (const translations of Object.values(localeData)) {
  95. for (const lang of Object.keys(translations)) {
  96. if (lang !== SOURCE_LANG) {
  97. allLangs.add(lang);
  98. }
  99. }
  100. }
  101. return [...allLangs];
  102. }
  103. function groupTasksByLang(localeData, targetLangs) {
  104. const tasks = {};
  105. for (const lang of targetLangs) {
  106. tasks[lang] = {
  107. keys: [],
  108. texts: [],
  109. };
  110. }
  111. for (const [key, translations] of Object.entries(localeData)) {
  112. const sourceText = translations[SOURCE_LANG];
  113. if (!sourceText) {
  114. console.warn(`⚠️ 缺少源语言(${SOURCE_LANG})文本: ${key}`);
  115. continue;
  116. }
  117. for (const lang of targetLangs) {
  118. if (!translations[lang] || translations[lang].trim() === '') {
  119. tasks[lang].keys.push(key);
  120. tasks[lang].texts.push(sourceText);
  121. }
  122. }
  123. }
  124. return tasks;
  125. }
  126. async function main() {
  127. // 读取语言文件
  128. const rawData = fs.readFileSync(LOCALE_FILE);
  129. const localeData = JSON.parse(rawData);
  130. const TARGET_LANGS = extractTargetLangs(localeData);
  131. const langTasks = groupTasksByLang(localeData, TARGET_LANGS);
  132. let totalUpdated = 0;
  133. const BATCH_SIZE = 10;
  134. // 创建多进度条容器
  135. const multibar = new cliProgress.MultiBar(
  136. {
  137. format: '{lang} |' + colors.cyan('{bar}') + '| {percentage}% | {value}/{total} 条',
  138. barCompleteChar: '\u2588',
  139. barIncompleteChar: '\u2591',
  140. hideCursor: true,
  141. clearOnComplete: true,
  142. stopOnComplete: true,
  143. },
  144. cliProgress.Presets.shades_grey
  145. );
  146. // 为每个语言创建进度条
  147. const progressBars = {};
  148. for (const lang of TARGET_LANGS) {
  149. if (langTasks[lang].texts.length > 0) {
  150. progressBars[lang] = multibar.create(langTasks[lang].texts.length, 0, {
  151. lang: lang.padEnd(6, ' '),
  152. });
  153. }
  154. }
  155. // 并行处理所有语言
  156. await Promise.all(
  157. Object.entries(langTasks).map(async ([lang, task]) => {
  158. if (task.texts.length === 0) return;
  159. // 分批处理
  160. for (let i = 0; i < task.texts.length; i += BATCH_SIZE) {
  161. const batchKeys = task.keys.slice(i, i + BATCH_SIZE);
  162. const batchTexts = task.texts.slice(i, i + BATCH_SIZE);
  163. try {
  164. const batchResults = await translateBatch(batchTexts, lang);
  165. // 更新翻译结果
  166. batchKeys.forEach((key, index) => {
  167. if (batchResults[index] !== undefined) {
  168. localeData[key][lang] = batchResults[index];
  169. totalUpdated++;
  170. } else {
  171. console.error(`❌ 缺失翻译结果 [${key}@${lang}]`);
  172. localeData[key][lang] = `[BATCH_ERROR] ${localeData[key][SOURCE_LANG]}`;
  173. }
  174. });
  175. // 更新进度条
  176. progressBars[lang].increment(batchTexts.length);
  177. // 每批处理后保存进度
  178. fs.writeFileSync(LOCALE_FILE, JSON.stringify(localeData, null, 2));
  179. // 添加请求间隔避免速率限制
  180. await new Promise((resolve) => setTimeout(resolve, 300));
  181. } catch (error) {
  182. console.error(`\n❌ 批次翻译失败 [${lang}]:`, error.message);
  183. // 标记失败条目
  184. batchKeys.forEach((key) => {
  185. localeData[key][lang] = `[TRANSLATION_FAILED] ${localeData[key][SOURCE_LANG]}`;
  186. });
  187. // 跳过当前批次继续处理
  188. progressBars[lang].increment(batchTexts.length);
  189. }
  190. }
  191. })
  192. );
  193. // 停止所有进度条
  194. multibar.stop();
  195. // 最终保存
  196. fs.writeFileSync(LOCALE_FILE, JSON.stringify(localeData, null, 2));
  197. // 显示最终结果
  198. if (totalUpdated > 0) {
  199. console.log(`\n✅ 翻译完成! 共更新 ${totalUpdated} 处翻译`);
  200. } else {
  201. console.log('\nℹ️ 没有需要更新的翻译');
  202. }
  203. }
  204. main().catch(console.error);