mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
@@ -8,18 +8,19 @@ use bat_infrastructure::DEFAULT_DOWNLOAD_CONCURRENCY;
|
||||
use bat_infrastructure::{
|
||||
apply_patch_file, apply_unityfs_field_patch_file, apply_unityfs_string_field_patch_file,
|
||||
apply_unityfs_text_asset_patch_file, changed_endpoint_urls, diff_extended_snapshot,
|
||||
export_translation_workbench, gc_orphan_staging, lexical_absolute,
|
||||
export_translation_workbench, gc_orphan_staging, get_translation_entry, lexical_absolute,
|
||||
localized_text_asset_patches, 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,
|
||||
validate_output_root, validate_runtime_state_dir, validate_translation_workbench,
|
||||
write_file_atomic, write_official_textunit_queues, CurlProxyConfig, CurlProxyMode,
|
||||
LocalizedPatchConfig, LocalizedPatchReport, LocalizedPatchService, OfficialEndpointMarkerRole,
|
||||
OfficialFailedVersionRecord, OfficialParseCacheService, OfficialParseConfig,
|
||||
OfficialResourceHashVerification, OfficialResourceVerification, OfficialServerInfoSource,
|
||||
OfficialTextUnitQuery, OfficialTextUnitTaskQuery, OfficialUpdateConfig, OfficialUpdateProgress,
|
||||
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
||||
unset_translation, validate_output_root, validate_runtime_state_dir,
|
||||
validate_translation_workbench, write_file_atomic, write_official_textunit_queues,
|
||||
CurlProxyConfig, CurlProxyMode, LocalizedPatchConfig, LocalizedPatchReport,
|
||||
LocalizedPatchService, OfficialEndpointMarkerRole, OfficialFailedVersionRecord,
|
||||
OfficialParseCacheService, OfficialParseConfig, OfficialResourceHashVerification,
|
||||
OfficialResourceVerification, OfficialServerInfoSource, OfficialTextUnitQuery,
|
||||
OfficialTextUnitTaskQuery, OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport,
|
||||
OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
||||
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, RepackReport,
|
||||
SqliteResourceRepository, SqliteTranslationTaskRepository, TranslationTaskStatus,
|
||||
@@ -80,7 +81,8 @@ use translation_query::{
|
||||
};
|
||||
use workflow_commands::{
|
||||
run_parse_clear_cache, run_parse_once, run_publish_localized, run_repack, run_translate_once,
|
||||
run_translation_set, run_translation_task_update, run_translation_validate,
|
||||
run_translation_get, run_translation_set, run_translation_task_update, run_translation_unset,
|
||||
run_translation_validate,
|
||||
};
|
||||
|
||||
const EXIT_ERROR: i32 = 1;
|
||||
@@ -209,6 +211,14 @@ fn run() -> anyhow::Result<i32> {
|
||||
run_translation_set(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationGet => {
|
||||
run_translation_get(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationUnset => {
|
||||
run_translation_unset(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationTaskUpdate => {
|
||||
run_translation_task_update(&options)?;
|
||||
Ok(0)
|
||||
@@ -527,6 +537,8 @@ enum CliCommand {
|
||||
Translate,
|
||||
TranslationValidate,
|
||||
TranslationSet,
|
||||
TranslationGet,
|
||||
TranslationUnset,
|
||||
TranslationTaskUpdate,
|
||||
Repack,
|
||||
PublishLocalized,
|
||||
@@ -7207,12 +7219,58 @@ fn parse_args_with_env(
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::TranslationSet | CliCommand::Repack => {
|
||||
CliCommand::TranslationSet | CliCommand::TranslationGet | CliCommand::TranslationUnset => {
|
||||
if options.watch || options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!("translation set/repack 只支持单次执行"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"translation workbench 编辑命令只支持单次执行"
|
||||
));
|
||||
}
|
||||
if options.config.force || options.sync_option_explicit || options.run_count.is_some() {
|
||||
return Err(anyhow::anyhow!("translation set/repack 不接受资源同步选项"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"translation workbench 编辑命令不接受资源同步选项"
|
||||
));
|
||||
}
|
||||
if options.translation_file.is_none() || options.translation_id.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"translation workbench 编辑命令必须指定 --translation-file 和 --translation-id"
|
||||
));
|
||||
}
|
||||
if matches!(options.command, CliCommand::TranslationSet) {
|
||||
if options.translation_text.is_some() == options.translation_text_file.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n set 必须且只能指定 --translated-text 或 --translated-file"
|
||||
));
|
||||
}
|
||||
} else if options.translation_text.is_some() || options.translation_text_file.is_some()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n get/unset 不接受 --translated-text 或 --translated-file"
|
||||
));
|
||||
}
|
||||
if options.output_explicit
|
||||
|| options.resource_root.is_some()
|
||||
|| options.localized_release_id.is_some()
|
||||
|| options.repack_spec.is_some()
|
||||
|| options.query_option_explicit
|
||||
|| options.schedule_option_explicit
|
||||
|| options.translation_failure_reason.is_some()
|
||||
|| options.translation_provider_run_id.is_some()
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config, &options.env_baseline_config)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"translation workbench 编辑命令只接受 --translation-file、--translation-id、译文字段、--state-dir 和 --json/--human"
|
||||
));
|
||||
}
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::Repack => {
|
||||
if options.watch || options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!("repack 只支持单次执行"));
|
||||
}
|
||||
if options.config.force || options.sync_option_explicit || options.run_count.is_some() {
|
||||
return Err(anyhow::anyhow!("repack 不接受资源同步选项"));
|
||||
}
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
@@ -7461,10 +7519,15 @@ fn parse_translation_command(
|
||||
if action == "task" {
|
||||
return parse_translation_task_command(args, options);
|
||||
}
|
||||
if action == "workbench" || action == "wb" {
|
||||
return parse_translation_workbench_command(args, options);
|
||||
}
|
||||
let command = match action.as_str() {
|
||||
"run" => CliCommand::Translate,
|
||||
"export" => CliCommand::Translate,
|
||||
"set" => CliCommand::TranslationSet,
|
||||
"get" | "show" => CliCommand::TranslationGet,
|
||||
"unset" | "clear" => CliCommand::TranslationUnset,
|
||||
"validate" => CliCommand::TranslationValidate,
|
||||
"publish" => CliCommand::PublishLocalized,
|
||||
"tasks" => CliCommand::TranslationTasks,
|
||||
@@ -7481,6 +7544,28 @@ fn parse_translation_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_workbench_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
) -> anyhow::Result<()> {
|
||||
let action = next_option_value(args, "translation workbench")?;
|
||||
let command = match action.as_str() {
|
||||
"export" => CliCommand::Translate,
|
||||
"set" => CliCommand::TranslationSet,
|
||||
"get" | "show" => CliCommand::TranslationGet,
|
||||
"unset" | "clear" => CliCommand::TranslationUnset,
|
||||
"validate" => CliCommand::TranslationValidate,
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"未知 translation workbench 二级命令:{other}"
|
||||
))
|
||||
}
|
||||
};
|
||||
ensure_command_not_set(options.command, &format!("translation workbench {action}"))?;
|
||||
options.command = command;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_task_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
@@ -7606,6 +7691,8 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" i18n run Refresh offline translation work");
|
||||
eprintln!(" i18n export Export an editable translation workbench");
|
||||
eprintln!(" i18n set Update one translation workbench entry");
|
||||
eprintln!(" i18n get Show one translation workbench entry");
|
||||
eprintln!(" i18n unset Clear one translated workbench entry");
|
||||
eprintln!(" i18n validate Validate workbench against the current official release");
|
||||
eprintln!(" i18n task update Update one provider worker task status");
|
||||
eprintln!(" i18n publish Publish a localized release");
|
||||
@@ -7641,6 +7728,12 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" {binary} res pull --auto-discover --run-count 3 --interval 1h");
|
||||
eprintln!(" {binary} parse run --force --resource-root /tmp/bat-release");
|
||||
eprintln!(" {binary} i18n export --translation-file /tmp/bat-workbench.json");
|
||||
eprintln!(
|
||||
" {binary} i18n get --translation-file /tmp/bat-workbench.json --translation-id unit-1"
|
||||
);
|
||||
eprintln!(
|
||||
" {binary} i18n unset --translation-file /tmp/bat-workbench.json --translation-id unit-1"
|
||||
);
|
||||
eprintln!(" {binary} i18n publish --translation-file /tmp/bat-workbench.json --force");
|
||||
eprintln!(" {binary} status");
|
||||
eprintln!(" {binary} refresh --force --json");
|
||||
|
||||
@@ -229,6 +229,31 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationValidate);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"get",
|
||||
"--translation-file",
|
||||
"/tmp/workbench.json",
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationGet);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"workbench",
|
||||
"unset",
|
||||
"--translation-file",
|
||||
"/tmp/workbench.json",
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationUnset);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
@@ -261,6 +286,18 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
Some("provider-run-1")
|
||||
);
|
||||
assert!(parse(&["bat", "i18n", "task", "update", "--task-id", "task-only",]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"get",
|
||||
"--translation-file",
|
||||
"/tmp/workbench.json",
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
"--translated-text",
|
||||
"多余文本",
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
@@ -289,6 +326,74 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_workbench_commands_read_update_and_clear_entries() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let path = temp.path().join("workbench.json");
|
||||
let workbench = bat_infrastructure::TranslationWorkbench {
|
||||
schema_version: bat_infrastructure::TRANSLATION_WORKBENCH_VERSION,
|
||||
official_release_id: "release-1".to_string(),
|
||||
official_resource_root: temp.path().to_path_buf(),
|
||||
generated_unix_seconds: 1,
|
||||
entries: vec![bat_infrastructure::TranslationWorkbenchEntry {
|
||||
id: "unit-1".to_string(),
|
||||
destination: "bundles/test.bundle".to_string(),
|
||||
archive_entry: None,
|
||||
serialized_file: Some("CAB-test".to_string()),
|
||||
path_id: Some(7),
|
||||
asset_name: Some("Story".to_string()),
|
||||
source_text: "原文".to_string(),
|
||||
translated_text: None,
|
||||
format: Some("plain".to_string()),
|
||||
text_source_kind: Some("text_asset".to_string()),
|
||||
}],
|
||||
};
|
||||
bat_infrastructure::write_translation_workbench(&path, &workbench).unwrap();
|
||||
let path_arg = path.to_string_lossy().to_string();
|
||||
|
||||
let set = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"set",
|
||||
"--translation-file",
|
||||
&path_arg,
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
"--translated-text",
|
||||
"译文",
|
||||
])
|
||||
.unwrap();
|
||||
run_translation_set(&set).unwrap();
|
||||
|
||||
let get = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"get",
|
||||
"--translation-file",
|
||||
&path_arg,
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
])
|
||||
.unwrap();
|
||||
run_translation_get(&get).unwrap();
|
||||
|
||||
let unset = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"workbench",
|
||||
"clear",
|
||||
"--translation-file",
|
||||
&path_arg,
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
])
|
||||
.unwrap();
|
||||
run_translation_unset(&unset).unwrap();
|
||||
|
||||
let loaded = bat_infrastructure::read_translation_workbench(&path).unwrap();
|
||||
assert_eq!(loaded.entries[0].translated_text, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_command_long_aliases_and_schedule_options_are_accepted() {
|
||||
let options = parse(&[
|
||||
|
||||
@@ -156,6 +156,56 @@ pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn run_translation_get(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let path = options
|
||||
.translation_file
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n get 必须指定 --translation-file"))?;
|
||||
let entry_id = options
|
||||
.translation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n get 必须指定 --translation-id"))?;
|
||||
let entry = get_translation_entry(path, entry_id)?;
|
||||
let data = serde_json::json!({
|
||||
"translation_file": path,
|
||||
"entry": entry,
|
||||
});
|
||||
print_report(
|
||||
options.output_format,
|
||||
&CommandReport {
|
||||
command: "translation-get",
|
||||
status: "ok",
|
||||
message: "翻译工作台条目已读取",
|
||||
data,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn run_translation_unset(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let path = options
|
||||
.translation_file
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n unset 必须指定 --translation-file"))?;
|
||||
let entry_id = options
|
||||
.translation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n unset 必须指定 --translation-id"))?;
|
||||
let entry = unset_translation(path, entry_id)?;
|
||||
let data = serde_json::json!({
|
||||
"translation_file": path,
|
||||
"entry": entry,
|
||||
});
|
||||
print_report(
|
||||
options.output_format,
|
||||
&CommandReport {
|
||||
command: "translation-unset",
|
||||
status: "updated",
|
||||
message: "翻译工作台条目已恢复为未审核",
|
||||
data,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn run_translation_task_update(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let task_id = options
|
||||
.query_task_id
|
||||
|
||||
@@ -140,9 +140,10 @@ pub use translation_tasks::{
|
||||
TRANSLATION_TASK_SCHEMA_VERSION,
|
||||
};
|
||||
pub use translation_workflow::{
|
||||
export_translation_workbench, localized_text_asset_patches, read_translation_workbench,
|
||||
repack_bundle, set_translation, validate_translation_workbench, write_translation_workbench,
|
||||
RepackOperation, RepackReport, RepackSpec, TranslationWorkbench, TranslationWorkbenchEntry,
|
||||
export_translation_workbench, get_translation_entry, localized_text_asset_patches,
|
||||
read_translation_workbench, repack_bundle, set_translation, unset_translation,
|
||||
validate_translation_workbench, write_translation_workbench, RepackOperation, RepackReport,
|
||||
RepackSpec, TranslationWorkbench, TranslationWorkbenchEntry,
|
||||
TranslationWorkbenchValidationReport, REPACK_SPEC_VERSION, TRANSLATION_WORKBENCH_VERSION,
|
||||
};
|
||||
|
||||
|
||||
@@ -166,6 +166,37 @@ pub fn set_translation(
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Reads one translation entry from a workbench.
|
||||
pub fn get_translation_entry(
|
||||
workbench_path: &Path,
|
||||
entry_id: &str,
|
||||
) -> anyhow::Result<TranslationWorkbenchEntry> {
|
||||
let workbench = read_translation_workbench(workbench_path)?;
|
||||
workbench
|
||||
.entries
|
||||
.into_iter()
|
||||
.find(|entry| entry.id == entry_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("翻译工作台中不存在 TextUnit:{entry_id}"))
|
||||
}
|
||||
|
||||
/// Clears one reviewed translation and writes the workbench atomically.
|
||||
pub fn unset_translation(
|
||||
workbench_path: &Path,
|
||||
entry_id: &str,
|
||||
) -> anyhow::Result<TranslationWorkbenchEntry> {
|
||||
let mut workbench = read_translation_workbench(workbench_path)?;
|
||||
let entry = workbench
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|entry| entry.id == entry_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("翻译工作台中不存在 TextUnit:{entry_id}"))?;
|
||||
entry.translated_text = None;
|
||||
let updated = entry.clone();
|
||||
workbench.generated_unix_seconds = unix_seconds_now();
|
||||
write_translation_workbench(workbench_path, &workbench)?;
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Validates a workbench against the current official TextUnit index.
|
||||
///
|
||||
/// This checks the release identity and every stored source/target location
|
||||
@@ -656,6 +687,23 @@ mod tests {
|
||||
assert!(error.to_string().contains("不存在 TextUnit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_get_and_unset_round_trip_atomically() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let path = temp.path().join("workbench.json");
|
||||
let mut workbench = workbench(temp.path());
|
||||
workbench.entries[0].translated_text = Some("译文".to_string());
|
||||
write_translation_workbench(&path, &workbench).unwrap();
|
||||
|
||||
let entry = get_translation_entry(&path, "unit-1").unwrap();
|
||||
assert_eq!(entry.translated_text.as_deref(), Some("译文"));
|
||||
|
||||
let updated = unset_translation(&path, "unit-1").unwrap();
|
||||
assert_eq!(updated.translated_text, None);
|
||||
let loaded = read_translation_workbench(&path).unwrap();
|
||||
assert_eq!(loaded.entries[0].translated_text, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_reports_publishable_and_unreviewed_entries() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user