use super::*; pub(super) fn run_parse_once(options: &CliOptions) -> anyhow::Result<()> { let (resource_root, release_id) = current_official_release(options)?; let parse_config = OfficialParseConfig::new(&resource_root, options.config.unzip_command.clone()) .with_force(options.config.force); let parse_report = OfficialParseCacheService::new() .run(&parse_config) .map_err(anyhow::Error::msg)?; let queue_report = write_official_textunit_queues(&resource_root).map_err(anyhow::Error::msg)?; let data = serde_json::json!({ "official_release_id": release_id, "resource_root": resource_root, "forced": options.config.force, "parse": parse_report, "translation_queue": queue_report, }); print_report( options.output_format, &CommandReport { command: "parse", status: "completed", message: "官方资源解析已执行", data, }, ) } pub(super) fn run_parse_clear_cache(options: &CliOptions) -> anyhow::Result<()> { let (resource_root, release_id) = current_official_release(options)?; let artifact_names = [ OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE, CROWDIN_TEXTUNIT_QUEUE_FILE, ]; let mut removed = Vec::new(); for name in artifact_names { let path = resource_root.join(name); if remove_regenerable_file(&path)? { removed.push(path); } } let data = serde_json::json!({ "official_release_id": release_id, "resource_root": resource_root, "removed": removed, "translation_task_repository_preserved": true, }); print_report( options.output_format, &CommandReport { command: "parse-clear-cache", status: "cleared", message: "当前官方 release 的可再生解析缓存和翻译队列已清理", data, }, ) } pub(super) fn run_translate_once(options: &CliOptions) -> anyhow::Result<()> { let (resource_root, release_id) = current_official_release(options)?; let queue = write_official_textunit_queues(&resource_root).map_err(anyhow::Error::msg)?; let exported = options .translation_file .as_ref() .map(|path| { export_translation_workbench(&resource_root, release_id.clone(), path).map( |workbench| { serde_json::json!({ "path": path, "entry_count": workbench.entries.len(), }) }, ) }) .transpose()?; let data = serde_json::json!({ "official_release_id": release_id, "resource_root": resource_root, "queue": queue, "workbench": exported, "provider": "offline", "note": "当前 translate 只生成/刷新离线队列和可编辑工作台,不调用外部翻译 provider", }); print_report( options.output_format, &CommandReport { command: "translate", status: "queued", message: "翻译离线队列已刷新", data, }, ) } pub(super) fn run_translation_validate(options: &CliOptions) -> anyhow::Result<()> { let path = options .translation_file .as_ref() .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 data = serde_json::json!({ "official_release_id": release_id, "resource_root": resource_root, "translation_file": path, "validation": validation, }); print_json_value(options.output_format, &data) } pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> { let path = options .translation_file .as_ref() .ok_or_else(|| anyhow::anyhow!("translation-set 必须指定 --translation-file"))?; let text = match (&options.translation_text, &options.translation_text_file) { (Some(_), Some(_)) => { return Err(anyhow::anyhow!( "--translated-text 与 --translated-file 只能指定一个" )) } (Some(text), None) => text.clone(), (None, Some(path)) => String::from_utf8( read_file_no_symlink(path, "翻译文本文件") .map_err(anyhow::Error::msg)? .ok_or_else(|| anyhow::anyhow!("翻译文本文件不存在:{}", path.display()))?, )?, (None, None) => { return Err(anyhow::anyhow!( "translation-set 必须指定 --translated-text 或 --translated-file" )) } }; let entry_id = options .translation_id .as_deref() .ok_or_else(|| anyhow::anyhow!("translation-set 必须指定 --translation-id"))?; let entry = set_translation(path, entry_id, text)?; let data = serde_json::json!({ "translation_file": path, "entry": entry, }); print_report( options.output_format, &CommandReport { command: "translation-set", status: "updated", message: "翻译工作台条目已更新", data, }, ) } pub(super) fn run_translation_task_update(options: &CliOptions) -> anyhow::Result<()> { let task_id = options .query_task_id .as_deref() .ok_or_else(|| anyhow::anyhow!("i18n task update 必须指定 --task-id"))?; let status = options .query_task_status .as_deref() .ok_or_else(|| anyhow::anyhow!("i18n task update 必须指定 --task-status"))?; let mut params = serde_json::Map::from_iter([ ("task_id".to_string(), serde_json::json!(task_id)), ("status".to_string(), serde_json::json!(status)), ]); if let Some(reason) = options.translation_failure_reason.as_deref() { params.insert("failure_reason".to_string(), serde_json::json!(reason)); } if let Some(provider_run_id) = options.translation_provider_run_id.as_deref() { params.insert( "provider_run_id".to_string(), serde_json::json!(provider_run_id), ); } let report = update_translation_task_status_report( &options.state_dir, Some(&serde_json::Value::Object(params)), )?; print_json_value(options.output_format, &report) } pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> { let spec = options .repack_spec .as_ref() .ok_or_else(|| anyhow::anyhow!("repack 必须指定 --repack-spec"))?; let report = repack_bundle(spec)?; print_report(options.output_format, &report) } pub(super) fn run_publish_localized(options: &CliOptions) -> anyhow::Result<()> { let translation_file = options .translation_file .as_ref() .ok_or_else(|| anyhow::anyhow!("publish-localized 必须指定 --translation-file"))?; let (resource_root, official_release_id) = current_official_release(options)?; let workbench = read_translation_workbench(translation_file)?; if workbench.official_release_id != official_release_id { return Err(anyhow::anyhow!( "翻译工作台 release={} 与当前官方 release={} 不一致;请重新导出", workbench.official_release_id, official_release_id )); } let expected_root = lexical_absolute(&resource_root).map_err(anyhow::Error::msg)?; if workbench.official_resource_root != expected_root { return Err(anyhow::anyhow!( "翻译工作台资源根目录与当前 release 不一致;请重新导出" )); } let patches = localized_text_asset_patches(&resource_root, &workbench)?; let localized_release_id = options.localized_release_id.clone().or_else(|| { options .config .force .then(|| format!("{}-manual-{}", official_release_id, unix_seconds_now())) }); let mut config = LocalizedPatchConfig::new( resource_root, options.config.localized_output_root.clone(), official_release_id, patches, ) .with_force(options.config.force); if let Some(release_id) = localized_release_id { config = config.with_localized_release_id(release_id); } let report = LocalizedPatchService::new().publish(&config)?; print_report(options.output_format, &report) } fn current_official_release(options: &CliOptions) -> anyhow::Result<(PathBuf, String)> { let state = read_version_state(&options.config.version_state_path())?; let resource_root = if let Some(resource_root) = options.resource_root.clone() { lexical_absolute(&resource_root).map_err(anyhow::Error::msg)? } else { state .as_ref() .and_then(|state| state.current_completed_version.as_ref()) .map(|record| record.resource_root.clone()) .unwrap_or(active_official_resource_root(&options.config.output_root)?) }; let release_id = state .as_ref() .and_then(|state| state.current_completed_version.as_ref()) .filter(|_| options.resource_root.is_none()) .map(|record| record.id.clone()) .or_else(|| { resource_root .file_name() .and_then(|name| name.to_str()) .map(str::to_string) }) .ok_or_else(|| anyhow::anyhow!("无法从当前官方资源根目录确定 release id"))?; if read_download_manifest_at(&resource_root) .map_err(anyhow::Error::msg)? .is_none() { return Err(anyhow::anyhow!( "当前官方 release 缺少官方下载 manifest:{}", resource_root.display() )); } Ok((resource_root, release_id)) } fn remove_regenerable_file(path: &Path) -> anyhow::Result { let metadata = match fs::symlink_metadata(path) { Ok(metadata) => metadata, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(error) => return Err(error.into()), }; if metadata.file_type().is_symlink() { return Err(anyhow::anyhow!( "拒绝删除符号链接形式的可再生文件:{}", path.display() )); } if !metadata.is_file() { return Err(anyhow::anyhow!( "可再生缓存路径不是普通文件:{}", path.display() )); } fs::remove_file(path)?; Ok(true) }