chore(release): 移除被 dootask-release 技能取代的翻译/版本脚本

- 删 language/translate.php(OpenAI 翻译)+ composer.json/lock + README
- 删 bin/version.js(git-cliff + OpenAI 更新日志)+ cliff.toml
- package.json 去掉 version/translate 两条 script,cmd 去掉 translate 子命令
- README_PUBLISH 指向 dootask-release 技能

翻译/版本号/更新日志改由 dootask-release 技能完成;CI 不受影响(只读 package.json version + CHANGELOG.md)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-03 07:54:37 +00:00
parent 035c9d9d3d
commit 7335c59b68
9 changed files with 3 additions and 889 deletions

View File

@ -9,9 +9,9 @@
## 发布版本
> 翻译、版本号、更新日志改由 `dootask-release` 技能完成(见 `.claude/skills/dootask-release/`)。
```shell
npm run translate # 翻译(可选)
npm run version # 生成版本
npm run build # 编译前端
```

347
bin/version.js vendored
View File

@ -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();

View File

@ -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"

4
cmd
View File

@ -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 "$@"

View File

@ -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` 可选,留空时不会设置代理。

View File

@ -1,9 +0,0 @@
{
"name": "dootask/language",
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-json": "*",
"orhanerday/open-ai": "^5.2"
}
}

82
language/composer.lock generated
View File

@ -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"
}

View File

@ -1,352 +0,0 @@
<?php
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
require __DIR__ . '/vendor/autoload.php';
use Orhanerday\OpenAi\OpenAi;
// 读取 .env 文件的简单工具函数
function language_parse_env_file(string $path): array
{
$env = [];
$lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return $env;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
$delimiterPosition = strpos($line, '=');
if ($delimiterPosition === false) {
continue;
}
$name = trim(substr($line, 0, $delimiterPosition));
if (strpos($name, 'export ') === 0) {
$name = trim(substr($name, 7));
}
if ($name === '') {
continue;
}
$value = trim(substr($line, $delimiterPosition + 1));
$length = strlen($value);
if ($length >= 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" => <<<EOF
你是一个专业的翻译器,翻译的结果尽量符合 “项目任务管理系统” 的使用,请将提供的内容按每行一个翻译成:
```json
[
{
"key": "", // 原文本
"zh": "", // 留空(不用翻译)
"zh-CHT": "", // 繁体中文
"en": "", // 英语
"ko": "", // 韩语
"ja": "", // 日语
"de": "", // 德语
"fr": "", // 法语
"id": "", // 印度尼西亚语
"ru": "" // 俄语
}
]
```
请注意:(%T1)(%T2)(%T3)(%M1)(%M2) ...... 这类以 `小括号(%+内容)` 的字符组合是一个变量,翻译时请保留。
例子1
原文:此(%T1)已经处于【(%T2)】共享文件夹中,无法重复共享。
翻译成英语This (%T1) is already in the (%T2) shared folder and cannot be shared again。
例子2
原文:(%T1)的周报[(%T2)][(%T3)月第(%T4)]
翻译成英语Weekly report of (%T1) [(%T2)] [(Week (%T4) of (%T3) month)]
例子3
原文:(%T1)提交的「(%M2)」待你审批
翻译成英语:'(%M2)' submitted by (%T1) is waiting for your approval
例子4
原文:您发起的「(%M1)」已通过
翻译成英语The '(%M1)' you initiated has been approved
EOF,
],
[
"role" => "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");

View File

@ -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",