fix(glossary): 绑定 QA identity 与人工 override
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-09 00:25:49 +08:00
parent 0275a890bc
commit 68c6c91b1e
22 changed files with 844 additions and 92 deletions
+10 -5
View File
@@ -10,10 +10,10 @@ use bat_infrastructure::{
apply_unityfs_text_asset_patch_file, changed_endpoint_urls,
completed_worker_translation_workbench, diff_extended_snapshot, export_translation_workbench,
gc_orphan_staging_with_cas_root, get_translation_entry, lexical_absolute,
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,
localized_patch_operations_with_glossary_path, 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,
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,
@@ -430,6 +430,7 @@ struct CliOptions {
glossary_reviewer: Option<String>,
glossary_reason: Option<String>,
glossary_override_provenance: Option<String>,
glossary_qa_identity: Option<String>,
glossary_source_text: Option<String>,
glossary_context_json: Option<String>,
glossary_review_status: Option<String>,
@@ -555,6 +556,7 @@ impl Default for CliOptions {
glossary_reviewer: None,
glossary_reason: None,
glossary_override_provenance: None,
glossary_qa_identity: None,
glossary_source_text: None,
glossary_context_json: None,
glossary_review_status: None,
@@ -6949,6 +6951,9 @@ fn parse_args_with_env(
"--glossary-provenance" | "--glossary-override-provenance" => {
options.glossary_override_provenance = Some(next_option_value(&mut args, &flag)?);
}
"--glossary-qa-identity" => {
options.glossary_qa_identity = Some(next_option_value(&mut args, &flag)?);
}
"--glossary-source-text" => {
options.glossary_source_text = Some(next_option_value(&mut args, &flag)?);
}
@@ -7871,7 +7876,7 @@ fn parse_args_with_env(
|| tools_are_non_default(&options.config, &options.env_baseline_config)
{
return Err(anyhow::anyhow!(
"translation workbench 编辑命令只接受 --translation-file、--translation-id、译文字段、--state-dir --json/--human"
"translation workbench 编辑命令只接受 --translation-file、--translation-id、译文和 Glossary override 字段、--state-dir 以及 --json/--human"
));
}
options.progress = false;
@@ -448,6 +448,7 @@ Sync:
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews/delete
--glossary-reason <TEXT> Reason for Glossary review/delete or override
--glossary-provenance <TEXT> Provenance for an explicit Glossary override
--glossary-qa-identity <ID> Current blocking Glossary QA identity for an 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
+10 -15
View File
@@ -1,6 +1,6 @@
use super::report_output::print_json_value;
use super::*;
use bat_core::domain::{GlossaryOverride, TranslationMemoryContext};
use bat_core::domain::{validate_glossary_override, GlossaryOverride, TranslationMemoryContext};
use bat_core::repositories::TranslationMemoryRepository;
use std::collections::BTreeMap;
@@ -377,24 +377,19 @@ async fn build_manual_translation_results(
} 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 {
if let Some(qa) = glossary_qa.as_ref() {
if qa.status.is_blocked() {
validate_glossary_override(qa, param.glossary_override.as_ref()).map_err(
|error| {
anyhow::anyhow!("TextUnit {} 的 glossary_override 无效:{error}", unit_id)
},
)?;
} else if param.glossary_override.is_some() {
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 无效",
"TextUnit {} 不能为非 blocking Glossary QA 指定 override",
unit_id
));
}
let _ = qa;
} else if param.glossary_override.is_some() {
return Err(anyhow::anyhow!(
"TextUnit {} 不能为非 blocking Glossary QA 指定 override",
@@ -150,10 +150,12 @@ pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
options.glossary_reviewer.as_deref(),
options.glossary_reason.as_deref(),
options.glossary_override_provenance.as_deref(),
options.glossary_qa_identity.as_deref(),
) {
(None, None, None) => None,
(Some(reviewer), Some(reason), Some(provenance)) => Some(
(None, None, None, None) => None,
(Some(reviewer), Some(reason), Some(provenance), Some(qa_identity)) => Some(
bat_core::domain::GlossaryOverride {
qa_identity: qa_identity.to_string(),
reviewer: reviewer.to_string(),
reason: reason.to_string(),
provenance: provenance.to_string(),
@@ -162,7 +164,7 @@ pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
),
_ => {
return Err(anyhow::anyhow!(
"Glossary override 必须同时指定 --glossary-reviewer、--glossary-reason 和 --glossary-provenance"
"Glossary override 必须同时指定 --glossary-qa-identity、--glossary-reviewer、--glossary-reason 和 --glossary-provenance"
))
}
};
@@ -371,7 +373,11 @@ pub(super) fn publish_localized_report(
&workbench,
options.glossary_path.as_deref(),
)?;
let operations = localized_patch_operations(&resource_root, &workbench)?;
let operations = localized_patch_operations_with_glossary_path(
&resource_root,
&workbench,
options.glossary_path.as_deref(),
)?;
let localized_release_id = options.localized_release_id.clone().or_else(|| {
options
.config
+6 -6
View File
@@ -173,12 +173,12 @@ pub use translation_worker::{
pub use translation_workflow::{
completed_worker_translation_workbench, export_completed_worker_translation_workbench,
export_translation_workbench, get_translation_entry, localized_patch_operations,
localized_text_asset_patches, read_translation_workbench, repack_bundle, set_translation,
set_translation_checked, set_translation_checked_with_glossary_path, unset_translation,
validate_translation_workbench, validate_translation_workbench_with_glossary_path,
write_translation_workbench, RepackOperation, RepackReport, RepackSpec, TranslationWorkbench,
TranslationWorkbenchEntry, TranslationWorkbenchValidationReport, REPACK_SPEC_VERSION,
TRANSLATION_WORKBENCH_VERSION,
localized_patch_operations_with_glossary_path, localized_text_asset_patches,
read_translation_workbench, repack_bundle, set_translation, set_translation_checked,
set_translation_checked_with_glossary_path, unset_translation, validate_translation_workbench,
validate_translation_workbench_with_glossary_path, write_translation_workbench,
RepackOperation, RepackReport, RepackSpec, TranslationWorkbench, TranslationWorkbenchEntry,
TranslationWorkbenchValidationReport, REPACK_SPEC_VERSION, TRANSLATION_WORKBENCH_VERSION,
};
/// Infrastructure 版本号
+51
View File
@@ -352,6 +352,12 @@ pub struct LocalizedPatchOperation {
/// Review state used by the publication input.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub review_status: Option<String>,
/// Glossary QA recomputed for this publication.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub glossary_qa: Option<bat_core::domain::GlossaryQaReport>,
/// Human confirmation bound to the published Glossary QA identity.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub glossary_override: Option<bat_core::domain::GlossaryOverride>,
}
/// Rollback information recorded for a localized publication.
@@ -962,6 +968,8 @@ impl LocalizedPatchOperation {
translation_source_kind: None,
translation_memory_record_id: None,
review_status: None,
glossary_qa: None,
glossary_override: None,
},
metadata,
)
@@ -989,6 +997,8 @@ impl LocalizedPatchOperation {
translation_source_kind: None,
translation_memory_record_id: None,
review_status: None,
glossary_qa: None,
glossary_override: None,
},
metadata,
)
@@ -1015,6 +1025,8 @@ impl LocalizedPatchOperation {
translation_source_kind: None,
translation_memory_record_id: None,
review_status: None,
glossary_qa: None,
glossary_override: None,
},
metadata,
))
@@ -1032,6 +1044,8 @@ impl LocalizedPatchOperation {
operation.translation_source_kind = metadata.translation_source_kind.clone();
operation.translation_memory_record_id = metadata.translation_memory_record_id.clone();
operation.review_status = Some(metadata.review_status.clone());
operation.glossary_qa = metadata.glossary_qa.clone();
operation.glossary_override = metadata.glossary_override.clone();
}
operation
}
@@ -1058,6 +1072,8 @@ impl Default for LocalizedPatchOperation {
translation_source_kind: None,
translation_memory_record_id: None,
review_status: None,
glossary_qa: None,
glossary_override: None,
}
}
}
@@ -1432,6 +1448,8 @@ mod tests {
translation_source_kind: Some("provider".to_string()),
translation_memory_record_id: None,
review_status: Some("provider_completed".to_string()),
glossary_qa: None,
glossary_override: None,
}],
}],
rollback: LocalizedPatchRollbackInfo {
@@ -1461,6 +1479,39 @@ mod tests {
);
}
#[test]
fn manifest_operation_keeps_current_glossary_qa_provenance() {
let qa = bat_core::domain::GlossaryQaReport {
qa_identity: "gqa-v1-current".to_string(),
status: bat_core::domain::GlossaryQaStatus::Pass,
constraints: Vec::new(),
diagnostics: Vec::new(),
};
let input = LocalizedPatchInput::TextAsset(LocalizedTextAssetPatch {
bundle_path: "Bundles/file.bundle".to_string(),
text_asset: TextAssetPatch::new("CAB-asset", 1, b"target".to_vec()),
metadata: Some(LocalizedPatchOperationMetadata {
text_unit_id: "unit-1".to_string(),
source_text_blake3: "source-hash".to_string(),
translation_provider: None,
provider_run_id: None,
translation_source_kind: Some("manual".to_string()),
translation_memory_record_id: None,
review_status: "manual_reviewed".to_string(),
glossary_qa: Some(qa.clone()),
glossary_override: None,
}),
});
let operation = input.manifest_operation().unwrap();
assert_eq!(
operation
.glossary_qa
.as_ref()
.map(|report| report.qa_identity.as_str()),
Some("gqa-v1-current")
);
}
#[cfg(unix)]
#[test]
fn publishes_a_separate_localized_release_atomically() {
+5
View File
@@ -968,6 +968,7 @@ async fn process_claimed_task(
}
} else {
bat_core::domain::GlossaryEvaluation {
qa_identity: String::new(),
constraints: Vec::new(),
diagnostics: Vec::new(),
blocked: false,
@@ -1760,6 +1761,10 @@ mod tests {
.map(|qa| qa.status),
Some(bat_core::domain::GlossaryQaStatus::Pass)
);
assert!(task.translation_results[0]
.glossary_qa
.as_ref()
.is_some_and(|qa| !qa.qa_identity.is_empty()));
}
#[tokio::test]
+309 -26
View File
@@ -16,7 +16,10 @@ use bat_assetbundle::{
patch_unityfs_field, patch_unityfs_string_field, patch_unityfs_text_asset, FieldPatch,
StringFieldPatch, TextAssetPatch, UnitySerializedReplacementValue,
};
use bat_core::domain::{GlossaryOverride, GlossaryQaReport};
use bat_core::domain::{
validate_glossary_override as validate_core_glossary_override, GlossaryOverride,
GlossaryQaReport,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
@@ -333,9 +336,8 @@ pub fn set_translation_checked_with_glossary_path(
.as_ref()
.map(|glossary| evaluate_glossary_entry(glossary, &current, &translated_text))
.transpose()?;
if let Some(qa) = qa.as_ref().filter(|qa| qa.status.is_blocked()) {
validate_glossary_override(glossary_override.as_ref())?;
let _ = qa;
if let Some(qa) = qa.as_ref() {
validate_current_glossary_qa(qa, glossary_override.as_ref())?;
} else if glossary_override.is_some() {
return Err(anyhow::anyhow!(
"Glossary override 只能用于存在 blocking QA 的译文"
@@ -464,9 +466,12 @@ pub fn validate_translation_workbench_with_glossary_path(
}
if let Some(glossary) = glossary.as_ref() {
let qa = evaluate_glossary_entry(glossary, current, translated_text)?;
if qa.status.is_blocked() {
validate_glossary_override(entry.glossary_override.as_ref())?;
}
validate_current_glossary_qa(&qa, entry.glossary_override.as_ref())?;
} else if entry.glossary_override.is_some() {
return Err(anyhow::anyhow!(
"TextUnit {} 存在 Glossary override,但当前没有 Glossary 数据库",
entry.id
));
}
changed_entries += 1;
let source_kind = normalized_text_source_kind(entry.text_source_kind.as_deref());
@@ -560,20 +565,15 @@ fn evaluate_glossary_entry(
.map_err(|error| anyhow::anyhow!("执行 Glossary QA 失败:{error}"))
}
fn validate_glossary_override(glossary_override: Option<&GlossaryOverride>) -> anyhow::Result<()> {
let Some(glossary_override) = glossary_override else {
return Err(anyhow::anyhow!(
"Glossary QA blocked;需要 reviewer、reason 和 provenance 显式确认"
));
};
if glossary_override.reviewer.trim().is_empty()
|| glossary_override.reason.trim().is_empty()
|| glossary_override.provenance.trim().is_empty()
|| glossary_override.confirmed_unix_seconds == 0
{
return Err(anyhow::anyhow!(
"Glossary override 的 reviewer、reason、provenance 和 confirmed_unix_seconds 必须有效"
));
fn validate_current_glossary_qa(
qa: &GlossaryQaReport,
glossary_override: Option<&GlossaryOverride>,
) -> anyhow::Result<()> {
if qa.status.is_blocked() {
validate_core_glossary_override(qa, glossary_override)
.map_err(|error| anyhow::anyhow!("{error}"))?;
} else if glossary_override.is_some() {
return Err(anyhow::anyhow!("Glossary override 只能用于 blocking QA"));
}
Ok(())
}
@@ -587,6 +587,23 @@ pub fn localized_patch_operations(
resource_root: &Path,
workbench: &TranslationWorkbench,
) -> anyhow::Result<Vec<LocalizedPatchInput>> {
localized_patch_operations_with_glossary_path(resource_root, workbench, None)
}
/// Converts reviewed entries to patch operations using QA recomputed from the
/// current Glossary. The recomputed report is the one persisted to the
/// localized release manifest.
pub fn localized_patch_operations_with_glossary_path(
resource_root: &Path,
workbench: &TranslationWorkbench,
configured_glossary_path: Option<&Path>,
) -> anyhow::Result<Vec<LocalizedPatchInput>> {
validate_translation_workbench_with_glossary_path(
resource_root,
&workbench.official_release_id,
workbench,
configured_glossary_path,
)?;
let index = read_textunit_index_at(resource_root)
.map_err(anyhow::Error::msg)?
.ok_or_else(|| anyhow::anyhow!("缺少当前官方 TextUnit 索引"))?;
@@ -595,6 +612,10 @@ pub fn localized_patch_operations(
.iter()
.map(|unit| (unit.id.as_str(), unit))
.collect::<std::collections::HashMap<_, _>>();
let glossary_path = configured_glossary_path
.map(PathBuf::from)
.unwrap_or_else(|| SqliteGlossaryRepository::repository_path(resource_root));
let glossary = open_glossary_if_present(&glossary_path)?;
let mut seen = BTreeSet::new();
let mut operations = Vec::new();
@@ -609,6 +630,18 @@ pub fn localized_patch_operations(
if translated_text == &entry.source_text {
continue;
}
let current_qa = glossary
.as_ref()
.map(|glossary| evaluate_glossary_entry(glossary, current, translated_text))
.transpose()?;
if let Some(qa) = current_qa.as_ref() {
validate_current_glossary_qa(qa, entry.glossary_override.as_ref())?;
} else if entry.glossary_override.is_some() {
return Err(anyhow::anyhow!(
"TextUnit {} 存在 Glossary override,但当前没有 Glossary 数据库",
entry.id
));
}
let Some(serialized_file) = entry.serialized_file.clone() else {
return Err(anyhow::anyhow!(
"TextUnit {} 没有 serialized_file,当前不能生成重打包 patch",
@@ -640,7 +673,11 @@ pub fn localized_patch_operations(
entry.id
));
}
let metadata = Some(localized_patch_metadata(entry));
let metadata = Some(localized_patch_metadata(
entry,
current_qa.as_ref(),
entry.glossary_override.as_ref(),
));
match source_kind.as_deref() {
Some("textasset") => {
let mut patch = TextAssetPatch::new(
@@ -706,6 +743,8 @@ pub fn localized_text_asset_patches(
.iter()
.map(|unit| (unit.id.as_str(), unit))
.collect::<std::collections::HashMap<_, _>>();
let glossary =
open_glossary_if_present(&SqliteGlossaryRepository::repository_path(resource_root))?;
let mut seen = BTreeSet::new();
let mut patches = Vec::new();
@@ -758,10 +797,26 @@ pub fn localized_text_asset_patches(
translated_text.as_bytes().to_vec(),
);
patch.expected_name = entry.asset_name.clone();
let current_qa = glossary
.as_ref()
.map(|glossary| evaluate_glossary_entry(glossary, current, translated_text))
.transpose()?;
if let Some(qa) = current_qa.as_ref() {
validate_current_glossary_qa(qa, entry.glossary_override.as_ref())?;
} else if entry.glossary_override.is_some() {
return Err(anyhow::anyhow!(
"TextUnit {} 存在 Glossary override,但当前没有 Glossary 数据库",
entry.id
));
}
patches.push(LocalizedTextAssetPatch {
bundle_path: entry.destination.clone(),
text_asset: patch,
metadata: Some(localized_patch_metadata(entry)),
metadata: Some(localized_patch_metadata(
entry,
current_qa.as_ref(),
entry.glossary_override.as_ref(),
)),
});
}
@@ -773,7 +828,11 @@ pub fn localized_text_asset_patches(
Ok(patches)
}
fn localized_patch_metadata(entry: &TranslationWorkbenchEntry) -> LocalizedPatchOperationMetadata {
fn localized_patch_metadata(
entry: &TranslationWorkbenchEntry,
glossary_qa: Option<&GlossaryQaReport>,
glossary_override: Option<&GlossaryOverride>,
) -> LocalizedPatchOperationMetadata {
LocalizedPatchOperationMetadata {
text_unit_id: entry.id.clone(),
source_text_blake3: blake3::hash(entry.source_text.as_bytes())
@@ -787,8 +846,8 @@ fn localized_patch_metadata(entry: &TranslationWorkbenchEntry) -> LocalizedPatch
.review_status
.clone()
.unwrap_or_else(|| "manual_reviewed".to_string()),
glossary_qa: entry.glossary_qa.clone(),
glossary_override: entry.glossary_override.clone(),
glossary_qa: glossary_qa.cloned(),
glossary_override: glossary_override.cloned(),
}
}
@@ -1251,6 +1310,172 @@ mod tests {
assert_eq!(report.unreviewed_entries, 0);
}
#[test]
fn checked_translation_rejects_override_after_glossary_change() {
let temp = tempfile::TempDir::new().unwrap();
let index = crate::official_parse::OfficialTextUnitIndex {
version: crate::official_parse::OFFICIAL_TEXTUNIT_INDEX_VERSION,
generated_unix_seconds: 1,
resource_root: temp.path().to_path_buf(),
summary: Default::default(),
units: vec![OfficialTextUnitIndexUnit {
id: "unit-1".to_string(),
parse_entry_key: "bundle".to_string(),
source_url: "https://example.invalid/bundle".to_string(),
destination: "bundles/test.bundle".to_string(),
archive_entry: None,
source_kind: crate::official_parse::OfficialParseSourceKind::DirectBundle,
unity_version: None,
source_text: "原文".to_string(),
serialized_file: Some("CAB-test".to_string()),
path_id: Some(7),
class_id: Some(49),
field_path: None,
field_offset: None,
field_byte_size: None,
format: Some("plain".to_string()),
text_source_kind: Some("text_asset".to_string()),
asset_name: Some("Story".to_string()),
context: Default::default(),
}],
errors: Vec::new(),
};
crate::official_parse::write_textunit_index_at(temp.path(), &index).unwrap();
let workbench_path = temp.path().join("workbench.json");
write_translation_workbench(&workbench_path, &workbench(temp.path())).unwrap();
let glossary_path = temp.path().join("glossary.sqlite");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let qa = runtime.block_on(async {
let glossary = SqliteGlossaryRepository::new(&glossary_path).await.unwrap();
glossary
.add(bat_core::domain::GlossaryTermDraft {
term_id: "term-source".to_string(),
definition: bat_core::domain::GlossaryTermSnapshot {
source_term: "原文".to_string(),
aliases: Vec::new(),
recommended_translation: "译文".to_string(),
allowed_translations: Vec::new(),
source_language: None,
target_language: None,
category: None,
priority: 1,
scope: BTreeMap::new(),
},
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
source: bat_core::domain::GlossarySourceRecord {
source_kind: bat_core::domain::GlossarySourceKind::Manual,
source_ref: None,
source_author: None,
source_note: None,
observed_unix_seconds: 1,
},
})
.await
.unwrap();
glossary
.review(
"term-source",
bat_core::domain::GlossaryReviewStatus::Approved,
"reviewer",
Some("approve".to_string()),
)
.await
.unwrap();
let context = crate::translation_memory::translation_memory_context(
"bundles/test.bundle",
None,
Some("CAB-test"),
Some(7),
Some(49),
None,
Some("plain"),
Some("Story"),
Some("text_asset"),
&BTreeMap::new(),
);
let evaluation = glossary.diagnose("原文", &context).await.unwrap();
(glossary, evaluation.check_translation("错误"))
});
let (glossary, qa) = qa;
let override_record = GlossaryOverride {
qa_identity: qa.qa_identity.clone(),
reviewer: "reviewer".to_string(),
reason: "manual review".to_string(),
provenance: "workbench".to_string(),
confirmed_unix_seconds: 1,
};
let updated = set_translation_checked_with_glossary_path(
temp.path(),
&workbench_path,
"unit-1",
"错误".to_string(),
Some(override_record.clone()),
Some(&glossary_path),
)
.unwrap();
assert_eq!(
updated
.glossary_qa
.as_ref()
.map(|report| report.qa_identity.as_str()),
Some(qa.qa_identity.as_str())
);
let loaded = read_translation_workbench(&workbench_path).unwrap();
let validation = validate_translation_workbench_with_glossary_path(
temp.path(),
"release-1",
&loaded,
Some(&glossary_path),
)
.unwrap();
assert_eq!(validation.status, "valid");
runtime.block_on(async {
let current = glossary.find("term-source").await.unwrap();
glossary
.update(
bat_core::domain::GlossaryTermDraft {
term_id: current.term_id,
definition: bat_core::domain::GlossaryTermSnapshot {
recommended_translation: "新译文".to_string(),
..current.definition
},
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
source: bat_core::domain::GlossarySourceRecord {
observed_unix_seconds: 2,
..current.source
},
},
"reviewer",
Some("change recommendation".to_string()),
)
.await
.unwrap();
glossary
.review(
"term-source",
bat_core::domain::GlossaryReviewStatus::Approved,
"reviewer",
Some("approve changed term".to_string()),
)
.await
.unwrap();
});
let error = set_translation_checked_with_glossary_path(
temp.path(),
&workbench_path,
"unit-1",
"错误".to_string(),
Some(override_record),
Some(&glossary_path),
)
.unwrap_err();
assert!(error.to_string().contains("qa_identity"));
}
#[test]
fn localized_operations_preserve_type_tree_field_traceability() {
let temp = tempfile::TempDir::new().unwrap();
@@ -1290,6 +1515,59 @@ mod tests {
entry.translated_text = Some("译文".to_string());
entry.translation_provider = Some("mock".to_string());
entry.provider_run_id = Some("run-1".to_string());
entry.glossary_qa = Some(GlossaryQaReport {
qa_identity: "stale-workbench-qa".to_string(),
status: bat_core::domain::GlossaryQaStatus::Pass,
constraints: Vec::new(),
diagnostics: Vec::new(),
});
let glossary_path = temp.path().join("glossary.sqlite");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let expected_qa = runtime.block_on(async {
let glossary = SqliteGlossaryRepository::new(&glossary_path).await.unwrap();
glossary
.add(bat_core::domain::GlossaryTermDraft {
term_id: "term-source".to_string(),
definition: bat_core::domain::GlossaryTermSnapshot {
source_term: "原文".to_string(),
aliases: Vec::new(),
recommended_translation: "译文".to_string(),
allowed_translations: Vec::new(),
source_language: None,
target_language: None,
category: None,
priority: 1,
scope: BTreeMap::new(),
},
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
source: bat_core::domain::GlossarySourceRecord {
source_kind: bat_core::domain::GlossarySourceKind::Manual,
source_ref: None,
source_author: None,
source_note: None,
observed_unix_seconds: 1,
},
})
.await
.unwrap();
glossary
.review(
"term-source",
bat_core::domain::GlossaryReviewStatus::Approved,
"reviewer",
Some("approve".to_string()),
)
.await
.unwrap();
glossary
.diagnose("原文", &BTreeMap::new())
.await
.unwrap()
.check_translation("译文")
});
let operations = localized_patch_operations(temp.path(), &workbench).unwrap();
assert_eq!(operations.len(), 1);
@@ -1305,6 +1583,11 @@ mod tests {
assert_eq!(metadata.text_unit_id, "unit-1");
assert_eq!(metadata.translation_provider.as_deref(), Some("mock"));
assert_eq!(metadata.provider_run_id.as_deref(), Some("run-1"));
assert_eq!(
metadata.glossary_qa.as_ref().map(|qa| qa.status),
Some(bat_core::domain::GlossaryQaStatus::Pass)
);
assert_eq!(metadata.glossary_qa.as_ref(), Some(&expected_qa));
}
other => panic!("unexpected localized operation: {other:?}"),
}