diff --git a/README_PUBLISH.md b/README_PUBLISH.md index be0a42d3f..fd2f56d6d 100644 --- a/README_PUBLISH.md +++ b/README_PUBLISH.md @@ -9,9 +9,9 @@ ## 发布版本 +> 翻译、版本号、更新日志改由 `dootask-release` 技能完成(见 `.claude/skills/dootask-release/`)。 + ```shell -npm run translate # 翻译(可选) -npm run version # 生成版本 npm run build # 编译前端 ``` diff --git a/bin/version.js b/bin/version.js deleted file mode 100644 index d23a0d54f..000000000 --- a/bin/version.js +++ /dev/null @@ -1,347 +0,0 @@ -const fs = require('fs'); -const path = require("path"); -const exec = require('child_process').exec; -let ProxyAgent = null; -try { - ProxyAgent = require("undici").ProxyAgent; -} catch (error) { - ProxyAgent = null; -} -const packageFile = path.resolve(process.cwd(), "package.json"); -const changeFile = path.resolve(process.cwd(), "CHANGELOG.md"); - -const verOffset = 6394; // 版本号偏移量 -const codeOffset = 35; // 代码版本号偏移量 - -const envFilePath = path.resolve(process.cwd(), ".env"); -const defaultAiSystemPrompt = "你是一位软件发布日志编辑专家。请产出 Markdown 更新日志,面向普通用户,以通俗友好的简体中文描述更新带来的直接好处,避免技术术语。所有章节标题必须以 `### ` 开头并保持英文 Title Case(例如 `### Features`、`### Bug Fixes`、`### Performance`、`### Documentation` 等)。每个章节内的条目按用户价值和影响范围排序,将更重要、影响更广的更新放在前面。"; -const defaultOpenAiEndpoint = "https://api.openai.com/v1/chat/completions"; - -function loadEnvFile(filePath) { - if (!fs.existsSync(filePath)) { - return; - } - const content = fs.readFileSync(filePath, "utf8"); - content.split(/\r?\n/).forEach(rawLine => { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) { - return; - } - const equalsIndex = line.indexOf("="); - if (equalsIndex === -1) { - return; - } - let key = line.slice(0, equalsIndex).trim(); - if (key.startsWith("export ")) { - key = key.slice(7).trim(); - } - let value = line.slice(equalsIndex + 1).trim(); - if (!value) { - value = ""; - } - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } else { - const commentIndex = value.indexOf(" #"); - if (commentIndex !== -1) { - value = value.slice(0, commentIndex).trim(); - } - } - if (process.env[key] === undefined) { - process.env[key] = value; - } - }); -} - - -loadEnvFile(envFilePath); - -function resolveApiEndpoint(candidate) { - const source = (candidate || "").trim(); - if (!source) { - return defaultOpenAiEndpoint; - } - if (/\/chat\/completions(\?|$)/.test(source)) { - return source; - } - const normalized = source.replace(/\/+$/, ""); - if (/\/v\d+$/i.test(normalized)) { - return `${normalized}/chat/completions`; - } - return `${normalized}/v1/chat/completions`; -} - -function loadSocksProxyAgent(proxyUrl) { - try { - const { SocksProxyAgent } = require('socks-proxy-agent'); - return new SocksProxyAgent(proxyUrl); - } catch (error) { - if (error && error.code === 'MODULE_NOT_FOUND') { - console.warn("检测到 SOCKS 代理,但未安装 socks-proxy-agent,请运行 `npm install --save-dev socks-proxy-agent` 后重试。"); - } else { - console.warn(`无法初始化 SOCKS 代理: ${error?.message || error}`); - } - return null; - } -} - -function createProxyDispatcher(proxyUrl) { - if (!proxyUrl) { - return null; - } - let parsedProtocol = ''; - try { - parsedProtocol = new URL(proxyUrl).protocol.replace(':', '').toLowerCase(); - } catch (error) { - console.warn(`代理地址无效 (${proxyUrl}): ${error.message}`); - return null; - } - if (parsedProtocol.startsWith('socks')) { - return loadSocksProxyAgent(proxyUrl); - } - if (!ProxyAgent) { - console.warn('未找到 undici.ProxyAgent,无法启用 HTTP 代理。'); - return null; - } - try { - return new ProxyAgent(proxyUrl); - } catch (error) { - console.warn(`无法初始化代理 (${proxyUrl}): ${error.message}`); - return null; - } -} - -function buildDefaultUserPrompt(version, changelogSection) { - return [ - "你是一位软件发布日志编辑专家。", - "下面是一段通过 git 提交记录自动生成的更新日志文本。", - "", - "请将其整理为一份「面向普通用户、简洁概览风格」的 changelog,保持 Markdown 格式,包含以下结构:", - "", - `## [${version}]`, - "", - "### Features", - "", - "- ...", - "", - "### Bug Fixes", - "", - "- ...", - "", - "### Performance", - "", - "- ...", - "", - "**要求:**", - "1. 删除技术性或重复的细节,合并相似项。", - "2. 语句自然简洁,用简体中文描述。", - "3. 使用贴近日常的词汇,突出更新对普通用户的直接价值,避免开发或管理术语(如\"refactor\"、\"merge branch\"、\"commit lint\")。", - "4. 小节标题必须以 `### ` 开头并保持英文 Title Case(例如 `### Features`、`### Bug Fixes`、`### Performance`、`### Documentation`、`### Security`、`### Miscellaneous` 等),不得翻译成中文。", - "5. 每个小节内的条目按用户价值和影响范围排序,将更重要、影响更广的更新放在前面。", - "6. 若某个小节没有内容,请省略整段小节(包括标题)。", - "7. 输出仅为 Markdown changelog 内容,不加其他解释。", - "", - "以下是原始日志:", - "```markdown", - changelogSection, - "```" - ].join("\n"); -} - -function runExec(command) { - return new Promise((resolve, reject) => { - exec(command, { maxBuffer: 1024 * 1024 * 10 }, (err, stdout, stderr) => { - if (err) { - reject(err); - return; - } - resolve(stdout.toString()); - }); - }); -} - -function removeDuplicateLines(log) { - const logs = log.split(/(\n## \[.*?\])/); - return logs.map(str => { - const array = []; - const items = str.split("\n"); - items.forEach(item => { - if (/^-/.test(item)) { - if (array.indexOf(item) === -1) { - array.push(item); - } - } else { - array.push(item); - } - }); - return array.join("\n"); - }).join(''); -} - -function findSectionBounds(content, version) { - const heading = `## [${version}]`; - const start = content.indexOf(heading); - if (start === -1) { - return null; - } - const nextHeadingIndex = content.indexOf("\n## [", start + heading.length); - const end = nextHeadingIndex === -1 ? content.length : nextHeadingIndex; - return { start, end }; -} - -function trimCliffOutput(rawOutput, version) { - const markerIndex = rawOutput.indexOf("## ["); - if (markerIndex === -1) { - return ""; - } - return rawOutput - .slice(markerIndex) - .replace("## [Unreleased]", `## [${version}]`) - .trim(); -} - -function buildAiHeaders(apiUrl, apiKey) { - const headers = { "Content-Type": "application/json" }; - const customHeader = process.env.CHANGELOG_AI_AUTH_HEADER; - if (customHeader) { - const separatorIndex = customHeader.indexOf(":"); - if (separatorIndex !== -1) { - const headerName = customHeader.slice(0, separatorIndex).trim(); - const headerValue = customHeader.slice(separatorIndex + 1).trim(); - if (headerName && headerValue) { - headers[headerName] = headerValue; - } - } - return headers; - } - if (apiUrl.includes("openai.azure.com")) { - headers["api-key"] = apiKey; - } else { - headers.Authorization = `Bearer ${apiKey}`; - } - return headers; -} - -async function enhanceWithAI(version, changelogSection) { - const apiKey = (process.env.OPENAI_API_KEY || "").trim(); - if (!apiKey) { - console.warn("未设置 OPENAI_API_KEY,跳过 AI 发布日志整理。"); - return changelogSection; - } - const proxyUrl = (process.env.OPENAI_PROXY_URL || "").trim(); - const explicitApiUrl = process.env.CHANGELOG_AI_URL || process.env.OPENAI_API_URL || process.env.OPENAI_BASE_URL; - const apiUrl = resolveApiEndpoint(explicitApiUrl); - const dispatcher = createProxyDispatcher(proxyUrl); - const model = process.env.CHANGELOG_AI_MODEL || process.env.OPENAI_API_MODEL || "gpt-4o-mini"; - const systemPrompt = process.env.CHANGELOG_AI_SYSTEM_PROMPT || defaultAiSystemPrompt; - const userPrompt = process.env.CHANGELOG_AI_PROMPT || buildDefaultUserPrompt(version, changelogSection); - - try { - const requestInit = { - method: "POST", - headers: buildAiHeaders(apiUrl, apiKey), - body: JSON.stringify({ - model, - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userPrompt } - ], - }) - }; - if (dispatcher) { - requestInit.dispatcher = dispatcher; - } - const response = await fetch(apiUrl, requestInit); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`AI request failed: ${errorText}`); - } - const data = await response.json(); - const aiText = data?.choices?.[0]?.message?.content?.trim(); - if (!aiText) { - throw new Error("AI response did not contain content."); - } - return aiText - .replace(/^\s*```markdown\s*/i, "") - .replace(/\s*```\s*$/i, "") - .trim(); - } catch (error) { - console.warn("AI summarization failed, falling back to original section:", error.message); - return changelogSection; - } -} - -async function generateLatestSection(version) { - const rawOutput = await runExec('docker run -t --rm -v "$(pwd)":/app/ orhunp/git-cliff:1.3.0 --unreleased'); - const section = trimCliffOutput(rawOutput, version); - if (!section.trim() || section.trim() === `## [${version}]`) { - return ""; - } - return section; -} - -function insertChangelogSection(existing, section, version) { - const trimmedSection = section.trim(); - if (!trimmedSection) { - return existing; - } - const bounds = findSectionBounds(existing, version); - if (bounds) { - return `${existing.slice(0, bounds.start)}${trimmedSection}\n\n${existing.slice(bounds.end).replace(/^(\n)+/, "")}`; - } - const insertIndex = existing.indexOf("\n## ["); - if (insertIndex === -1) { - return `${existing.trimEnd()}\n\n${trimmedSection}\n`; - } - const head = existing.slice(0, insertIndex).trimEnd(); - const tail = existing.slice(insertIndex).replace(/^(\n)+/, ""); - return `${head}\n\n${trimmedSection}\n\n${tail}`; -} - -async function main() { - try { - const verCountRaw = await runExec("git rev-list --count HEAD"); - const codeCountRaw = await runExec("git tag --merged pro -l 'v*' | wc -l"); - const verCount = verCountRaw.trim(); - const codeCount = codeCountRaw.trim(); - - const num = verOffset + parseInt(verCount, 10); - if (Number.isNaN(num) || Math.floor(num % 100) < 0) { - throw new Error(`get version error ${verCount}`); - } - const version = `${Math.floor(num / 10000)}.${Math.floor((num % 10000) / 100)}.${Math.floor(num % 100)}`; - const codeVersion = codeOffset + parseInt(codeCount, 10); - - let packageContent = fs.readFileSync(packageFile, "utf8"); - packageContent = packageContent.replace(/"version":\s*"(.*?)"/, `"version": "${version}"`); - packageContent = packageContent.replace(/"codeVerson":(.*?)(,|$)/, `"codeVerson": ${codeVersion}$2`); - fs.writeFileSync(packageFile, packageContent, "utf8"); - - console.log("New version: " + version); - console.log("New code verson: " + codeVersion); - - if (!fs.existsSync(changeFile)) { - throw new Error("Change file does not exist"); - } - - const latestSection = await generateLatestSection(version); - if (!latestSection) { - console.log("No new changelog entries detected."); - return; - } - - const aiSection = await enhanceWithAI(version, latestSection); - - const changelogContent = fs.readFileSync(changeFile, "utf8"); - const mergedContent = insertChangelogSection(changelogContent, aiSection, version); - const dedupedContent = removeDuplicateLines(mergedContent); - - fs.writeFileSync(changeFile, dedupedContent.trimEnd() + "\n", "utf8"); - console.log("Log file updated: CHANGELOG.md"); - } catch (error) { - console.error(error); - process.exitCode = 1; - } -} - -main(); diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index cde0b5930..000000000 --- a/cliff.toml +++ /dev/null @@ -1,60 +0,0 @@ -# configuration file for git-cliff (0.1.0) - -[changelog] -# changelog header -header = """ -# Changelog\n -All notable changes to this project will be documented in this file.\n -""" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] -{% else %}\ - ## [Unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ - {% endfor %} -{% endfor %}\n -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = """ -""" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = true -# regex for parsing and grouping commits -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^doc", group = "Documentation"}, - { message = "^perf", group = "Performance"}, - { message = "^pref", group = "Performance"}, - { message = "^refactor", group = "Refactor"}, - { message = "^style", group = "Styling"}, - { message = "^test", group = "Testing"}, - { message = "^chore\\(release\\): prepare for", skip = true}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -# filter out the commits that are not matched by commit parsers -filter_commits = true -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -ignore_tags = "" -# sort the tags chronologically -date_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "newest" diff --git a/cmd b/cmd index d882e71c0..96e41a906 100755 --- a/cmd +++ b/cmd @@ -931,10 +931,6 @@ case "$1" in container_exec php "php app/Models/clearHelper.php" container_exec php "php artisan ide-helper:models -W" ;; - "translate") - shift 1 - container_exec php "cd /var/www/language && php translate.php" - ;; "restart") shift 1 $COMPOSE stop "$@" diff --git a/language/README.md b/language/README.md deleted file mode 100644 index e61b1694a..000000000 --- a/language/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# 语言翻译工具说明 - -`language/translate.php` 脚本用于根据 `original-web.txt` 和 `original-api.txt` 中的内容,自动生成/更新 `translate.json` 以及前端使用的多语言文件。 - -## 使用步骤 - -1. 在项目根目录 `.env` 文件中配置: - - ```dotenv - OPENAI_API_KEY=你的OpenAI密钥 - OPENAI_BASE_URL=可选的自定义API地址 - OPENAI_PROXY_URL=可选的代理地址 - ``` - -2. 在 `language` 目录下执行: - - ```bash - php translate.php - ``` - -3. 查看生成的翻译结果: - - - 翻译详情:`language/translate.json` - - API 文件:`public/language/api/*.json` - - Web 文件:`public/language/web/*.js` - -## 注意事项 - -- 若 `.env` 未设置 `OPENAI_API_KEY`,脚本会直接退出。 -- `OPENAI_PROXY_URL` 可选,留空时不会设置代理。 diff --git a/language/composer.json b/language/composer.json deleted file mode 100644 index be9c2f610..000000000 --- a/language/composer.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "dootask/language", - "require": { - "php": ">=7.4", - "ext-curl": "*", - "ext-json": "*", - "orhanerday/open-ai": "^5.2" - } -} diff --git a/language/composer.lock b/language/composer.lock deleted file mode 100644 index 6ff628037..000000000 --- a/language/composer.lock +++ /dev/null @@ -1,82 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "ec9d23d3c9171a27ef10589ff18aaf1d", - "packages": [ - { - "name": "orhanerday/open-ai", - "version": "5.2", - "source": { - "type": "git", - "url": "https://github.com/orhanerday/open-ai.git", - "reference": "d8c78fe2f5fed59e0ba458f90b5589ed9f13a367" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/orhanerday/open-ai/zipball/d8c78fe2f5fed59e0ba458f90b5589ed9f13a367", - "reference": "d8c78fe2f5fed59e0ba458f90b5589ed9f13a367", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "php": ">=7.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.0", - "pestphp/pest": "^1.20", - "spatie/ray": "^1.28" - }, - "type": "library", - "autoload": { - "psr-4": { - "Orhanerday\\OpenAi\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Orhan Erday", - "email": "orhanerday@gmail.com", - "role": "Developer" - } - ], - "description": "OpenAI GPT-3 Api Client in PHP", - "homepage": "https://github.com/orhanerday/open-ai", - "keywords": [ - "open-ai", - "orhanerday" - ], - "support": { - "issues": "https://github.com/orhanerday/open-ai/issues", - "source": "https://github.com/orhanerday/open-ai/tree/5.2" - }, - "funding": [ - { - "url": "https://github.com/orhanerday", - "type": "github" - } - ], - "time": "2024-05-29T12:31:54+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=7.4", - "ext-curl": "*", - "ext-json": "*" - }, - "platform-dev": [], - "plugin-api-version": "2.6.0" -} diff --git a/language/translate.php b/language/translate.php deleted file mode 100755 index 6860ca9c0..000000000 --- a/language/translate.php +++ /dev/null @@ -1,352 +0,0 @@ -= 2) { - $first = $value[0]; - $last = $value[$length - 1]; - if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) { - $value = substr($value, 1, $length - 2); - } - } - - $env[$name] = $value; - } - - return $env; -} - -// 获取环境变量值的简单工具函数 -function language_env_value(string $key, array $env): ?string -{ - if (array_key_exists($key, $env)) { - return $env[$key]; - } - - $value = getenv($key); - if ($value !== false) { - return $value; - } - - return null; -} - -// 读取语言环境配置 -$languageEnvFile = dirname(__DIR__) . '/.env'; -$languageEnv = is_readable($languageEnvFile) ? language_parse_env_file($languageEnvFile) : []; - -// 优先从 .env 读取 OPENAI 配置,未找到时再次尝试 getenv 覆盖 -$openAiKey = trim(language_env_value('OPENAI_API_KEY', $languageEnv) ?? ''); -if ($openAiKey === '') { - fwrite(STDERR, "OPENAI_API_KEY 未设置,请在项目根目录的 .env 中配置。\n"); - exit(1); -} -$openAiProxy = trim(language_env_value('OPENAI_PROXY_URL', $languageEnv) ?? ''); -$openAiBaseUrl = trim(language_env_value('OPENAI_BASE_URL', $languageEnv) ?? ''); -$openAiModel = trim(language_env_value('OPENAI_API_MODEL', $languageEnv) ?? ''); - -// 读取所有要翻译的内容 -$originals = []; -$generateds = []; -foreach (['web', 'api'] as $type) { - $content = file_exists("original-{$type}.txt") ? file_get_contents("original-{$type}.txt") : ""; - $array = array_values(array_filter(array_unique(explode("\n", $content)))); - $generateds[$type] = $array; - $originals = array_merge($originals, $array); -} - -// 判定是否存在translate.json文件 -if (!file_exists("translate.json")) { - print_r("translate.json not exists"); - exit; -} - -$translations = []; // 翻译数据 -$regrror = []; // 正则匹配错误的数据 -$redundants = []; // 多余的数据 -$needs = []; // 需要翻译的数据 - -// 读取翻译数据 -$tmps = json_decode(file_get_contents("translate.json"), true); -foreach ($tmps as $obj) { - if (!isset($obj['key'])) { - continue; - } - - $currentKey = $obj['key']; - $originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey); - $translations[$originalKey] = $obj; - - if (!in_array($originalKey, $originals)) { - unset($translations[$originalKey]); - $redundants[$originalKey] = $obj; - continue; - } - - if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) { - foreach ($matches[0] as $match) { - foreach ($obj as $k => $v) { - if (empty($v)) { - continue; - } - if (!str_contains($v, $match)) { - // 正则匹配错误 - $regrror[$originalKey] = [ - $k => $v, - 'match' => $match, - 'key' => $currentKey, - ]; - continue 2; - } - } - } - } -} - -if (count($regrror) > 0) { - print_r("正则匹配错误的数据:\n"); - print_r($regrror); - exit(); -} -if (count($redundants) > 0) { - print_r("多余的数据:\n"); - print_r(implode(", ", array_keys($redundants)) . "\n\n"); -} - -// 需要翻译的数据 -foreach ($originals as $text) { - $key = trim($text); - if (!isset($translations[$key])) { - $needs[$key] = $key; - } -} -if (count($needs) > 0) { - $array = array_chunk($needs, 10, true); - $success = []; - $error = []; - $done = 0; - foreach ($array as $index => $keys) { - // 生成翻译内容 - foreach ($keys as &$key) { - $c = 1; - $key = preg_replace_callback('/\((\*+)\)/', function ($m) use (&$c) { - $label = strlen($m[1]) > 1 ? "M" : "T"; - return "(%" . $label . $c++ . ")"; - }, $key); - } - $content = implode("\n", $keys); - - // 开始翻译 - print_r("正在翻译:" . (count($keys) + $done) . "/" . count($needs) . "...\n"); - $openAi = new OpenAi($openAiKey); - if ($openAiBaseUrl !== '') { - $openAi->setBaseURL(rtrim(preg_replace('#/v\d+/?$#', '', $openAiBaseUrl), '/')); - } - if ($openAiProxy !== '') { - $openAi->setProxy($openAiProxy); - } - $result = $openAi->chat([ - "model" => $openAiModel, - "reasoning_effort" => "low", - 'messages' => [ - [ - "role" => "system", - "content" => << "user", - "content" => $content, - ], - ] - ]); - - // 处理结果 - $obj = json_decode($result); - $txt = preg_replace('/(^\s*```json\s*|\s*```\s*$)/', "", $obj->choices[0]->message->content); - $txt = preg_replace('/\(%([TM]\d+)\)/', '(%$1)', $txt); - $arr = json_decode($txt, true); - if (!$arr || !is_array($arr)) { - $error = array_merge($error, array_flip($keys)); - print_r("翻译失败:\n" . $content . "\n\n"); - file_put_contents("translate-gpt.log", json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n", FILE_APPEND); - continue; - } - - // 验证结果 - foreach ($arr as $item) { - if (empty($item['key'])) { - print_r("翻译结果不符合规范:key为空。\n"); - print_r($item); - continue; - } - foreach (['key', 'zh', 'zh-CHT', 'en', 'ko', 'ja', 'de', 'fr', 'id', 'ru'] as $lang) { - if (!isset($item[$lang])) { - print_r("翻译结果不符合规范:{$item['key']},缺少:{$lang} 的值。\n"); - continue 2; - } - } - $currentKey = $item['key']; - $originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey); - if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) { - foreach ($matches[0] as $match) { - foreach ($item as $k => $v) { - if (empty($v)) { - continue; - } - if (!str_contains($v, $match)) { - // 正则匹配错误 - $error[$originalKey] = [ - 'key' => $currentKey, - $k => $v, - 'match' => $match, - ]; - continue 3; - } - } - } - } - - $item['zh'] = ""; - $translations[$originalKey] = $item; - $success[$originalKey] = $item; - } - print_r("翻译完成:" . (count($keys) + $done) . "/" . count($needs) . "\n\n"); - $done += count($keys); - } - - if (count($error) > 0) { - print_r("正则匹配错误的数据:\n"); - print_r(json_encode(array_values($error), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n"); - } - - // 保存翻译结果 - file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); - print_r("----------------\n\n"); - print_r("总翻译:" . count($needs) . " 条\n"); - print_r("成功:" . count($success) . " 条\n"); - print_r("错误:" . count($error) . " 条\n\n"); - print_r("----------------\n\n"); -} - -// 生成前端使用的文件 -foreach ($generateds as $type => $array) { - $datas = []; - foreach ($array as $text) { - $text = trim($text); - if (isset($translations[$text])) { - $datas[] = $translations[$text]; - } - } - // 按长度排序 - $inOrder = []; - foreach ($datas as $index => $item) { - if (preg_match('/\(%[TM]\d+\)/', $item['key'])) { - $inOrder[$index] = strlen($item['key']); - } else { - $inOrder[$index] = strlen($item['key']) + 10000000000; - } - } - array_multisort($inOrder, SORT_DESC, $datas); - // 合成数组 - $results = []; - $index = 0; - foreach ($datas as $items) { - foreach ($items as $kk => $item) { - if (!isset($results)) { - $results[$kk] = []; - } - $results[$kk][] = $item; - } - } - // 生成文件 - if ($type === 'api') { - if (!is_dir("../public/language/api")) { - mkdir("../public/language/api", 0777, true); - } - foreach ($results as $kk => $item) { - $file = "../public/language/api/$kk.json"; - file_put_contents($file, json_encode($item, JSON_UNESCAPED_UNICODE)); - } - } elseif ($type === 'web') { - if (!is_dir("../public/language/web")) { - mkdir("../public/language/web", 0777, true); - } - foreach ($results as $kk => $item) { - $file = "../public/language/web/$kk.js"; - file_put_contents($file, "if(typeof window.LANGUAGE_DATA===\"undefined\")window.LANGUAGE_DATA={};window.LANGUAGE_DATA[\"{$kk}\"]=" . json_encode($item, JSON_UNESCAPED_UNICODE)); - } - } - print_r("[$type] total: " . count($results['key']) . "\n"); -} - -print_r("\n任务结束\n"); diff --git a/package.json b/package.json index 2046072a0..507f859c6 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,7 @@ "description": "DooTask is task management system.", "scripts": { "start": "./cmd dev", - "build": "./cmd prod", - "version": "node ./bin/version.js", - "translate": "./cmd translate" + "build": "./cmd prod" }, "author": { "name": "KuaiFan",