mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 13:54:53 +08:00
feat(glossary): 实现 Rust Glossary V1
This commit is contained in:
@@ -13,8 +13,9 @@ use bat_infrastructure::{
|
||||
localized_patch_operations, open_append_file, read_download_manifest_at, read_file_no_symlink,
|
||||
read_localized_patch_manifest_at, read_localized_version_state, read_parse_cache_at,
|
||||
read_snapshot, read_textunit_index_at, read_translation_workbench, read_version_state,
|
||||
redact_proxy_url, repack_bundle, resolve_curl_proxy, set_translation, unset_translation,
|
||||
validate_output_root, validate_runtime_state_dir, validate_translation_workbench,
|
||||
redact_proxy_url, repack_bundle, resolve_curl_proxy, set_translation,
|
||||
set_translation_checked_with_glossary_path, unset_translation, validate_output_root,
|
||||
validate_runtime_state_dir, validate_translation_workbench_with_glossary_path,
|
||||
write_file_atomic, write_official_textunit_queues, CurlProxyConfig, CurlProxyMode,
|
||||
LocalizedPatchConfig, LocalizedPatchReport, LocalizedPatchService, LocalizedRollbackReport,
|
||||
OfficialEndpointMarkerRole, OfficialFailedVersionRecord, OfficialParseCacheService,
|
||||
@@ -54,6 +55,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[path = "config_file.rs"]
|
||||
mod config_file;
|
||||
#[path = "glossary_query.rs"]
|
||||
mod glossary_query;
|
||||
#[path = "patch_commands.rs"]
|
||||
mod patch_commands;
|
||||
#[path = "readonly_query.rs"]
|
||||
@@ -70,6 +73,11 @@ mod terminal_output;
|
||||
mod translation_query;
|
||||
#[path = "workflow_commands.rs"]
|
||||
mod workflow_commands;
|
||||
use glossary_query::{
|
||||
glossary_diagnose_rpc_report, glossary_mutation_rpc_report, glossary_query_rpc_report,
|
||||
glossary_review_rpc_report, glossary_rpc_envelope, glossary_summary_rpc_report,
|
||||
run_glossary_command,
|
||||
};
|
||||
use patch_commands::{
|
||||
is_write_patch_command, run_write_patch_command, validate_write_patch_options,
|
||||
};
|
||||
@@ -229,6 +237,16 @@ fn run() -> anyhow::Result<i32> {
|
||||
run_translation_memory_command(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::GlossarySummary
|
||||
| CliCommand::GlossaryQuery
|
||||
| CliCommand::GlossaryAdd
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDiagnose => {
|
||||
run_glossary_command(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationWorker => {
|
||||
run_repeated_workflow(&options, "translation-worker", run_translation_worker)?;
|
||||
Ok(0)
|
||||
@@ -385,6 +403,8 @@ struct CliOptions {
|
||||
translation_provider: Option<String>,
|
||||
translation_fixture: Option<PathBuf>,
|
||||
translation_memory_path: Option<PathBuf>,
|
||||
glossary_path: Option<PathBuf>,
|
||||
glossary_path_option_explicit: bool,
|
||||
translation_memory_option_explicit: bool,
|
||||
translation_memory_command_option_explicit: bool,
|
||||
translation_memory_source_text: Option<String>,
|
||||
@@ -392,6 +412,26 @@ struct CliOptions {
|
||||
translation_memory_record_id: Option<String>,
|
||||
translation_memory_reviewer: Option<String>,
|
||||
translation_memory_reason: Option<String>,
|
||||
glossary_term_id: Option<String>,
|
||||
glossary_source_term: Option<String>,
|
||||
glossary_aliases_json: Option<String>,
|
||||
glossary_recommended_translation: Option<String>,
|
||||
glossary_allowed_translations_json: Option<String>,
|
||||
glossary_source_language: Option<String>,
|
||||
glossary_target_language: Option<String>,
|
||||
glossary_category: Option<String>,
|
||||
glossary_priority: i32,
|
||||
glossary_scope_json: Option<String>,
|
||||
glossary_source_kind: Option<String>,
|
||||
glossary_source_ref: Option<String>,
|
||||
glossary_source_author: Option<String>,
|
||||
glossary_source_note: Option<String>,
|
||||
glossary_reviewer: Option<String>,
|
||||
glossary_reason: Option<String>,
|
||||
glossary_override_provenance: Option<String>,
|
||||
glossary_source_text: Option<String>,
|
||||
glossary_context_json: Option<String>,
|
||||
glossary_review_status: Option<String>,
|
||||
worker_concurrency: usize,
|
||||
worker_max_attempts: u32,
|
||||
worker_lease_seconds: u64,
|
||||
@@ -488,6 +528,8 @@ impl Default for CliOptions {
|
||||
translation_provider: None,
|
||||
translation_fixture: None,
|
||||
translation_memory_path: None,
|
||||
glossary_path: None,
|
||||
glossary_path_option_explicit: false,
|
||||
translation_memory_option_explicit: false,
|
||||
translation_memory_command_option_explicit: false,
|
||||
translation_memory_source_text: None,
|
||||
@@ -495,6 +537,26 @@ impl Default for CliOptions {
|
||||
translation_memory_record_id: None,
|
||||
translation_memory_reviewer: None,
|
||||
translation_memory_reason: None,
|
||||
glossary_term_id: None,
|
||||
glossary_source_term: None,
|
||||
glossary_aliases_json: None,
|
||||
glossary_recommended_translation: None,
|
||||
glossary_allowed_translations_json: None,
|
||||
glossary_source_language: None,
|
||||
glossary_target_language: None,
|
||||
glossary_category: None,
|
||||
glossary_priority: 0,
|
||||
glossary_scope_json: None,
|
||||
glossary_source_kind: None,
|
||||
glossary_source_ref: None,
|
||||
glossary_source_author: None,
|
||||
glossary_source_note: None,
|
||||
glossary_reviewer: None,
|
||||
glossary_reason: None,
|
||||
glossary_override_provenance: None,
|
||||
glossary_source_text: None,
|
||||
glossary_context_json: None,
|
||||
glossary_review_status: None,
|
||||
worker_concurrency: DEFAULT_TRANSLATION_CONCURRENCY,
|
||||
worker_max_attempts: DEFAULT_TRANSLATION_MAX_ATTEMPTS,
|
||||
worker_lease_seconds: DEFAULT_TRANSLATION_LEASE_SECONDS,
|
||||
@@ -590,6 +652,13 @@ enum CliCommand {
|
||||
TranslationMemorySummary,
|
||||
TranslationMemoryQuery,
|
||||
TranslationMemoryConfirm,
|
||||
GlossarySummary,
|
||||
GlossaryQuery,
|
||||
GlossaryAdd,
|
||||
GlossaryUpdate,
|
||||
GlossaryApprove,
|
||||
GlossaryDeprecate,
|
||||
GlossaryDiagnose,
|
||||
Repack,
|
||||
PublishLocalized,
|
||||
LocalizedRollback,
|
||||
@@ -1107,6 +1176,13 @@ const RPC_METHOD_TRANSLATION_WORKER_RUN: &str = "translation.worker.run";
|
||||
const RPC_METHOD_TRANSLATION_MEMORY_SUMMARY: &str = "translation.memory.summary";
|
||||
const RPC_METHOD_TRANSLATION_MEMORY_QUERY: &str = "translation.memory.query";
|
||||
const RPC_METHOD_TRANSLATION_MEMORY_CONFIRM: &str = "translation.memory.confirm";
|
||||
const RPC_METHOD_GLOSSARY_SUMMARY: &str = "translation.glossary.summary";
|
||||
const RPC_METHOD_GLOSSARY_QUERY: &str = "translation.glossary.query";
|
||||
const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
|
||||
const RPC_METHOD_GLOSSARY_UPDATE: &str = "translation.glossary.update";
|
||||
const RPC_METHOD_GLOSSARY_APPROVE: &str = "translation.glossary.approve";
|
||||
const RPC_METHOD_GLOSSARY_DEPRECATE: &str = "translation.glossary.deprecate";
|
||||
const RPC_METHOD_GLOSSARY_DIAGNOSE: &str = "translation.glossary.diagnose";
|
||||
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
||||
const RPC_METHOD_LOCALIZED_PUBLISH: &str = "localized.publish";
|
||||
const RPC_METHOD_LOCALIZED_ROLLBACK: &str = "localized.rollback";
|
||||
@@ -2105,7 +2181,11 @@ fn dispatch_rpc_method(
|
||||
RPC_METHOD_TRANSLATION_TASK_UPDATE => rpc_envelope_from_result(
|
||||
request_id,
|
||||
"translation.task.update",
|
||||
update_translation_task_status_report(state_dir, request.params.as_ref()),
|
||||
update_translation_task_status_report(
|
||||
state_dir,
|
||||
request.params.as_ref(),
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_TRANSLATION_PROOFREAD => {
|
||||
let _sync_guard = tasks
|
||||
@@ -2155,6 +2235,67 @@ fn dispatch_rpc_method(
|
||||
request.params.as_ref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_SUMMARY => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_summary_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_QUERY => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_query_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_DIAGNOSE => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_diagnose_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_ADD => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_mutation_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
false,
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_UPDATE => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_mutation_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
true,
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_APPROVE | RPC_METHOD_GLOSSARY_DEPRECATE => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_review_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
if request.method == RPC_METHOD_GLOSSARY_APPROVE {
|
||||
bat_core::domain::GlossaryReviewStatus::Approved
|
||||
} else {
|
||||
bat_core::domain::GlossaryReviewStatus::Deprecated
|
||||
},
|
||||
),
|
||||
),
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN => {
|
||||
let config = match rpc_translation_worker_config_with_defaults(
|
||||
request.params.as_ref(),
|
||||
@@ -3504,6 +3645,13 @@ fn rpc_translation_worker_config_with_defaults(
|
||||
)?
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| defaults.translation_memory_path.clone()),
|
||||
glossary_path: rpc_translation_worker_string_param(
|
||||
params,
|
||||
&["glossary_path", "translation_glossary_path"],
|
||||
"glossary_path",
|
||||
)?
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| defaults.glossary_path.clone()),
|
||||
};
|
||||
config.validate().map_err(|error| {
|
||||
ApiError::new(
|
||||
@@ -6109,6 +6257,10 @@ fn daemon_child_args(options: &CliOptions) -> Vec<String> {
|
||||
args.push("--translation-memory-path".to_string());
|
||||
args.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
if let Some(path) = options.glossary_path.as_ref() {
|
||||
args.push("--glossary-path".to_string());
|
||||
args.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
args.push("--worker-concurrency".to_string());
|
||||
args.push(options.worker_concurrency.to_string());
|
||||
args.push("--worker-max-attempts".to_string());
|
||||
@@ -6420,6 +6572,9 @@ fn apply_bat_env_overrides(
|
||||
{
|
||||
options.translation_memory_path = Some(PathBuf::from(v));
|
||||
}
|
||||
if let Some(v) = value("BAT_GLOSSARY_PATH") {
|
||||
options.glossary_path = Some(PathBuf::from(v));
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_CONCURRENCY") {
|
||||
options.worker_concurrency =
|
||||
parse_translation_worker_concurrency(&v, "环境变量 BAT_TRANSLATION_CONCURRENCY")?;
|
||||
@@ -6702,6 +6857,10 @@ fn parse_args_with_env(
|
||||
options.translation_memory_option_explicit = true;
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--glossary-path" | "--translation-glossary-path" => {
|
||||
options.glossary_path = Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
||||
options.glossary_path_option_explicit = true;
|
||||
}
|
||||
"--tm-source-text" => {
|
||||
options.translation_memory_source_text = Some(next_option_value(&mut args, &flag)?);
|
||||
options.translation_memory_command_option_explicit = true;
|
||||
@@ -6723,6 +6882,70 @@ fn parse_args_with_env(
|
||||
options.translation_memory_reason = Some(next_option_value(&mut args, &flag)?);
|
||||
options.translation_memory_command_option_explicit = true;
|
||||
}
|
||||
"--glossary-term-id" => {
|
||||
options.glossary_term_id = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-term" => {
|
||||
options.glossary_source_term = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-aliases-json" => {
|
||||
options.glossary_aliases_json = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-recommended-translation" => {
|
||||
options.glossary_recommended_translation =
|
||||
Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-allowed-translations-json" => {
|
||||
options.glossary_allowed_translations_json =
|
||||
Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-language" => {
|
||||
options.glossary_source_language = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-target-language" => {
|
||||
options.glossary_target_language = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-category" => {
|
||||
options.glossary_category = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-priority" => {
|
||||
options.glossary_priority = next_option_value(&mut args, &flag)?
|
||||
.parse()
|
||||
.map_err(|error| anyhow::anyhow!("--glossary-priority 无效:{error}"))?;
|
||||
}
|
||||
"--glossary-scope-json" => {
|
||||
options.glossary_scope_json = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-kind" => {
|
||||
options.glossary_source_kind = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-ref" => {
|
||||
options.glossary_source_ref = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-author" => {
|
||||
options.glossary_source_author = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-note" => {
|
||||
options.glossary_source_note = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-reviewer" => {
|
||||
options.glossary_reviewer = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-reason" => {
|
||||
options.glossary_reason = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-provenance" | "--glossary-override-provenance" => {
|
||||
options.glossary_override_provenance = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-source-text" => {
|
||||
options.glossary_source_text = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-context-json" => {
|
||||
options.glossary_context_json = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--glossary-review-status" => {
|
||||
options.glossary_review_status = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--worker-concurrency" | "--translation-concurrency" => {
|
||||
options.worker_concurrency = parse_translation_worker_concurrency(
|
||||
&next_option_value(&mut args, &flag)?,
|
||||
@@ -7240,6 +7463,30 @@ fn parse_args_with_env(
|
||||
"翻译 worker 参数只适用于 i18n worker run 或 daemon restart/reload"
|
||||
));
|
||||
}
|
||||
if options.glossary_path_option_explicit
|
||||
&& !options.daemon_child
|
||||
&& !matches!(
|
||||
options.command,
|
||||
CliCommand::TranslationWorker
|
||||
| CliCommand::GlossarySummary
|
||||
| CliCommand::GlossaryQuery
|
||||
| CliCommand::GlossaryAdd
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDiagnose
|
||||
| CliCommand::TranslationSet
|
||||
| CliCommand::TranslationValidate
|
||||
| CliCommand::TranslationTaskUpdate
|
||||
| CliCommand::PublishLocalized
|
||||
| CliCommand::Restart
|
||||
| CliCommand::Reload
|
||||
)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary 路径参数只适用于 Glossary、翻译 worker、工作台/任务发布或 daemon restart/reload"
|
||||
));
|
||||
}
|
||||
if options.translation_memory_option_explicit
|
||||
&& !matches!(
|
||||
options.command,
|
||||
@@ -7247,6 +7494,13 @@ fn parse_args_with_env(
|
||||
| CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::GlossarySummary
|
||||
| CliCommand::GlossaryQuery
|
||||
| CliCommand::GlossaryAdd
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDiagnose
|
||||
| CliCommand::Restart
|
||||
| CliCommand::Reload
|
||||
)
|
||||
@@ -7369,6 +7623,74 @@ fn parse_args_with_env(
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::GlossarySummary
|
||||
| CliCommand::GlossaryQuery
|
||||
| CliCommand::GlossaryAdd
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDiagnose => {
|
||||
if options.watch
|
||||
|| options.daemon
|
||||
|| options.daemon_child
|
||||
|| options.config.force
|
||||
|| options.config.dry_run
|
||||
|| options.run_count.is_some()
|
||||
|| options.sync_option_explicit
|
||||
|| options.output_explicit
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config, &options.env_baseline_config)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n glossary 命令只接受 --state-dir、Glossary 参数和 --json/--human"
|
||||
));
|
||||
}
|
||||
match options.command {
|
||||
CliCommand::GlossarySummary => {}
|
||||
CliCommand::GlossaryQuery => {
|
||||
if options.glossary_term_id.is_some()
|
||||
|| options.glossary_recommended_translation.is_some()
|
||||
|| options.glossary_reviewer.is_some()
|
||||
{
|
||||
return Err(anyhow::anyhow!("Glossary query 不接受 mutation 参数"));
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryDiagnose => {
|
||||
if options.glossary_source_text.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary diagnose 必须指定 --glossary-source-text"
|
||||
));
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryAdd | CliCommand::GlossaryUpdate => {
|
||||
if options.glossary_term_id.is_none()
|
||||
|| options.glossary_source_term.is_none()
|
||||
|| options.glossary_recommended_translation.is_none()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary add/update 必须指定 term-id、source-term 和 recommended-translation"
|
||||
));
|
||||
}
|
||||
if options.command == CliCommand::GlossaryUpdate
|
||||
&& options.glossary_reviewer.is_none()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary update 必须指定 --glossary-reviewer"
|
||||
));
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryApprove | CliCommand::GlossaryDeprecate => {
|
||||
if options.glossary_term_id.is_none() || options.glossary_reviewer.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary review 必须指定 --glossary-term-id 和 --glossary-reviewer"
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::PatchApply
|
||||
| CliCommand::UnityFsPatchTextAsset
|
||||
| CliCommand::UnityFsPatchStringField
|
||||
@@ -7916,6 +8238,9 @@ fn parse_translation_command(
|
||||
if action == "memory" || action == "tm" {
|
||||
return parse_translation_memory_command(args, options);
|
||||
}
|
||||
if action == "glossary" || action == "terms" {
|
||||
return parse_translation_glossary_command(args, options);
|
||||
}
|
||||
let command = match action.as_str() {
|
||||
"run" => CliCommand::Translate,
|
||||
"export" => CliCommand::Translate,
|
||||
@@ -7956,6 +8281,30 @@ fn parse_translation_memory_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_glossary_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
) -> anyhow::Result<()> {
|
||||
let action = next_option_value(args, "translation glossary")?;
|
||||
let command = match action.as_str() {
|
||||
"summary" | "status" => CliCommand::GlossarySummary,
|
||||
"query" | "find" => CliCommand::GlossaryQuery,
|
||||
"add" | "create" => CliCommand::GlossaryAdd,
|
||||
"update" | "edit" => CliCommand::GlossaryUpdate,
|
||||
"approve" | "trust" => CliCommand::GlossaryApprove,
|
||||
"deprecate" | "retire" => CliCommand::GlossaryDeprecate,
|
||||
"diagnose" | "check" => CliCommand::GlossaryDiagnose,
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"未知 translation glossary 二级命令:{other}"
|
||||
))
|
||||
}
|
||||
};
|
||||
ensure_command_not_set(options.command, &format!("translation glossary {action}"))?;
|
||||
options.command = command;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_worker_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
@@ -8086,6 +8435,7 @@ fn translation_worker_config_from_options(
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_worker_id.to_string()),
|
||||
translation_memory_path: options.translation_memory_path.clone(),
|
||||
glossary_path: options.glossary_path.clone(),
|
||||
};
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
|
||||
@@ -366,6 +366,77 @@ fn translation_memory_subcommands_reject_irrelevant_options() {
|
||||
assert!(error.to_string().contains("query 参数"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glossary_commands_parse_and_validate() {
|
||||
let query = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"glossary",
|
||||
"query",
|
||||
"--glossary-source-text",
|
||||
"Sensei",
|
||||
"--glossary-review-status",
|
||||
"approved",
|
||||
"--limit",
|
||||
"5",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(query.command, CliCommand::GlossaryQuery);
|
||||
assert_eq!(query.glossary_source_text.as_deref(), Some("Sensei"));
|
||||
assert_eq!(query.glossary_review_status.as_deref(), Some("approved"));
|
||||
assert_eq!(query.query_limit, 5);
|
||||
|
||||
let add = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"glossary",
|
||||
"add",
|
||||
"--glossary-term-id",
|
||||
"term-sensei",
|
||||
"--glossary-source-term",
|
||||
"Sensei",
|
||||
"--glossary-recommended-translation",
|
||||
"老师",
|
||||
"--glossary-source-kind",
|
||||
"manual",
|
||||
"--glossary-scope-json",
|
||||
r#"{"destination":"story.bundle"}"#,
|
||||
"--glossary-path",
|
||||
"/tmp/project-glossary.sqlite",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(add.command, CliCommand::GlossaryAdd);
|
||||
assert_eq!(add.glossary_term_id.as_deref(), Some("term-sensei"));
|
||||
assert_eq!(add.glossary_priority, 0);
|
||||
assert_eq!(
|
||||
add.glossary_path,
|
||||
Some(PathBuf::from("/tmp/project-glossary.sqlite"))
|
||||
);
|
||||
|
||||
let diagnose = parse(&[
|
||||
"bat",
|
||||
"translation",
|
||||
"glossary",
|
||||
"diagnose",
|
||||
"--glossary-source-text",
|
||||
"Sensei",
|
||||
"--glossary-context-json",
|
||||
r#"{"destination":"story.bundle"}"#,
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(diagnose.command, CliCommand::GlossaryDiagnose);
|
||||
assert!(parse(&["bat", "i18n", "glossary", "diagnose"]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"glossary",
|
||||
"query",
|
||||
"--glossary-term-id",
|
||||
"term-sensei",
|
||||
])
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_worker_env_defaults_apply() {
|
||||
let options = parse_with_env(
|
||||
@@ -765,6 +836,8 @@ fn translation_workbench_commands_read_update_and_clear_entries() {
|
||||
review_status: None,
|
||||
format: Some("plain".to_string()),
|
||||
text_source_kind: Some("text_asset".to_string()),
|
||||
glossary_qa: None,
|
||||
glossary_override: None,
|
||||
}],
|
||||
};
|
||||
bat_infrastructure::write_translation_workbench(&path, &workbench).unwrap();
|
||||
@@ -2679,6 +2752,38 @@ fn dispatch_translation_memory_summary_defaults_to_worker_config_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_glossary_summary_reports_missing_database_without_creating_it() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let output_root = temp.path().join("output");
|
||||
let state_dir = temp.path().join("state");
|
||||
let (queue, _rx) = mpsc::channel::<TaskJob>();
|
||||
let context = DaemonTaskContext {
|
||||
registry: TaskRegistry::new(),
|
||||
queue,
|
||||
base_config: OfficialUpdateConfig {
|
||||
output_root: output_root.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
translation_worker_config: TranslationWorkerConfig::default(),
|
||||
sync_lock: Arc::new(Mutex::new(())),
|
||||
restart_controller: test_restart_controller,
|
||||
};
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("translation.glossary.summary", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&context,
|
||||
"req-glossary-summary-1".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], false);
|
||||
assert_eq!(value["data"]["reason"], "database_missing");
|
||||
assert!(!output_root.join("glossary.sqlite").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_translation_memory_rejects_invalid_params_with_stable_error_code() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
|
||||
@@ -67,6 +67,7 @@ download_concurrency = 8
|
||||
provider = 'mock'
|
||||
fixture = ''
|
||||
translation_memory_path = ''
|
||||
glossary_path = ''
|
||||
concurrency = 8
|
||||
max_attempts = 3
|
||||
lease_seconds = 300
|
||||
@@ -150,6 +151,7 @@ struct TranslationWorkerSection {
|
||||
provider: Option<String>,
|
||||
fixture: Option<PathBuf>,
|
||||
translation_memory_path: Option<PathBuf>,
|
||||
glossary_path: Option<PathBuf>,
|
||||
concurrency: Option<usize>,
|
||||
max_attempts: Option<u32>,
|
||||
lease_seconds: Option<u64>,
|
||||
@@ -367,6 +369,9 @@ impl BatConfigFile {
|
||||
if let Some(value) = self.translation.worker.translation_memory_path.as_ref() {
|
||||
options.translation_memory_path = Some(value.clone());
|
||||
}
|
||||
if let Some(value) = self.translation.worker.glossary_path.as_ref() {
|
||||
options.glossary_path = Some(value.clone());
|
||||
}
|
||||
if let Some(value) = self.translation.worker.concurrency {
|
||||
options.worker_concurrency = value;
|
||||
}
|
||||
@@ -591,6 +596,10 @@ impl BatConfigFile {
|
||||
line_number,
|
||||
)?;
|
||||
}
|
||||
(SectionPath::TranslationWorker, "glossary_path") => {
|
||||
self.translation.worker.glossary_path =
|
||||
parse_optional_path(value, "translation.worker.glossary_path", line_number)?;
|
||||
}
|
||||
(SectionPath::TranslationWorker, "concurrency") => {
|
||||
self.translation.worker.concurrency = Some(parse_translation_worker_concurrency(
|
||||
&parse_scalar_text(value, "translation.worker.concurrency", line_number)?,
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
use super::report_output::print_json_value;
|
||||
use super::*;
|
||||
use bat_core::domain::{
|
||||
GlossaryReviewStatus, GlossarySourceKind, GlossarySourceRecord, GlossaryTermDraft,
|
||||
GlossaryTermSnapshot, TranslationMemoryContext,
|
||||
};
|
||||
use bat_infrastructure::{SqliteGlossaryRepository, GLOSSARY_SCHEMA_VERSION};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) fn run_glossary_command(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let method = glossary_method(options.command)?;
|
||||
if daemon_rpc_available(&options.state_dir)
|
||||
&& options.resource_root.is_none()
|
||||
&& !options.output_explicit
|
||||
{
|
||||
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
||||
let report = daemon_rpc_call(&options.state_dir, method, glossary_cli_params(options)?)?;
|
||||
print_json_value(options.output_format, &report)?;
|
||||
return Ok(());
|
||||
}
|
||||
let path = glossary_cli_path(options)?;
|
||||
let report =
|
||||
match options.command {
|
||||
CliCommand::GlossarySummary => build_glossary_summary_report(&path)?,
|
||||
CliCommand::GlossaryQuery => build_glossary_query_report(
|
||||
&path,
|
||||
options.glossary_source_text.as_deref(),
|
||||
options.glossary_category.as_deref(),
|
||||
options.glossary_review_status.as_deref(),
|
||||
options.query_limit,
|
||||
)?,
|
||||
CliCommand::GlossaryDiagnose => build_glossary_diagnose_report(
|
||||
&path,
|
||||
options.glossary_source_text.as_deref().unwrap_or_default(),
|
||||
parse_glossary_context(options.glossary_context_json.as_deref())?,
|
||||
)?,
|
||||
CliCommand::GlossaryAdd | CliCommand::GlossaryUpdate => {
|
||||
let draft = glossary_term_draft(options)?;
|
||||
let reviewer = options.glossary_reviewer.as_deref();
|
||||
build_glossary_mutation_report(
|
||||
&path,
|
||||
&draft,
|
||||
options.command == CliCommand::GlossaryUpdate,
|
||||
reviewer,
|
||||
options.glossary_reason.clone(),
|
||||
)?
|
||||
}
|
||||
CliCommand::GlossaryApprove | CliCommand::GlossaryDeprecate => {
|
||||
let term_id = options.glossary_term_id.as_deref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Glossary review 必须指定 --glossary-term-id")
|
||||
})?;
|
||||
let reviewer = options.glossary_reviewer.as_deref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Glossary review 必须指定 --glossary-reviewer")
|
||||
})?;
|
||||
let status = if options.command == CliCommand::GlossaryApprove {
|
||||
GlossaryReviewStatus::Approved
|
||||
} else {
|
||||
GlossaryReviewStatus::Deprecated
|
||||
};
|
||||
build_glossary_review_report(
|
||||
&path,
|
||||
term_id,
|
||||
status,
|
||||
reviewer,
|
||||
options.glossary_reason.clone(),
|
||||
)?
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
print_json_value(options.output_format, &report)
|
||||
}
|
||||
|
||||
fn glossary_method(command: CliCommand) -> anyhow::Result<&'static str> {
|
||||
Ok(match command {
|
||||
CliCommand::GlossarySummary => RPC_METHOD_GLOSSARY_SUMMARY,
|
||||
CliCommand::GlossaryQuery => RPC_METHOD_GLOSSARY_QUERY,
|
||||
CliCommand::GlossaryAdd => RPC_METHOD_GLOSSARY_ADD,
|
||||
CliCommand::GlossaryUpdate => RPC_METHOD_GLOSSARY_UPDATE,
|
||||
CliCommand::GlossaryApprove => RPC_METHOD_GLOSSARY_APPROVE,
|
||||
CliCommand::GlossaryDeprecate => RPC_METHOD_GLOSSARY_DEPRECATE,
|
||||
CliCommand::GlossaryDiagnose => RPC_METHOD_GLOSSARY_DIAGNOSE,
|
||||
_ => return Err(anyhow::anyhow!("不是 Glossary 命令")),
|
||||
})
|
||||
}
|
||||
|
||||
fn glossary_cli_path(options: &CliOptions) -> anyhow::Result<std::path::PathBuf> {
|
||||
if let Some(path) = options.glossary_path.as_ref() {
|
||||
return lexical_absolute(path).map_err(anyhow::Error::msg);
|
||||
}
|
||||
let resource_root = options
|
||||
.resource_root
|
||||
.as_deref()
|
||||
.map(lexical_absolute)
|
||||
.transpose()
|
||||
.map_err(anyhow::Error::msg)?
|
||||
.unwrap_or(active_official_resource_root(&options.config.output_root)?);
|
||||
Ok(SqliteGlossaryRepository::repository_path(&resource_root))
|
||||
}
|
||||
|
||||
fn glossary_cli_params(options: &CliOptions) -> anyhow::Result<Option<serde_json::Value>> {
|
||||
let mut params = serde_json::Map::new();
|
||||
if let Some(path) = options.glossary_path.as_ref() {
|
||||
params.insert("glossary_path".to_string(), serde_json::json!(path));
|
||||
}
|
||||
match options.command {
|
||||
CliCommand::GlossarySummary => {}
|
||||
CliCommand::GlossaryQuery => {
|
||||
if let Some(source_text) = options.glossary_source_text.as_deref() {
|
||||
params.insert("source_text".to_string(), serde_json::json!(source_text));
|
||||
}
|
||||
if let Some(category) = options.glossary_category.as_deref() {
|
||||
params.insert("category".to_string(), serde_json::json!(category));
|
||||
}
|
||||
if let Some(status) = options.glossary_review_status.as_deref() {
|
||||
params.insert("review_status".to_string(), serde_json::json!(status));
|
||||
}
|
||||
params.insert("limit".to_string(), serde_json::json!(options.query_limit));
|
||||
}
|
||||
CliCommand::GlossaryDiagnose => {
|
||||
let source_text = options.glossary_source_text.as_deref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Glossary diagnose 必须指定 --glossary-source-text")
|
||||
})?;
|
||||
params.insert("source_text".to_string(), serde_json::json!(source_text));
|
||||
params.insert(
|
||||
"context".to_string(),
|
||||
serde_json::json!(parse_glossary_context(
|
||||
options.glossary_context_json.as_deref()
|
||||
)?),
|
||||
);
|
||||
}
|
||||
CliCommand::GlossaryAdd | CliCommand::GlossaryUpdate => {
|
||||
let draft = glossary_term_draft(options)?;
|
||||
params.extend(
|
||||
serde_json::to_value(draft)?
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if options.command == CliCommand::GlossaryUpdate {
|
||||
params.insert(
|
||||
"reviewer".to_string(),
|
||||
serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()),
|
||||
);
|
||||
if let Some(reason) = options.glossary_reason.as_deref() {
|
||||
params.insert("reason".to_string(), serde_json::json!(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryApprove | CliCommand::GlossaryDeprecate => {
|
||||
params.insert(
|
||||
"term_id".to_string(),
|
||||
serde_json::json!(options.glossary_term_id.as_deref().unwrap_or_default()),
|
||||
);
|
||||
params.insert(
|
||||
"reviewer".to_string(),
|
||||
serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()),
|
||||
);
|
||||
if let Some(reason) = options.glossary_reason.as_deref() {
|
||||
params.insert("reason".to_string(), serde_json::json!(reason));
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(Some(serde_json::Value::Object(params)))
|
||||
}
|
||||
|
||||
fn glossary_term_draft(options: &CliOptions) -> anyhow::Result<GlossaryTermDraft> {
|
||||
let term_id = required_option(options.glossary_term_id.as_deref(), "--glossary-term-id")?;
|
||||
let source_term = required_option(
|
||||
options.glossary_source_term.as_deref(),
|
||||
"--glossary-source-term",
|
||||
)?;
|
||||
let recommended_translation = required_option(
|
||||
options.glossary_recommended_translation.as_deref(),
|
||||
"--glossary-recommended-translation",
|
||||
)?;
|
||||
let aliases = parse_string_array(
|
||||
options.glossary_aliases_json.as_deref(),
|
||||
"--glossary-aliases-json",
|
||||
)?;
|
||||
let allowed_translations = parse_string_array(
|
||||
options.glossary_allowed_translations_json.as_deref(),
|
||||
"--glossary-allowed-translations-json",
|
||||
)?;
|
||||
let scope = parse_glossary_context(options.glossary_scope_json.as_deref())?;
|
||||
let source_kind = options.glossary_source_kind.as_deref().unwrap_or("manual");
|
||||
let source_kind = GlossarySourceKind::parse(source_kind)
|
||||
.ok_or_else(|| anyhow::anyhow!("Glossary source kind 无效:{source_kind}"))?;
|
||||
let review_status = options.glossary_review_status.as_deref().unwrap_or("draft");
|
||||
let review_status = GlossaryReviewStatus::parse(review_status)
|
||||
.ok_or_else(|| anyhow::anyhow!("Glossary review status 无效:{review_status}"))?;
|
||||
let now = unix_seconds_now();
|
||||
Ok(GlossaryTermDraft {
|
||||
term_id,
|
||||
definition: GlossaryTermSnapshot {
|
||||
source_term,
|
||||
aliases,
|
||||
recommended_translation,
|
||||
allowed_translations,
|
||||
source_language: options.glossary_source_language.clone(),
|
||||
target_language: options.glossary_target_language.clone(),
|
||||
category: options.glossary_category.clone(),
|
||||
priority: options.glossary_priority,
|
||||
scope,
|
||||
},
|
||||
review_status,
|
||||
source: GlossarySourceRecord {
|
||||
source_kind,
|
||||
source_ref: options.glossary_source_ref.clone(),
|
||||
source_author: options.glossary_source_author.clone(),
|
||||
source_note: options.glossary_source_note.clone(),
|
||||
observed_unix_seconds: now,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn required_option(value: Option<&str>, label: &str) -> anyhow::Result<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| anyhow::anyhow!("Glossary 必须指定 {label}"))
|
||||
}
|
||||
|
||||
fn parse_string_array(value: Option<&str>, label: &str) -> anyhow::Result<Vec<String>> {
|
||||
let Some(value) = value else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
serde_json::from_str(value)
|
||||
.map_err(|error| anyhow::anyhow!("{label} 必须是 JSON string array:{error}"))
|
||||
}
|
||||
|
||||
fn parse_glossary_context(value: Option<&str>) -> anyhow::Result<TranslationMemoryContext> {
|
||||
let Some(value) = value else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
serde_json::from_str(value)
|
||||
.map_err(|error| anyhow::anyhow!("Glossary context 必须是 JSON object:{error}"))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_summary_report(
|
||||
path: &std::path::Path,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? {
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"path": path,
|
||||
"reason": "database_missing",
|
||||
}));
|
||||
}
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let summary = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::open(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
repository
|
||||
.summary()
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"schema_version": GLOSSARY_SCHEMA_VERSION,
|
||||
"summary": summary,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_query_report(
|
||||
path: &std::path::Path,
|
||||
source_text: Option<&str>,
|
||||
category: Option<&str>,
|
||||
review_status: Option<&str>,
|
||||
limit: usize,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let status = review_status
|
||||
.map(|value| {
|
||||
GlossaryReviewStatus::parse(value)
|
||||
.ok_or_else(|| anyhow::anyhow!("Glossary review_status 无效"))
|
||||
})
|
||||
.transpose()?;
|
||||
if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? {
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"path": path,
|
||||
"source_text": source_text,
|
||||
"terms": [],
|
||||
"reason": "database_missing",
|
||||
}));
|
||||
}
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let terms = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::open(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
repository
|
||||
.query(source_text, category, status, limit)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"source_text": source_text,
|
||||
"terms": terms,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_diagnose_report(
|
||||
path: &std::path::Path,
|
||||
source_text: &str,
|
||||
context: TranslationMemoryContext,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
if source_text.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("Glossary diagnose 的 source_text 不能为空"));
|
||||
}
|
||||
if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? {
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"path": path,
|
||||
"source_text": source_text,
|
||||
"context": context,
|
||||
"evaluation": {
|
||||
"constraints": [],
|
||||
"diagnostics": [],
|
||||
"blocked": false
|
||||
},
|
||||
"reason": "database_missing",
|
||||
}));
|
||||
}
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let evaluation = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::open(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
repository
|
||||
.diagnose(source_text, &context)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"source_text": source_text,
|
||||
"context": context,
|
||||
"evaluation": evaluation,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_mutation_report(
|
||||
path: &std::path::Path,
|
||||
draft: &GlossaryTermDraft,
|
||||
update: bool,
|
||||
reviewer: Option<&str>,
|
||||
reason: Option<String>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let term = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::new(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
let result = if update {
|
||||
repository
|
||||
.update(
|
||||
draft.clone(),
|
||||
reviewer.ok_or_else(|| anyhow::anyhow!("Glossary update 需要 reviewer"))?,
|
||||
reason,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
repository.add(draft.clone()).await
|
||||
};
|
||||
result.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"schema_version": GLOSSARY_SCHEMA_VERSION,
|
||||
"term": term,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_review_report(
|
||||
path: &std::path::Path,
|
||||
term_id: &str,
|
||||
status: GlossaryReviewStatus,
|
||||
reviewer: &str,
|
||||
reason: Option<String>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let term = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::open(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
repository
|
||||
.review(term_id, status, reviewer, reason)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"term": term,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_rpc_envelope(
|
||||
request_id: String,
|
||||
result: Result<serde_json::Value, ApiError>,
|
||||
) -> RpcEnvelope {
|
||||
match result {
|
||||
Ok(data) => rpc_envelope_ok(request_id, "ok", data),
|
||||
Err(error) => rpc_envelope_error(request_id, error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn glossary_summary_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
params,
|
||||
RPC_METHOD_GLOSSARY_SUMMARY,
|
||||
)?;
|
||||
build_glossary_summary_report(&path)
|
||||
.map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_SUMMARY, error))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_query_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let params = glossary_params(params, RPC_METHOD_GLOSSARY_QUERY)?;
|
||||
let source_text = glossary_string(¶ms, "source_text", RPC_METHOD_GLOSSARY_QUERY)?;
|
||||
let category = glossary_string(¶ms, "category", RPC_METHOD_GLOSSARY_QUERY)?;
|
||||
let review_status = glossary_string(¶ms, "review_status", RPC_METHOD_GLOSSARY_QUERY)?;
|
||||
let limit = glossary_limit(¶ms, RPC_METHOD_GLOSSARY_QUERY)?;
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
Some(&serde_json::Value::Object(params.clone())),
|
||||
RPC_METHOD_GLOSSARY_QUERY,
|
||||
)?;
|
||||
build_glossary_query_report(&path, source_text, category, review_status, limit)
|
||||
.map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_QUERY, error))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_diagnose_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let params = glossary_params(params, RPC_METHOD_GLOSSARY_DIAGNOSE)?;
|
||||
let source_text = glossary_string(¶ms, "source_text", RPC_METHOD_GLOSSARY_DIAGNOSE)?
|
||||
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DIAGNOSE, "缺少 source_text"))?;
|
||||
let context = glossary_context(¶ms, RPC_METHOD_GLOSSARY_DIAGNOSE)?;
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
Some(&serde_json::Value::Object(params.clone())),
|
||||
RPC_METHOD_GLOSSARY_DIAGNOSE,
|
||||
)?;
|
||||
build_glossary_diagnose_report(&path, source_text, context)
|
||||
.map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_DIAGNOSE, error))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_mutation_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
update: bool,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let method = if update {
|
||||
RPC_METHOD_GLOSSARY_UPDATE
|
||||
} else {
|
||||
RPC_METHOD_GLOSSARY_ADD
|
||||
};
|
||||
let params = glossary_params(params, method)?;
|
||||
let draft: GlossaryTermDraft =
|
||||
serde_json::from_value(serde_json::Value::Object(params.clone())).map_err(|error| {
|
||||
glossary_invalid(method, format!("Glossary term 参数无效:{error}"))
|
||||
})?;
|
||||
let reviewer = glossary_string(¶ms, "reviewer", method)?;
|
||||
if update && reviewer.is_none() {
|
||||
return Err(glossary_invalid(method, "update 缺少 reviewer"));
|
||||
}
|
||||
let reason = glossary_string(¶ms, "reason", method)?.map(str::to_string);
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
Some(&serde_json::Value::Object(params.clone())),
|
||||
method,
|
||||
)?;
|
||||
build_glossary_mutation_report(&path, &draft, update, reviewer, reason)
|
||||
.map_err(|error| glossary_internal_error(method, error))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_review_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
status: GlossaryReviewStatus,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let method = if status == GlossaryReviewStatus::Approved {
|
||||
RPC_METHOD_GLOSSARY_APPROVE
|
||||
} else {
|
||||
RPC_METHOD_GLOSSARY_DEPRECATE
|
||||
};
|
||||
let params = glossary_params(params, method)?;
|
||||
let term_id = glossary_string(¶ms, "term_id", method)?
|
||||
.ok_or_else(|| glossary_invalid(method, "缺少 term_id"))?;
|
||||
let reviewer = glossary_string(¶ms, "reviewer", method)?
|
||||
.ok_or_else(|| glossary_invalid(method, "缺少 reviewer"))?;
|
||||
let reason = glossary_string(¶ms, "reason", method)?.map(str::to_string);
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
Some(&serde_json::Value::Object(params.clone())),
|
||||
method,
|
||||
)?;
|
||||
build_glossary_review_report(&path, term_id, status, reviewer, reason)
|
||||
.map_err(|error| glossary_internal_error(method, error))
|
||||
}
|
||||
|
||||
fn glossary_rpc_path(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
method: &'static str,
|
||||
) -> Result<std::path::PathBuf, ApiError> {
|
||||
if let Some(path) = params
|
||||
.and_then(|value| value.get("glossary_path"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return lexical_absolute(Path::new(path))
|
||||
.map_err(|error| glossary_internal_error(method, anyhow::anyhow!(error)));
|
||||
}
|
||||
if let Some(path) = default_path {
|
||||
return lexical_absolute(path)
|
||||
.map_err(|error| glossary_internal_error(method, anyhow::anyhow!(error)));
|
||||
}
|
||||
let (_, version_state) = read_daemon_resource_state(state_dir)
|
||||
.map_err(|error| glossary_internal_error(method, error))?;
|
||||
if let Some(record) = version_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.current_completed_version.as_ref())
|
||||
{
|
||||
return Ok(SqliteGlossaryRepository::repository_path(
|
||||
&record.resource_root,
|
||||
));
|
||||
}
|
||||
Ok(SqliteGlossaryRepository::repository_path(output_root))
|
||||
}
|
||||
|
||||
fn glossary_params(
|
||||
params: Option<&serde_json::Value>,
|
||||
method: &'static str,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>, ApiError> {
|
||||
match params {
|
||||
None | Some(serde_json::Value::Null) => Ok(serde_json::Map::new()),
|
||||
Some(serde_json::Value::Object(value)) => Ok(value.clone()),
|
||||
Some(_) => Err(glossary_invalid(method, "params 必须是 JSON object")),
|
||||
}
|
||||
}
|
||||
|
||||
fn glossary_string<'a>(
|
||||
params: &'a serde_json::Map<String, serde_json::Value>,
|
||||
key: &str,
|
||||
method: &'static str,
|
||||
) -> Result<Option<&'a str>, ApiError> {
|
||||
let Some(value) = params.get(key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| glossary_invalid(method, format!("{key} 必须是非空字符串")))
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn glossary_context(
|
||||
params: &serde_json::Map<String, serde_json::Value>,
|
||||
method: &'static str,
|
||||
) -> Result<TranslationMemoryContext, ApiError> {
|
||||
let Some(value) = params
|
||||
.get("context")
|
||||
.or_else(|| params.get("source_context"))
|
||||
else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
serde_json::from_value(value.clone()).map_err(|error| {
|
||||
glossary_invalid(method, format!("context 必须是 JSON string map:{error}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn glossary_limit(
|
||||
params: &serde_json::Map<String, serde_json::Value>,
|
||||
method: &'static str,
|
||||
) -> Result<usize, ApiError> {
|
||||
let limit = params
|
||||
.get("limit")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(100);
|
||||
let limit = usize::try_from(limit)
|
||||
.map_err(|error| glossary_invalid(method, format!("limit 无效:{error}")))?;
|
||||
if !(1..=1000).contains(&limit) {
|
||||
return Err(glossary_invalid(method, "limit 必须在 1..=1000 范围内"));
|
||||
}
|
||||
Ok(limit)
|
||||
}
|
||||
|
||||
fn glossary_invalid(method: &'static str, message: impl Into<String>) -> ApiError {
|
||||
ApiError::new(ErrorCode::RPC_INVALID_PARAMS, method, message.into())
|
||||
}
|
||||
|
||||
fn glossary_internal_error(method: &'static str, error: anyhow::Error) -> ApiError {
|
||||
ApiError::new(ErrorCode::INTERNAL, method, error.to_string())
|
||||
}
|
||||
@@ -115,9 +115,15 @@ impl HumanReport for bat_infrastructure::TranslationWorkerReport {
|
||||
print_field("TM 可用", format_bool(self.translation_memory_available));
|
||||
print_field("TM 命中 TextUnit", self.translation_memory_hit_count);
|
||||
print_field("Provider TextUnit", self.provider_unit_count);
|
||||
print_path_field("Glossary", &self.glossary_path);
|
||||
print_field("Glossary 可用", format_bool(self.glossary_available));
|
||||
print_field("Glossary blocking TextUnit", self.glossary_blocked_count);
|
||||
for failure in &self.translation_memory_failures {
|
||||
println!(" - TM: {failure}");
|
||||
}
|
||||
for failure in &self.glossary_failures {
|
||||
println!(" - Glossary: {failure}");
|
||||
}
|
||||
for failure in &self.failures {
|
||||
println!(
|
||||
" - {} [{}] retryable={} {}",
|
||||
|
||||
@@ -345,6 +345,10 @@ Commands:
|
||||
i18n handoff Query current translation handoff
|
||||
i18n status Show localized release status for current official release
|
||||
i18n task update Update one provider worker task status
|
||||
i18n glossary summary/query Show project Glossary terms and review counts
|
||||
i18n glossary add/update Add or replace one Glossary term definition
|
||||
i18n glossary approve/deprecate Review one Glossary term
|
||||
i18n glossary diagnose Run deterministic Glossary QA for one TextUnit source
|
||||
i18n publish Publish a localized release from a workbench or worker results
|
||||
i18n rollback Roll back the current localized release
|
||||
i18n schedule Manage translation schedules
|
||||
@@ -384,6 +388,7 @@ Examples:
|
||||
{binary} i18n unset --translation-file /tmp/bat-workbench.json --translation-id unit-1
|
||||
{binary} i18n proofread --json
|
||||
{binary} i18n worker run --provider mock --worker-concurrency 8 --run-count 2 --interval 30s
|
||||
{binary} i18n glossary diagnose --glossary-source-text Sensei --json
|
||||
{binary} i18n tasks --json
|
||||
{binary} i18n handoff --json
|
||||
{binary} i18n status --json
|
||||
@@ -433,6 +438,15 @@ Sync:
|
||||
--provider-run-id <ID> Provider run ID for i18n task update
|
||||
--translation-provider <NAME> / --provider <NAME> Provider for i18n worker run (mock/crowdin)
|
||||
--translation-fixture <PATH> Mock/provider fixture for i18n worker run
|
||||
--glossary-path <PATH> Project Glossary SQLite path
|
||||
--glossary-term-id <ID> Glossary term ID for add/update/review
|
||||
--glossary-source-term <TEXT> Source spelling for a Glossary term
|
||||
--glossary-recommended-translation <TEXT> Recommended target translation
|
||||
--glossary-source-text <TEXT> Source TextUnit text for Glossary query/diagnose
|
||||
--glossary-context-json <JSON> TextUnit context for Glossary diagnose
|
||||
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews
|
||||
--glossary-reason <TEXT> Reason for Glossary review or override
|
||||
--glossary-provenance <TEXT> Provenance for an explicit Glossary override
|
||||
--worker-concurrency <N> Translation worker concurrency (default: 8, range 1..=256)
|
||||
--worker-max-attempts <N> Maximum claims per translation task
|
||||
--worker-lease-seconds <N> Lease seconds for one claimed task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::report_output::print_json_value;
|
||||
use super::*;
|
||||
use bat_core::domain::TranslationMemoryContext;
|
||||
use bat_core::domain::{GlossaryOverride, TranslationMemoryContext};
|
||||
use bat_core::repositories::TranslationMemoryRepository;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -9,6 +9,8 @@ struct TranslationTaskResultUpdateParam {
|
||||
unit_id: String,
|
||||
source_text: String,
|
||||
translated_text: String,
|
||||
#[serde(default)]
|
||||
glossary_override: Option<GlossaryOverride>,
|
||||
}
|
||||
|
||||
pub(super) fn build_translation_tasks_report(
|
||||
@@ -170,6 +172,7 @@ pub(super) fn build_translation_handoff_report(
|
||||
pub(super) fn update_translation_task_status_report(
|
||||
state_dir: &Path,
|
||||
params: Option<&serde_json::Value>,
|
||||
configured_glossary_path: Option<&Path>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let task_id = rpc_string_param(params, "task_id")
|
||||
.ok_or_else(|| anyhow::anyhow!("translation.task.update 缺少 task_id"))?;
|
||||
@@ -229,6 +232,22 @@ pub(super) fn update_translation_task_status_report(
|
||||
.find(task_id)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
let glossary_path = configured_glossary_path
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
bat_infrastructure::SqliteGlossaryRepository::repository_path(
|
||||
¤t.resource_root,
|
||||
)
|
||||
});
|
||||
let glossary = if std::fs::symlink_metadata(&glossary_path).is_ok() {
|
||||
Some(
|
||||
bat_infrastructure::SqliteGlossaryRepository::open(&glossary_path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("打开 Glossary 数据库失败:{error}"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let results = build_manual_translation_results(
|
||||
¤t_task,
|
||||
index,
|
||||
@@ -236,7 +255,9 @@ pub(super) fn update_translation_task_status_report(
|
||||
&result_provider,
|
||||
&result_provider_run_id,
|
||||
result_timestamp,
|
||||
)?;
|
||||
glossary.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
repository
|
||||
.update_status_with_results(
|
||||
task_id,
|
||||
@@ -291,13 +312,14 @@ fn translation_task_result_params(
|
||||
.map_err(|error| anyhow::anyhow!("translation_results 必须是结果数组:{error}"))
|
||||
}
|
||||
|
||||
fn build_manual_translation_results(
|
||||
async fn build_manual_translation_results(
|
||||
task: &bat_infrastructure::PersistedTranslationTask,
|
||||
index: &bat_infrastructure::OfficialTextUnitIndex,
|
||||
params: &[TranslationTaskResultUpdateParam],
|
||||
provider: &str,
|
||||
provider_run_id: &str,
|
||||
translated_unix_seconds: u64,
|
||||
glossary: Option<&bat_infrastructure::SqliteGlossaryRepository>,
|
||||
) -> anyhow::Result<Vec<bat_infrastructure::TranslationTaskUnitResult>> {
|
||||
let index_by_id = index
|
||||
.units
|
||||
@@ -332,6 +354,53 @@ fn build_manual_translation_results(
|
||||
"TextUnit {unit_id} 的 source_text 与当前索引不一致"
|
||||
));
|
||||
}
|
||||
let glossary_qa = if let Some(glossary) = glossary {
|
||||
let context = bat_infrastructure::translation_memory_context(
|
||||
&unit.destination,
|
||||
unit.archive_entry.as_deref(),
|
||||
unit.serialized_file.as_deref(),
|
||||
unit.path_id,
|
||||
unit.class_id,
|
||||
unit.field_path.as_deref(),
|
||||
unit.format.as_deref(),
|
||||
unit.asset_name.as_deref(),
|
||||
unit.text_source_kind.as_deref(),
|
||||
&unit.context,
|
||||
);
|
||||
Some(
|
||||
glossary
|
||||
.diagnose(&unit.source_text, &context)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("Glossary QA 失败:{error}"))?
|
||||
.check_translation(¶m.translated_text),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(qa) = glossary_qa.as_ref().filter(|qa| qa.status.is_blocked()) {
|
||||
let Some(override_record) = param.glossary_override.as_ref() else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"TextUnit {} 的 Glossary QA blocked;必须提供 glossary_override",
|
||||
unit_id
|
||||
));
|
||||
};
|
||||
if override_record.reviewer.trim().is_empty()
|
||||
|| override_record.reason.trim().is_empty()
|
||||
|| override_record.provenance.trim().is_empty()
|
||||
|| override_record.confirmed_unix_seconds == 0
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"TextUnit {} 的 glossary_override 不完整或 confirmed_unix_seconds 无效",
|
||||
unit_id
|
||||
));
|
||||
}
|
||||
let _ = qa;
|
||||
} else if param.glossary_override.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"TextUnit {} 不能为非 blocking Glossary QA 指定 override",
|
||||
unit_id
|
||||
));
|
||||
}
|
||||
results.push(bat_infrastructure::TranslationTaskUnitResult {
|
||||
unit_id: unit_id.to_string(),
|
||||
source_text: param.source_text.clone(),
|
||||
@@ -341,6 +410,8 @@ fn build_manual_translation_results(
|
||||
provider: provider.to_string(),
|
||||
provider_run_id: provider_run_id.to_string(),
|
||||
translated_unix_seconds,
|
||||
glossary_qa,
|
||||
glossary_override: param.glossary_override.clone(),
|
||||
});
|
||||
}
|
||||
Ok(results)
|
||||
|
||||
@@ -104,7 +104,12 @@ pub(super) fn run_translation_validate(options: &CliOptions) -> anyhow::Result<(
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n validate 必须指定 --translation-file"))?;
|
||||
let (resource_root, release_id) = current_official_release(options)?;
|
||||
let workbench = read_translation_workbench(path)?;
|
||||
let validation = validate_translation_workbench(&resource_root, &release_id, &workbench)?;
|
||||
let validation = validate_translation_workbench_with_glossary_path(
|
||||
&resource_root,
|
||||
&release_id,
|
||||
&workbench,
|
||||
options.glossary_path.as_deref(),
|
||||
)?;
|
||||
let data = serde_json::json!({
|
||||
"official_release_id": release_id,
|
||||
"resource_root": resource_root,
|
||||
@@ -141,7 +146,50 @@ pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
|
||||
.translation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("translation-set 必须指定 --translation-id"))?;
|
||||
let entry = set_translation(path, entry_id, text)?;
|
||||
let glossary_override = match (
|
||||
options.glossary_reviewer.as_deref(),
|
||||
options.glossary_reason.as_deref(),
|
||||
options.glossary_override_provenance.as_deref(),
|
||||
) {
|
||||
(None, None, None) => None,
|
||||
(Some(reviewer), Some(reason), Some(provenance)) => Some(
|
||||
bat_core::domain::GlossaryOverride {
|
||||
reviewer: reviewer.to_string(),
|
||||
reason: reason.to_string(),
|
||||
provenance: provenance.to_string(),
|
||||
confirmed_unix_seconds: unix_seconds_now(),
|
||||
},
|
||||
),
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary override 必须同时指定 --glossary-reviewer、--glossary-reason 和 --glossary-provenance"
|
||||
))
|
||||
}
|
||||
};
|
||||
let workbench = read_translation_workbench(path)?;
|
||||
let glossary_path = options.glossary_path.clone().unwrap_or_else(|| {
|
||||
bat_infrastructure::SqliteGlossaryRepository::repository_path(
|
||||
&workbench.official_resource_root,
|
||||
)
|
||||
});
|
||||
let entry = if std::fs::symlink_metadata(&glossary_path).is_ok() {
|
||||
let (resource_root, _) = current_official_release(options)?;
|
||||
set_translation_checked_with_glossary_path(
|
||||
&resource_root,
|
||||
path,
|
||||
entry_id,
|
||||
text,
|
||||
glossary_override,
|
||||
options.glossary_path.as_deref(),
|
||||
)?
|
||||
} else {
|
||||
if glossary_override.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"当前项目没有 Glossary 数据库,不能提交 Glossary override"
|
||||
));
|
||||
}
|
||||
set_translation(path, entry_id, text)?
|
||||
};
|
||||
let data = serde_json::json!({
|
||||
"translation_file": path,
|
||||
"entry": entry,
|
||||
@@ -232,6 +280,7 @@ pub(super) fn run_translation_task_update(options: &CliOptions) -> anyhow::Resul
|
||||
let report = update_translation_task_status_report(
|
||||
&options.state_dir,
|
||||
Some(&serde_json::Value::Object(params)),
|
||||
options.glossary_path.as_deref(),
|
||||
)?;
|
||||
print_json_value(options.output_format, &report)
|
||||
}
|
||||
@@ -316,6 +365,12 @@ pub(super) fn publish_localized_report(
|
||||
"翻译工作台资源根目录与当前 release 不一致;请重新导出"
|
||||
));
|
||||
}
|
||||
validate_translation_workbench_with_glossary_path(
|
||||
&resource_root,
|
||||
&official_release_id,
|
||||
&workbench,
|
||||
options.glossary_path.as_deref(),
|
||||
)?;
|
||||
let operations = localized_patch_operations(&resource_root, &workbench)?;
|
||||
let localized_release_id = options.localized_release_id.clone().or_else(|| {
|
||||
options
|
||||
|
||||
Reference in New Issue
Block a user