feat(translation): add Rust Translation Memory and config migration

This commit is contained in:
2026-09-06 22:48:51 +08:00
parent 93f4bc69b3
commit 7d6389806b
18 changed files with 4414 additions and 384 deletions
@@ -1,4 +1,8 @@
use super::report_output::print_json_value;
use super::*;
use bat_core::domain::TranslationMemoryContext;
use bat_core::repositories::TranslationMemoryRepository;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
struct TranslationTaskResultUpdateParam {
@@ -332,6 +336,8 @@ fn build_manual_translation_results(
unit_id: unit_id.to_string(),
source_text: param.source_text.clone(),
translated_text: param.translated_text.clone(),
source_kind: bat_infrastructure::TranslationTaskResultSourceKind::Manual,
translation_memory_record_id: None,
provider: provider.to_string(),
provider_run_id: provider_run_id.to_string(),
translated_unix_seconds,
@@ -355,3 +361,241 @@ pub(super) fn translation_task_query_json(query: &OfficialTextUnitTaskQuery) ->
"has_failure_reason": query.has_failure_reason,
})
}
pub(super) fn run_translation_memory_command(options: &CliOptions) -> anyhow::Result<()> {
let method = match options.command {
CliCommand::TranslationMemorySummary => RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
CliCommand::TranslationMemoryQuery => RPC_METHOD_TRANSLATION_MEMORY_QUERY,
CliCommand::TranslationMemoryConfirm => RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
_ => return Err(anyhow::anyhow!("不是 Translation Memory 命令")),
};
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,
translation_memory_cli_params(options)?,
)?;
print_json_value(options.output_format, &report)?;
return Ok(());
}
let path = translation_memory_cli_path(options)?;
let report = match options.command {
CliCommand::TranslationMemorySummary => build_translation_memory_summary_report(&path)?,
CliCommand::TranslationMemoryQuery => {
let source_text = options
.translation_memory_source_text
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM query 必须指定 --tm-source-text"))?;
let context = parse_translation_memory_context(
options.translation_memory_context_json.as_deref(),
)?;
build_translation_memory_query_report(
&path,
source_text,
&context,
options.query_limit,
)?
}
CliCommand::TranslationMemoryConfirm => {
let record_id = options
.translation_memory_record_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-record-id"))?;
let reviewer = options
.translation_memory_reviewer
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-reviewer"))?;
build_translation_memory_confirm_report(
&path,
record_id,
reviewer,
options.translation_memory_reason.clone(),
)?
}
_ => unreachable!(),
};
print_json_value(options.output_format, &report)
}
fn translation_memory_cli_path(options: &CliOptions) -> anyhow::Result<std::path::PathBuf> {
if let Some(path) = options.translation_memory_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(bat_infrastructure::translation_memory_repository_path(
&resource_root,
))
}
fn translation_memory_cli_params(
options: &CliOptions,
) -> anyhow::Result<Option<serde_json::Value>> {
let mut params = serde_json::Map::new();
if let Some(path) = options.translation_memory_path.as_ref() {
params.insert(
"translation_memory_path".to_string(),
serde_json::json!(path),
);
}
match options.command {
CliCommand::TranslationMemorySummary => {}
CliCommand::TranslationMemoryQuery => {
let source_text = options
.translation_memory_source_text
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM query 必须指定 --tm-source-text"))?;
let context = parse_translation_memory_context(
options.translation_memory_context_json.as_deref(),
)?;
params.insert("source_text".to_string(), serde_json::json!(source_text));
params.insert("source_context".to_string(), serde_json::json!(context));
params.insert("limit".to_string(), serde_json::json!(options.query_limit));
}
CliCommand::TranslationMemoryConfirm => {
let record_id = options
.translation_memory_record_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-record-id"))?;
let reviewer = options
.translation_memory_reviewer
.as_deref()
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-reviewer"))?;
params.insert("record_id".to_string(), serde_json::json!(record_id));
params.insert("reviewer".to_string(), serde_json::json!(reviewer));
if let Some(reason) = options.translation_memory_reason.as_deref() {
params.insert("reason".to_string(), serde_json::json!(reason));
}
}
_ => unreachable!(),
}
Ok(Some(serde_json::Value::Object(params)))
}
pub(super) fn build_translation_memory_summary_report(
path: &std::path::Path,
) -> anyhow::Result<serde_json::Value> {
if !sqlite_file_exists_no_symlink(path, "Translation Memory 数据库")? {
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 = bat_infrastructure::SqliteTranslationMemoryRepository::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": summary.schema_version,
"summary": summary,
}))
}
pub(super) fn build_translation_memory_query_report(
path: &std::path::Path,
source_text: &str,
source_context: &TranslationMemoryContext,
limit: usize,
) -> anyhow::Result<serde_json::Value> {
if source_text.trim().is_empty() {
return Err(anyhow::anyhow!("TM query 的 source_text 不能为空"));
}
if !(1..=1000).contains(&limit) {
return Err(anyhow::anyhow!("TM query 的 limit 必须在 1..=1000 范围内"));
}
if !sqlite_file_exists_no_symlink(path, "Translation Memory 数据库")? {
return Ok(serde_json::json!({
"available": false,
"path": path,
"source_text": source_text,
"source_context": source_context,
"matches": [],
"reason": "database_missing",
}));
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let matches = runtime.block_on(async {
let repository = bat_infrastructure::SqliteTranslationMemoryRepository::open(path)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.find_matches(source_text, source_context, limit)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
})?;
Ok(serde_json::json!({
"available": true,
"path": path,
"source_text": source_text,
"source_context": source_context,
"matches": matches,
}))
}
pub(super) fn build_translation_memory_confirm_report(
path: &std::path::Path,
record_id: &str,
reviewer: &str,
reason: Option<String>,
) -> anyhow::Result<serde_json::Value> {
if record_id.trim().is_empty() || reviewer.trim().is_empty() {
return Err(anyhow::anyhow!(
"TM confirm 必须指定非空 record_id 和 reviewer"
));
}
if !sqlite_file_exists_no_symlink(path, "Translation Memory 数据库")? {
return Err(anyhow::anyhow!(
"Translation Memory 数据库不存在:{}",
path.display()
));
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let entry = runtime.block_on(async {
let repository = bat_infrastructure::SqliteTranslationMemoryRepository::open(path)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.confirm(record_id, reviewer, reason)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
})?;
Ok(serde_json::json!({
"available": true,
"path": path,
"entry": entry,
}))
}
fn parse_translation_memory_context(
value: Option<&str>,
) -> anyhow::Result<TranslationMemoryContext> {
let Some(value) = value else {
return Ok(BTreeMap::new());
};
serde_json::from_str::<TranslationMemoryContext>(value)
.map_err(|error| anyhow::anyhow!("--tm-context-json 必须是 JSON object{error}"))
}