@alauda-fe/i18n-tools
Version:
基于 Azure OpenAI 的 JSON i18n 文件翻译和英文语法检查工具集
607 lines (529 loc) • 17.3 kB
JavaScript
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,
};