mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:14:55 +08:00
@@ -23,6 +23,12 @@ pub const LOCALIZED_VERSION_STATE_FILE: &str = "localized-version-state.json";
|
||||
pub const LOCALIZED_PATCH_MANIFEST_FILE: &str = "localized-patch-manifest.json";
|
||||
/// Current localized patch manifest schema version.
|
||||
pub const LOCALIZED_PATCH_MANIFEST_VERSION: u32 = 1;
|
||||
/// Current localized version state schema version.
|
||||
pub const LOCALIZED_VERSION_STATE_VERSION: u32 = 1;
|
||||
/// Stable marker for localized releases that are under human proofreading.
|
||||
pub const LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING: &str = "manual_proofreading";
|
||||
/// Human label for `LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING`.
|
||||
pub const LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL: &str = "人工校对中";
|
||||
|
||||
/// One patch operation against a bundle in an official release.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -94,6 +100,7 @@ impl LocalizedPatchConfig {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalizedVersionState {
|
||||
/// State schema version.
|
||||
#[serde(default = "default_localized_version_state_version")]
|
||||
pub state_version: u32,
|
||||
/// Official release ID used as the patch source.
|
||||
pub official_release_id: String,
|
||||
@@ -101,10 +108,59 @@ pub struct LocalizedVersionState {
|
||||
pub current_release_id: Option<String>,
|
||||
/// Stable status label.
|
||||
pub status: String,
|
||||
/// Review/proofreading workflow marker independent from publish status.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub translation_workflow_status: Option<String>,
|
||||
/// Last update time.
|
||||
pub updated_unix_seconds: u64,
|
||||
}
|
||||
|
||||
impl LocalizedVersionState {
|
||||
/// Returns the stable translation workflow status, if set.
|
||||
pub fn translation_workflow_status(&self) -> Option<&str> {
|
||||
self.translation_workflow_status.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the user-facing translation workflow label, if set.
|
||||
pub fn translation_workflow_label(&self) -> Option<&'static str> {
|
||||
match self.translation_workflow_status() {
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING) => {
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report produced when changing localized translation workflow state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct LocalizedTranslationWorkflowReport {
|
||||
/// Stable command name.
|
||||
pub command: &'static str,
|
||||
/// Stable operation status.
|
||||
pub status: &'static str,
|
||||
/// Localized output root.
|
||||
pub localized_output_root: PathBuf,
|
||||
/// State file written under the localized output root.
|
||||
pub state_path: PathBuf,
|
||||
/// Official release associated with the workflow state.
|
||||
pub official_release_id: String,
|
||||
/// Current published localized release ID, when one exists.
|
||||
pub current_release_id: Option<String>,
|
||||
/// Stable localized publication label, such as `localized`.
|
||||
pub localized_release_status: String,
|
||||
/// Stable translation workflow marker.
|
||||
pub translation_workflow_status: String,
|
||||
/// Stable lifecycle status code for read-side callers.
|
||||
pub translation_workflow_status_code: &'static str,
|
||||
/// Human label for the workflow status.
|
||||
pub translation_workflow_label: &'static str,
|
||||
/// Whether an existing localized release remains publishable after the change.
|
||||
pub publish_allowed: bool,
|
||||
/// Last update time written to the state file.
|
||||
pub updated_unix_seconds: u64,
|
||||
}
|
||||
|
||||
/// One changed file in a localized patch manifest.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalizedPatchFile {
|
||||
@@ -295,6 +351,10 @@ impl LocalizedPatchService {
|
||||
.localized_output_root
|
||||
.join(LOCALIZED_VERSION_STATE_FILE);
|
||||
let patch_manifest_path = version_path.join(LOCALIZED_PATCH_MANIFEST_FILE);
|
||||
let previous_state = read_localized_version_state(&config.localized_output_root)?;
|
||||
let translation_workflow_status = previous_state
|
||||
.filter(|state| state.official_release_id == config.release_id)
|
||||
.and_then(|state| state.translation_workflow_status);
|
||||
|
||||
if version_path.exists() {
|
||||
let message = if config.force {
|
||||
@@ -373,10 +433,11 @@ impl LocalizedPatchService {
|
||||
)?;
|
||||
|
||||
let state = LocalizedVersionState {
|
||||
state_version: 1,
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: config.release_id.clone(),
|
||||
current_release_id: Some(config.published_release_id().to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status,
|
||||
updated_unix_seconds: unix_seconds_now(),
|
||||
};
|
||||
write_file_atomic(
|
||||
@@ -415,7 +476,7 @@ pub fn read_localized_version_state(
|
||||
return Ok(None);
|
||||
};
|
||||
let state: LocalizedVersionState = serde_json::from_slice(&bytes)?;
|
||||
if state.state_version != 1 {
|
||||
if state.state_version != LOCALIZED_VERSION_STATE_VERSION {
|
||||
return Err(anyhow::anyhow!(
|
||||
"不支持的汉化版本状态 schema:{},当前版本=1",
|
||||
state.state_version
|
||||
@@ -424,6 +485,70 @@ pub fn read_localized_version_state(
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
/// Writes the localized version state atomically.
|
||||
pub fn write_localized_version_state(
|
||||
localized_output_root: &Path,
|
||||
state: &LocalizedVersionState,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
ensure_safe_directory_path(localized_output_root, "汉化输出目录")
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let path = localized_output_root.join(LOCALIZED_VERSION_STATE_FILE);
|
||||
write_file_atomic(
|
||||
&path,
|
||||
&serde_json::to_vec_pretty(state)?,
|
||||
STATE_FILE_MODE,
|
||||
"汉化版本状态",
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Marks the current localized workflow as under manual proofreading without
|
||||
/// changing the published localized release pointer.
|
||||
pub fn mark_localized_manual_proofreading(
|
||||
localized_output_root: &Path,
|
||||
official_release_id: &str,
|
||||
) -> anyhow::Result<LocalizedTranslationWorkflowReport> {
|
||||
let mut state = read_localized_version_state(localized_output_root)?.unwrap_or_else(|| {
|
||||
LocalizedVersionState {
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: official_release_id.to_string(),
|
||||
current_release_id: None,
|
||||
status: "not_localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 0,
|
||||
}
|
||||
});
|
||||
if state.official_release_id != official_release_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"汉化状态 release={} 与当前官方 release={} 不一致;请先重新发布或指定匹配的官方 release",
|
||||
state.official_release_id,
|
||||
official_release_id
|
||||
));
|
||||
}
|
||||
state.state_version = LOCALIZED_VERSION_STATE_VERSION;
|
||||
state.translation_workflow_status =
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING.to_string());
|
||||
state.updated_unix_seconds = unix_seconds_now();
|
||||
let state_path = write_localized_version_state(localized_output_root, &state)?;
|
||||
|
||||
Ok(LocalizedTranslationWorkflowReport {
|
||||
command: "translation-proofread",
|
||||
status: "updated",
|
||||
localized_output_root: localized_output_root.to_path_buf(),
|
||||
state_path,
|
||||
official_release_id: state.official_release_id.clone(),
|
||||
current_release_id: state.current_release_id.clone(),
|
||||
localized_release_status: state.status.clone(),
|
||||
translation_workflow_status: LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING.to_string(),
|
||||
translation_workflow_status_code:
|
||||
crate::ReleaseFlowStatusCode::TranslationManualProofreading.as_str(),
|
||||
translation_workflow_label: LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL,
|
||||
publish_allowed: state.status == "localized" && state.current_release_id.is_some(),
|
||||
updated_unix_seconds: state.updated_unix_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads a localized patch manifest from a published version directory.
|
||||
pub fn read_localized_patch_manifest_at(
|
||||
version_path: &Path,
|
||||
@@ -754,6 +879,10 @@ fn default_patch_manifest_version() -> u32 {
|
||||
LOCALIZED_PATCH_MANIFEST_VERSION
|
||||
}
|
||||
|
||||
fn default_localized_version_state_version() -> u32 {
|
||||
LOCALIZED_VERSION_STATE_VERSION
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -846,6 +975,7 @@ mod tests {
|
||||
serde_json::from_slice(&fs::read(report.state_path).unwrap()).unwrap();
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.current_release_id.as_deref(), Some("release-1"));
|
||||
assert_eq!(state.translation_workflow_status(), None);
|
||||
assert!(report.patch_manifest_path.is_file());
|
||||
assert_eq!(report.manifest.file_count, 0);
|
||||
assert_eq!(report.integrity.verified_changed_file_count, 0);
|
||||
@@ -857,6 +987,62 @@ mod tests {
|
||||
assert_eq!(manifest.rollback.previous_current_target, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn localized_state_reads_legacy_file_without_workflow_status() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let localized = temp.path().join("localized");
|
||||
fs::create_dir_all(&localized).unwrap();
|
||||
fs::write(
|
||||
localized.join(LOCALIZED_VERSION_STATE_FILE),
|
||||
br#"{
|
||||
"state_version": 1,
|
||||
"official_release_id": "release-1",
|
||||
"current_release_id": "release-1",
|
||||
"status": "localized",
|
||||
"updated_unix_seconds": 123
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let state = read_localized_version_state(&localized).unwrap().unwrap();
|
||||
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.translation_workflow_status(), None);
|
||||
assert_eq!(state.translation_workflow_label(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_proofreading_marker_preserves_published_state() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let localized = temp.path().join("localized");
|
||||
fs::create_dir_all(&localized).unwrap();
|
||||
write_localized_version_state(
|
||||
&localized,
|
||||
&LocalizedVersionState {
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: "release-1".to_string(),
|
||||
current_release_id: Some("release-1-auto".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 123,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report = mark_localized_manual_proofreading(&localized, "release-1").unwrap();
|
||||
let state = read_localized_version_state(&localized).unwrap().unwrap();
|
||||
|
||||
assert_eq!(report.translation_workflow_label, "人工校对中");
|
||||
assert_eq!(
|
||||
state.translation_workflow_status(),
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING)
|
||||
);
|
||||
assert_eq!(state.translation_workflow_label(), Some("人工校对中"));
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.current_release_id.as_deref(), Some("release-1-auto"));
|
||||
assert!(report.publish_allowed);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn failed_patch_publish_cleans_staging_and_unpublished_version() {
|
||||
|
||||
Reference in New Issue
Block a user