UNPKG

@alauda-fe/i18n-tools

Version:

基于 Azure OpenAI 的 JSON i18n 文件翻译和英文语法检查工具集

607 lines (529 loc) 17.3 kB
#!/usr/bin/env node import https from "https"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; import { createRequire } from "module"; import { Logger } from "./utils.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const require = createRequire(import.meta.url); const logger = new Logger("sync"); // 读取配置文件 function loadConfig() { try { const configPath = path.join(__dirname, "i18n-resources.json"); const configData = require(configPath); return configData; } catch (error) { logger.error(`加载配置文件失败: ${error.message}`); process.exit(1); } } // 解析命令行参数 function parseArgs() { const args = process.argv.slice(2); const options = { repos: [], level: null, branch: null, token: null, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--repo" || arg === "-r") { options.repos.push(args[++i]); } else if (arg === "--level" || arg === "-l") { options.level = args[++i]; } else if (arg === "--branch" || arg === "-b") { options.branch = args[++i]; } else if (arg === "--token" || arg === "-t") { options.token = args[++i]; } else if (!arg.startsWith("-")) { // 没有标记的参数当作 repo 处理 options.repos.push(arg); } } // 注意:token 的智能选择将在获得配置信息后进行 // 这里不再直接从环境变量获取,而是在 main() 或 syncCommand() 中处理 return options; } // 根据仓库类型智能选择 token function selectTokenForRepos(repos, config, providedToken) { // 如果已经提供了 token,直接使用 if (providedToken) { return providedToken; } // 统计要处理的仓库类型 let hasGitLab = false; let hasGitHub = false; for (const repoKey of repos) { const repoConfig = config[repoKey]; if (repoConfig && repoConfig.repository) { const repoType = detectRepoType(repoConfig.repository); if (repoType === "github") { hasGitHub = true; } else { hasGitLab = true; } } } // 根据仓库类型选择合适的环境变量 if (hasGitHub && !hasGitLab) { // 只有 GitHub 仓库,优先使用 GITHUB_TOKEN return process.env.GITHUB_TOKEN || process.env.GITLAB_TOKEN || null; } else if (hasGitLab && !hasGitHub) { // 只有 GitLab 仓库,优先使用 GITLAB_TOKEN return process.env.GITLAB_TOKEN || process.env.GITHUB_TOKEN || null; } else { // 混合仓库或默认情况,优先使用 GITLAB_TOKEN(向后兼容) return process.env.GITLAB_TOKEN || process.env.GITHUB_TOKEN || null; } } // 过滤要处理的仓库 function filterRepos(config, options) { const filteredRepos = {}; for (const [repoKey, repoConfig] of Object.entries(config)) { let shouldInclude = true; // 如果指定了特定仓库,只包含指定的仓库 if (options.repos.length > 0) { shouldInclude = options.repos.includes(repoKey); } else { // 如果没有指定特定仓库,默认只包含 level 为 core 的仓库 // 除非用户明确指定了其他 level const targetLevel = options.level || "core"; shouldInclude = repoConfig.level === targetLevel; } // 如果明确指定了 level 且还指定了仓库,需要同时满足两个条件 if (options.level && options.repos.length > 0 && shouldInclude) { shouldInclude = repoConfig.level === options.level; } if (shouldInclude) { filteredRepos[repoKey] = repoConfig; } } return filteredRepos; } // 创建目录 async function ensureDirectory(dirPath) { try { await fs.access(dirPath); } catch { await fs.mkdir(dirPath, { recursive: true }); } } // 清理目录 async function cleanDirectory(dirPath) { try { // 检查目录是否存在 await fs.access(dirPath); // 读取目录内容 const files = await fs.readdir(dirPath); // 删除所有文件和子目录 for (const file of files) { const filePath = path.join(dirPath, file); const stat = await fs.lstat(filePath); if (stat.isDirectory()) { // 递归删除子目录 await fs.rm(filePath, { recursive: true, force: true }); } else { // 删除文件 await fs.unlink(filePath); } } logger.clean(`已清理目录: ${dirPath}`); } catch (error) { // 目录不存在或其他错误,忽略 if (error.code !== "ENOENT") { logger.warn(`无法清理目录 ${dirPath}: ${error.message}`); } } } // 检测仓库类型 function detectRepoType(repository) { if (repository.includes("github.com")) { return "github"; } else if (repository.includes("gitlab")) { return "gitlab"; } else { // 默认假设是 GitLab return "gitlab"; } } // 从 GitLab 或 GitHub 获取文件内容 function fetchFile(url, token = null, repoType = "gitlab") { return new Promise((resolve, reject) => { const options = { headers: {}, }; // 根据仓库类型设置不同的认证头 if (token) { if (repoType === "github") { options.headers["Authorization"] = `Bearer ${token}`; // GitHub API 需要 User-Agent options.headers["User-Agent"] = "i18n-tools"; } else { // GitLab options.headers["PRIVATE-TOKEN"] = token; } } https .get(url, options, (response) => { if (response.statusCode === 200) { let data = ""; response.on("data", (chunk) => { data += chunk; }); response.on("end", () => { resolve(data); }); } else if (response.statusCode === 302 || response.statusCode === 301) { // 处理重定向,传递 token 和 repoType fetchFile(response.headers.location, token, repoType) .then(resolve) .catch(reject); } else if (response.statusCode === 401) { reject( new Error( `HTTP 401: Unauthorized. Please check your ${ repoType === "github" ? "GitHub" : "GitLab" } access token.` ) ); } else if (response.statusCode === 403) { reject( new Error( `HTTP 403: Forbidden. Token may not have sufficient permissions for ${ repoType === "github" ? "GitHub" : "GitLab" }.` ) ); } else if (response.statusCode === 404) { reject( new Error( `HTTP 404: File not found. Check repository URL and file path.` ) ); } else { reject( new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`) ); } }) .on("error", reject); }); } // 构建 GitLab 或 GitHub API URL function buildApiUrl(repository, filePath, branch) { const repoType = detectRepoType(repository); if (repoType === "github") { return buildGitHubApiUrl(repository, filePath, branch); } else { return buildGitLabApiUrl(repository, filePath, branch); } } // 构建 GitLab API URL function buildGitLabApiUrl(repository, filePath, branch) { // 从 https://gitlab-ce.alauda.cn/frontend/alauda-fe 提取项目信息 const urlMatch = repository.match(/https?:\/\/([^\/]+)\/(.+)/); if (!urlMatch) { throw new Error(`Invalid GitLab repository URL format: ${repository}`); } const [, domain, projectPath] = urlMatch; // 对项目路径进行 URL 编码 const encodedProjectPath = encodeURIComponent(projectPath); // 对文件路径进行 URL 编码(去掉开头的 /) const cleanFilePath = filePath.startsWith("/") ? filePath.substring(1) : filePath; const encodedFilePath = encodeURIComponent(cleanFilePath); // 构建 GitLab API URL,只有指定分支时才添加 ref 参数 let url = `https://${domain}/api/v4/projects/${encodedProjectPath}/repository/files/${encodedFilePath}/raw`; if (branch) { url += `?ref=${branch}`; } return url; } // 构建 GitHub API URL function buildGitHubApiUrl(repository, filePath, branch) { // 从 https://github.com/owner/repo 提取项目信息 const urlMatch = repository.match( /https?:\/\/github\.com\/([^\/]+)\/([^\/]+)/ ); if (!urlMatch) { throw new Error(`Invalid GitHub repository URL format: ${repository}`); } const [, owner, repo] = urlMatch; // 对文件路径进行处理(去掉开头的 /) const cleanFilePath = filePath.startsWith("/") ? filePath.substring(1) : filePath; // 对文件路径进行 URL 编码 const encodedFilePath = encodeURIComponent(cleanFilePath); // 构建 GitHub API URL,只有指定分支时才添加 ref 参数 let url = `https://api.github.com/repos/${owner}/${repo}/contents/${encodedFilePath}`; if (branch) { url += `?ref=${branch}`; } return url; } // 获取文件名(不包含路径) function getFileName(filePath) { return path.basename(filePath); } // 下载单个文件 async function downloadFile( repository, filePath, branch, outputDir, repoKey, token ) { const repoType = detectRepoType(repository); const url = buildApiUrl(repository, filePath, branch); const fileName = getFileName(filePath); try { logger.download(`正在下载: ${url}`); const response = await fetchFile(url, token, repoType); let content; // GitHub API 返回的是 base64 编码的内容,需要解码 if (repoType === "github") { try { const jsonResponse = JSON.parse(response); if (jsonResponse.content && jsonResponse.encoding === "base64") { content = Buffer.from(jsonResponse.content, "base64").toString( "utf8" ); } else { throw new Error("Unexpected GitHub API response format"); } } catch (parseError) { throw new Error( `Failed to parse GitHub API response: ${parseError.message}` ); } } else { // GitLab API 直接返回文件内容 content = response; } // 确定最终的文件名 let fileNames = [fileName]; // 默认使用原文件名 // 尝试解析 JSON 内容,查找 "// i18n file name" 字段 try { const jsonContent = JSON.parse(content); const i18nFileName = jsonContent["// i18n file name"]; if (i18nFileName) { if (Array.isArray(i18nFileName)) { // 如果是数组,为每个有效的文件名创建文件 fileNames = i18nFileName .filter((name) => name && typeof name === "string" && name.trim()) .map((name) => { let finalName = name.trim(); if (!finalName.endsWith(".json")) { finalName += ".json"; } return finalName; }); if (fileNames.length === 0) { // 如果数组为空或没有有效文件名,使用原文件名 fileNames = [fileName]; } } else if (typeof i18nFileName === "string" && i18nFileName.trim()) { // 如果是字符串,使用该字符串作为文件名 let finalName = i18nFileName.trim(); if (!finalName.endsWith(".json")) { finalName += ".json"; } fileNames = [finalName]; } } } catch (parseError) { // JSON 解析失败,使用原文件名 logger.skip(`无法解析 JSON 提取文件名,使用默认名称: ${fileName}`); } // 为每个文件名创建文件 let savedCount = 0; for (const finalFileName of fileNames) { const outputPath = path.join(outputDir, finalFileName); await fs.writeFile(outputPath, content, "utf8"); logger.success(`已保存: ${finalFileName}`); savedCount++; } return savedCount > 0; } catch (error) { logger.error(`下载失败 ${url}: ${error.message}`); return false; } } // 下载仓库的所有文件 async function downloadRepoFiles( repoKey, repoConfig, branch, outputDir, token = null ) { logger.repoHeader(repoKey, repoConfig, branch); let successCount = 0; let totalCount = repoConfig.files.length; for (const filePath of repoConfig.files) { const fileCount = await downloadFile( repoConfig.repository, filePath, branch, outputDir, repoKey, token ); if (fileCount > 0) { // downloadFile 现在返回实际创建的文件数量 successCount += fileCount; } } logger.repoResult(repoKey, successCount, totalCount); return { success: successCount, total: totalCount }; } // 主函数 async function main() { const options = parseArgs(); const config = loadConfig(); const filteredRepos = filterRepos(config, options); if (Object.keys(filteredRepos).length === 0) { logger.error("没有仓库符合指定条件"); return; } // 智能选择 token if (!options.token) { options.token = selectTokenForRepos( Object.keys(filteredRepos), config, options.token ); } // 检查是否需要 token(私有仓库可能需要) if (!options.token) { logger.warn("未提供访问令牌,私有仓库可能无法访问"); logger.info( "使用 --token <your-token> 或设置环境变量 GITLAB_TOKEN/GITHUB_TOKEN" ); logger.info("对于公开仓库,此警告可以忽略"); } logger.start(`开始同步,目标仓库: ${Object.keys(filteredRepos).join(", ")}`); logger.info(`分支: ${options.branch}`); if (options.token) { logger.info(`令牌: ${options.token.substring(0, 8)}...`); } // 确保输出目录存在 const outputDir = path.join(process.cwd(), "new", "en"); // 确保目录存在(不清理) await ensureDirectory(outputDir); logger.info(`输出目录: ${outputDir}`); let totalSuccess = 0; let totalFiles = 0; // 下载所有仓库的文件 for (const [repoKey, repoConfig] of Object.entries(filteredRepos)) { const result = await downloadRepoFiles( repoKey, repoConfig, options.branch, outputDir, options.token ); totalSuccess += result.success; totalFiles += result.total; } logger.summary(totalSuccess, totalFiles, outputDir); if (totalSuccess < totalFiles) { logger.error("部分文件下载失败,请检查上面的错误信息"); if (!options.token) { logger.info( "💡 提示:如果仓库是私有的,请尝试提供访问令牌 (GitLab 或 GitHub)" ); } process.exit(1); } } // 同步命令函数(供命令行工具使用) async function syncCommand(options) { // 将命令行选项转换为内部格式 const internalOptions = { repos: options.repos || [], level: options.level || null, branch: options.branch || null, token: options.token || null, }; const config = loadConfig(); const filteredRepos = filterRepos(config, internalOptions); if (Object.keys(filteredRepos).length === 0) { logger.error("没有仓库符合指定条件"); return; } // 智能选择 token if (!internalOptions.token) { internalOptions.token = selectTokenForRepos( Object.keys(filteredRepos), config, internalOptions.token ); } // 检查是否需要 token(私有仓库可能需要) if (!internalOptions.token) { logger.warn("未提供访问令牌,私有仓库可能无法访问"); logger.info( "使用 --token <your-token> 或设置环境变量 GITLAB_TOKEN/GITHUB_TOKEN" ); logger.info("对于公开仓库,此警告可以忽略"); } logger.start(`开始同步,目标仓库: ${Object.keys(filteredRepos).join(", ")}`); logger.info(`分支: ${internalOptions.branch}`); if (internalOptions.token) { logger.info(`令牌: ${internalOptions.token.substring(0, 8)}...`); } // 确保输出目录存在 const outputDir = path.join(process.cwd(), "new", "en"); // 确保目录存在(不清理) await ensureDirectory(outputDir); logger.info(`输出目录: ${outputDir}`); let totalSuccess = 0; let totalFiles = 0; // 下载所有仓库的文件 for (const [repoKey, repoConfig] of Object.entries(filteredRepos)) { const result = await downloadRepoFiles( repoKey, repoConfig, internalOptions.branch, outputDir, internalOptions.token ); totalSuccess += result.success; totalFiles += result.total; } logger.summary(totalSuccess, totalFiles, outputDir); if (totalSuccess < totalFiles) { logger.error("部分文件下载失败,请检查上面的错误信息"); if (!internalOptions.token) { logger.info( "💡 提示:如果仓库是私有的,请尝试提供访问令牌 (GitLab 或 GitHub)" ); } } } // 运行脚本 if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error("Script failed:", error.message); process.exit(1); }); } export { loadConfig, filterRepos, downloadFile, buildApiUrl, buildGitLabApiUrl, buildGitHubApiUrl, detectRepoType, selectTokenForRepos, syncCommand, };