From 22a004c7e5b3db85010cf12f209c984700938eff Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 12:06:41 +0800 Subject: [PATCH 1/6] feat(corpus): add evidence-backed zh-CN mapping pipeline Use the user-provided workshop-data export to generate catalog aliases, settings locale data, provenance, and bidirectional conversion evidence. Keep exact-match exclusions fail-explicit and record the remaining release-gate gaps. Refs #2 --- README.md | 16 +- crates/workshop-rs-cli/tests/cli.rs | 27 +- .../src/bin/workshop-catalog-gen.rs | 866 ++++- .../workshop-rs/src/catalog/data/catalog.json | 985 +++-- crates/workshop-rs/src/emitter.rs | 155 +- .../workshop-rs/src/settings/data/zh-cn.json | 373 ++ crates/workshop-rs/src/settings/table.rs | 29 + crates/workshop-rs/tests/catalog.rs | 6 +- crates/workshop-rs/tests/corpus.rs | 77 + crates/workshop-rs/tests/identity.rs | 4 +- crates/workshop-rs/tests/locale.rs | 130 +- docs/provenance.md | 26 +- tools/corpus/zh-cn-corpus.json | 3173 +++++++++++++++++ 13 files changed, 5358 insertions(+), 509 deletions(-) create mode 100644 crates/workshop-rs/src/settings/data/zh-cn.json create mode 100644 crates/workshop-rs/tests/corpus.rs create mode 100644 tools/corpus/zh-cn-corpus.json diff --git a/README.md b/README.md index 03566c2..01658bf 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,19 @@ the canonical form with a fresh digest (byte-idempotent). See * `en-US`: complete declared surface (344/344 canonical entries), corpus round-trips and settings emission tested. -* `zh-CN`: declared as a locale with **zero mappings** pending a reviewed, - MIT-permissible reference source. Conversion into `zh-CN` fails explicitly - for every entry; no zh-CN compatibility claim is made. +* `zh-CN`: the reviewed export-backed corpus covers **327/344** canonical + entries (structural 11/11, actions 55/62, values 77/78, events 3/3, + operators 8/14, enum members 173/176). The 17 exact-match exclusions remain + fail-explicit; settings data covers the matched declared surface and records + its exclusions in `crates/workshop-rs/src/settings/data/zh-cn.json`. + +The corpus is reproducible with the user-provided export (not committed): + +```sh +cargo run -p workshop-rs --bin workshop-catalog-gen -- corpus \ + --export /path/to/workshop-data.json +cargo run -p workshop-rs --bin workshop-catalog-gen -- build +``` ## Validation diff --git a/crates/workshop-rs-cli/tests/cli.rs b/crates/workshop-rs-cli/tests/cli.rs index 4352a63..91cf2fe 100644 --- a/crates/workshop-rs-cli/tests/cli.rs +++ b/crates/workshop-rs-cli/tests/cli.rs @@ -51,7 +51,7 @@ fn locales_lists_declared_locales_with_coverage() { let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines.len(), 2); assert!(lines[0].starts_with("en-us 344/344"), "{stdout}"); - assert!(lines[1].starts_with("zh-cn 0/344"), "{stdout}"); + assert!(lines[1].starts_with("zh-cn 327/344"), "{stdout}"); } #[test] @@ -110,7 +110,7 @@ fn emit_emits_localized_text() { } #[test] -fn convert_to_zh_cn_fails_explicitly_without_fallback() { +fn convert_to_zh_cn_uses_the_corpus_without_fallback() { let file = fixture("basic-rule"); let output = run(&[ "convert", @@ -120,15 +120,22 @@ fn convert_to_zh_cn_fails_explicitly_without_fallback() { "--to", "zh-CN", ]); - assert_eq!(output.status.code(), Some(1)); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("missing"), "{stderr}"); - assert!(stderr.contains("zh-cn"), "{stderr}"); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("持续 - 全局"), "{stdout}"); + assert!(stdout.contains("禁用查看器录制"), "{stdout}"); } #[test] fn convert_to_zh_cn_with_fallback_reports_the_choice() { - let file = fixture("basic-rule"); + let dir = std::env::temp_dir().join("workshop-rs-cli-convert-fallback"); + std::fs::create_dir_all(&dir).unwrap(); + let file = dir.join("unmapped.ws"); + std::fs::write( + &file, + "rule (\"setup\") { event { Ongoing - Global; } actions { Force Player Hero(Event Player, Ana); } }", + ) + .unwrap(); let output = run(&[ "convert", file.to_str().unwrap(), @@ -145,12 +152,14 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("Disable Inspector Recording;"), "{stdout}"); + assert!(stdout.contains("持续 - 全局"), "{stdout}"); + assert!(stdout.contains("Force Player Hero"), "{stdout}"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("fallback-locale spelling") && stderr.contains("global"), + stderr.contains("fallback-locale spelling") && stderr.contains("forcePlayerHero"), "the fallback choice is visible in tooling output: {stderr}" ); + let _ = std::fs::remove_dir_all(&dir); } #[test] diff --git a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs index 928e645..c484f31 100644 --- a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs +++ b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs @@ -8,6 +8,7 @@ //! ```sh //! workshop-catalog-gen check [--file catalog.json] [--json] //! workshop-catalog-gen build [--file catalog.json] +//! workshop-catalog-gen corpus [--file catalog.json] [--export PATH] [--out-dir tools/corpus] [--settings-out PATH] //! ``` //! //! * `check` validates the catalog (schema, duplicate ids, colliding or @@ -16,21 +17,47 @@ //! identity (with `--json` as a JSON document). //! * `build` validates, canonicalizes, and (re)writes the file with a fresh //! content digest. Re-running is byte-idempotent. +//! * `corpus` applies the zh-CN corpus evidence to the catalog data +//! (ADR-0001 Decision 6): it reads the user-provided Workshop data export +//! (`--export`, or the `WORKSHOP_DATA_EXPORT` environment variable), +//! matches every catalog entry and enum member by its exact en-US spelling +//! against the export's localized index, and writes +//! - the merged catalog data file (zh-CN aliases added; data change only, +//! the declared digest is left stale for `build` to recompute), +//! - the machine-readable corpus manifest with every match, every exclusion +//! and its reason, and per-category match statistics, and +//! - the settings locale corpus for the declared settings surface. +//! Unmatched entries are excluded from the corpus with a recorded reason +//! and keep fail-explicit behavior (ADR-0001 Decision 7); no spelling is +//! fabricated. Re-running on the merged data is byte-idempotent. //! //! Updating localization data is a bounded data change: edit the JSON and -//! re-run `check`/`build`; no parser or emitter code changes. +//! re-run the pipeline; no parser or emitter code changes. The full zh-CN +//! corpus flow is: `corpus` (data merge) -> `build` (fresh digest) -> +//! `check` (verify); commit data and regenerated files together. -use std::path::PathBuf; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::process::ExitCode; use workshop_rs::catalog::{Catalog, build_canonical}; +use workshop_rs::settings::table; /// The committed catalog data, relative to the workspace root (where CI and /// the documented pipeline commands run); `--file` overrides it. const DEFAULT_FILE: &str = "crates/workshop-rs/src/catalog/data/catalog.json"; +/// The default corpus manifest output directory. +const DEFAULT_OUT_DIR: &str = "tools/corpus"; + +/// The default settings locale corpus output file. +const DEFAULT_SETTINGS_OUT: &str = "crates/workshop-rs/src/settings/data/zh-cn.json"; + +/// The environment variable naming the Workshop data export path. +const EXPORT_ENV: &str = "WORKSHOP_DATA_EXPORT"; + fn usage() -> &'static str { - "usage: workshop-catalog-gen [--file catalog.json] [--json]" + "usage: workshop-catalog-gen [--file catalog.json] [--json] [--export PATH] [--out-dir tools/corpus] [--settings-out PATH]" } fn main() -> ExitCode { @@ -38,6 +65,9 @@ fn main() -> ExitCode { let command = args.next(); let mut file = PathBuf::from(DEFAULT_FILE); let mut json = false; + let mut export: Option = None; + let mut out_dir = PathBuf::from(DEFAULT_OUT_DIR); + let mut settings_out = PathBuf::from(DEFAULT_SETTINGS_OUT); while let Some(arg) = args.next() { match arg.as_str() { "--file" => match args.next() { @@ -47,6 +77,27 @@ fn main() -> ExitCode { return ExitCode::from(2); } }, + "--export" => match args.next() { + Some(path) => export = Some(PathBuf::from(path)), + None => { + eprintln!("workshop-catalog-gen: missing value for --export"); + return ExitCode::from(2); + } + }, + "--out-dir" => match args.next() { + Some(path) => out_dir = PathBuf::from(path), + None => { + eprintln!("workshop-catalog-gen: missing value for --out-dir"); + return ExitCode::from(2); + } + }, + "--settings-out" => match args.next() { + Some(path) => settings_out = PathBuf::from(path), + None => { + eprintln!("workshop-catalog-gen: missing value for --settings-out"); + return ExitCode::from(2); + } + }, "--json" => json = true, other => { eprintln!("workshop-catalog-gen: unknown argument '{other}'"); @@ -124,9 +175,818 @@ fn main() -> ExitCode { ExitCode::from(1) } }, + Some("corpus") => { + let export_path = + match export.or_else(|| std::env::var_os(EXPORT_ENV).map(PathBuf::from)) { + Some(path) => path, + None => { + eprintln!( + "workshop-catalog-gen: corpus requires the export path via --export or \ + the {EXPORT_ENV} environment variable" + ); + return ExitCode::from(2); + } + }; + match corpus::generate(&content, &file, &export_path, &out_dir, &settings_out) { + Ok(report) => { + for line in report { + println!("{line}"); + } + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("workshop-catalog-gen: {error}"); + ExitCode::from(1) + } + } + } _ => { eprintln!("{}", usage()); ExitCode::from(2) } } } + +/// The zh-CN corpus pipeline (ADR-0001 Decision 6). +mod corpus { + use super::*; + use serde_json::{Map, Value}; + + /// A match candidate inside the export: the export key (for provenance) + /// and the zh-CN spelling. + #[derive(Debug, Clone)] + struct Candidate { + key: String, + zh_cn: String, + } + + /// An exact-en-US-spelling index over a slice of the export. + #[derive(Debug, Default)] + struct Index { + by_en: HashMap>, + } + + impl Index { + fn add(&mut self, key: &str, en: &str, zh: &str) { + if !en.is_empty() && !zh.is_empty() { + self.by_en + .entry(en.to_string()) + .or_default() + .push(Candidate { + key: key.to_string(), + zh_cn: zh.to_string(), + }); + } + } + + /// Match an exact en-US spelling. Returns `(sources, zh-CN)` when + /// every candidate agrees on zh-CN; a `String` reason otherwise. + fn match_spelling(&self, en: &str) -> Result, String> { + let Some(candidates) = self.by_en.get(en) else { + return Err("no exact en-US match in the export".to_string()); + }; + let zh = candidates[0].zh_cn.clone(); + for candidate in candidates { + if candidate.zh_cn != zh { + let keys: Vec<&str> = candidates.iter().map(|c| c.key.as_str()).collect(); + return Err(format!( + "ambiguous: export candidates disagree on zh-CN ({})", + keys.join(", ") + )); + } + } + Ok(candidates.clone()) + } + } + + /// Build an index from `localized` entries with any of the given key + /// prefixes. + fn localized_index(export: &Value, prefixes: &[&str]) -> Index { + let mut index = Index::default(); + let Some(localized) = export.get("localized").and_then(Value::as_object) else { + return index; + }; + for (key, entry) in localized { + if !prefixes.iter().any(|prefix| key.starts_with(prefix)) { + continue; + } + let Some(translations) = entry.get("translations") else { + continue; + }; + let en = translations.get("en-US").and_then(Value::as_str); + let zh = translations.get("zh-CN").and_then(Value::as_str); + if let (Some(en), Some(zh)) = (en, zh) { + index.add(key, en, zh); + } + } + index + } + + /// Build an index from a `data.*` section with direct en-US/zh-CN fields + /// (maps, heroes), keyed `data.` for provenance. + fn data_index(export: &Value, section: &str) -> Index { + let mut index = Index::default(); + let Some(entries) = export + .get("data") + .and_then(|data| data.get(section)) + .and_then(Value::as_object) + else { + return index; + }; + for (id, entry) in entries { + let en = entry.get("en-US").and_then(Value::as_str); + let zh = entry.get("zh-CN").and_then(Value::as_str); + if let (Some(en), Some(zh)) = (en, zh) { + index.add(&format!("data.{section}.{id}"), en, zh); + } + } + index + } + + /// One matched corpus entry. + #[derive(Debug, Clone)] + struct Match { + kind: String, + id: String, + en: String, + zh: String, + sources: Vec, + } + + /// One excluded catalog identity with its reason. + #[derive(Debug, Clone)] + struct Exclusion { + kind: String, + id: String, + en: String, + reason: String, + } + + /// The declared settings surface (mirrors `settings::table`). + #[derive(Debug)] + struct SettingsSurface { + /// `(surface id, en-US name)` of every rendering label. The + /// per-mode `enabled` members render no label (the mode header's + /// `disabled` prefix instead) and are excluded here. + labels: Vec<(String, String)>, + modes: Vec<(String, String)>, + maps: Vec<(String, String)>, + heroes: Vec<(String, String)>, + teams: Vec<(String, String)>, + enums: Vec<(String, String)>, + tokens: Vec<(String, String)>, + } + + type SettingsSection<'a> = (&'a str, Vec<(String, String)>, &'a Index); + + struct Report<'a> { + coverage: &'a [(String, usize, usize)], + total_matched: usize, + total_entries: usize, + excluded: &'a [Exclusion], + settings: &'a Value, + catalog_file: &'a Path, + manifest_path: &'a Path, + settings_out: &'a Path, + } + + fn settings_surface() -> SettingsSurface { + // The declared label surface is the set of distinct rendered names; + // per-mode repeats (enabled maps, Limit Roles, Competitive Rules) + // share one label. The per-mode `enabled` members render no label + // (the mode header's `disabled` prefix instead) and are excluded. + let mut labels: Vec<(String, String)> = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for entry in table::ENTRIES { + if matches!(entry.path.last(), Some(table::PathPart::Part("enabled"))) { + continue; + } + if seen.insert(entry.workshop_name) { + labels.push(( + table::path_string(entry.path), + entry.workshop_name.to_string(), + )); + } + } + let modes = table::MODE_NAMES + .iter() + .map(|m| (format!("mode.{}.name", m.key), m.name.to_string())) + .collect(); + let maps = table::MAP_NAMES + .iter() + .map(|m| (format!("map.{}.name", m.key), m.name.to_string())) + .collect(); + let heroes = table::HERO_NAMES + .iter() + .map(|m| (format!("hero.{}.name", m.key), m.name.to_string())) + .collect(); + let teams = table::TEAM_NAMES + .iter() + .map(|m| (format!("team.{}.name", m.key), m.name.to_string())) + .collect(); + let enums = table::ENUM_MEMBERS + .iter() + .map(|m| { + ( + format!("enum.{}.{}", m.domain, m.member), + m.name.to_string(), + ) + }) + .collect(); + let tokens = [ + ("token.on", "On"), + ("token.off", "Off"), + ("token.disabled", "disabled"), + ] + .iter() + .map(|(id, name)| (id.to_string(), name.to_string())) + .collect(); + SettingsSurface { + labels, + modes, + maps, + heroes, + teams, + enums, + tokens, + } + } + + /// The corpus pipeline report lines. + pub(crate) fn generate( + catalog_data: &str, + catalog_file: &Path, + export_path: &Path, + out_dir: &Path, + settings_out: &Path, + ) -> Result, String> { + // The base catalog must be valid before merging corpus data. + Catalog::load_unverified(catalog_data).map_err(|error| format!("catalog data: {error}"))?; + let export_text = std::fs::read_to_string(export_path) + .map_err(|error| format!("cannot read export {}: {error}", export_path.display()))?; + let export: Value = serde_json::from_str(&export_text) + .map_err(|error| format!("cannot parse export {}: {error}", export_path.display()))?; + let meta = export.get("meta").cloned().unwrap_or(Value::Null); + let catalog: Value = + serde_json::from_str(catalog_data).map_err(|error| format!("catalog data: {error}"))?; + + // --- builtin corpus ------------------------------------------------- + let actions = localized_index(&export, &["actions."]); + let values = localized_index(&export, &["values."]); + let events = localized_index(&export, &["other.events."]); + let operators = localized_index(&export, &["values.", "constants.__Operation__."]); + let maps = { + let mut index = localized_index(&export, &["maps."]); + merge_index(&mut index, data_index(&export, "maps")); + index + }; + let heroes = { + let mut index = localized_index(&export, &["heroes."]); + merge_index(&mut index, data_index(&export, "heroes")); + index + }; + let vector = localized_index(&export, &["values.Vector."]); + // Enum domains match the export's constants domain for their exact + // en-US spellings; the export renames a few domains. + let mut constants_by_domain: HashMap = HashMap::new(); + for domain in enum_domains(&catalog)? { + let export_domain = match domain.as_str() { + "Color" => "ColorLiteral", + "Team" => "TeamLiteral", + "Button" => "ButtonLiteral", + "Clipping" => "Clip", + "InworldTextReeval" => "WorldTextReeval", + "Operation" => "__Operation__", + "Rounding" => "__Rounding__", + other => other, + }; + let prefix = format!("constants.{export_domain}."); + constants_by_domain.insert(domain.to_string(), localized_index(&export, &[&prefix])); + } + + let mut matches: Vec = Vec::new(); + let mut excluded: Vec = Vec::new(); + let mut coverage: Vec<(String, usize, usize)> = Vec::new(); + + for (kind, category, entries) in [ + ("structural", "structural", catalog.get("structural")), + ("action", "actions", catalog.get("actions")), + ("value", "values", catalog.get("values")), + ("event", "events", catalog.get("events")), + ("operator", "operators", catalog.get("operators")), + ] { + let index = match category { + "structural" | "actions" => &actions, + "values" => &values, + "events" => &events, + "operators" => &operators, + _ => unreachable!(), + }; + let mut matched = 0; + let mut total = 0; + for entry in entries.and_then(Value::as_array).into_iter().flatten() { + total += 1; + let id = entry + .get("id") + .and_then(Value::as_str) + .ok_or_else(|| "catalog entry without id".to_string())?; + let en = en_alias(entry)?; + match index.match_spelling(en) { + Ok(candidates) => { + matched += 1; + let zh = candidates[0].zh_cn.clone(); + matches.push(Match { + kind: kind.to_string(), + id: id.to_string(), + en: en.to_string(), + zh: zh.clone(), + sources: candidates.iter().map(|c| c.key.clone()).collect(), + }); + } + Err(reason) => excluded.push(Exclusion { + kind: kind.to_string(), + id: id.to_string(), + en: en.to_string(), + reason, + }), + } + } + coverage.push((category.to_string(), matched, total)); + } + + // Enum members: Map/Hero/Vector domains match their own export + // domains; other domains match their (renamed) constants domain. + let mut members_matched = 0; + let mut members_total = 0; + let enum_domains = enum_domains(&catalog)?; + for domain_name in &enum_domains { + let index = match domain_name.as_str() { + "Map" => &maps, + "Hero" => &heroes, + "Vector" => &vector, + _ => constants_by_domain.get(domain_name).ok_or_else(|| { + format!("missing constants index for enum domain '{domain_name}'") + })?, + }; + let Some(domain) = catalog + .get("enums") + .and_then(Value::as_array) + .and_then(|domains| { + domains.iter().find(|d| { + d.get("domain").and_then(Value::as_str) == Some(domain_name.as_str()) + }) + }) + else { + continue; + }; + for member in domain + .get("members") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + members_total += 1; + let id = member + .get("id") + .and_then(Value::as_str) + .ok_or_else(|| "enum member without id".to_string())?; + let en = en_alias(member)?; + match index.match_spelling(en) { + Ok(candidates) => { + members_matched += 1; + let zh = candidates[0].zh_cn.clone(); + matches.push(Match { + kind: "enum member".to_string(), + id: format!("{domain_name}.{id}"), + en: en.to_string(), + zh: zh.clone(), + sources: candidates.iter().map(|c| c.key.clone()).collect(), + }); + } + Err(reason) => excluded.push(Exclusion { + kind: "enum member".to_string(), + id: format!("{domain_name}.{id}"), + en: en.to_string(), + reason, + }), + } + } + } + coverage.push(("enums".to_string(), members_matched, members_total)); + + let total_matched: usize = coverage.iter().map(|(_, m, _)| m).sum(); + let total_entries: usize = coverage.iter().map(|(_, _, t)| t).sum(); + + // --- merge zh-CN aliases into the catalog data ----------------------- + let merged = merge_zh_aliases(&catalog, &matches)?; + let merged_text = canonical_json(&merged)?; + std::fs::write(catalog_file, &merged_text) + .map_err(|error| format!("cannot write {}: {error}", catalog_file.display()))?; + + // --- corpus manifest ------------------------------------------------- + let manifest = manifest( + &meta, + &matches, + &excluded, + &coverage, + total_matched, + total_entries, + ); + std::fs::create_dir_all(out_dir) + .map_err(|error| format!("cannot create {}: {error}", out_dir.display()))?; + let manifest_path = out_dir.join("zh-cn-corpus.json"); + std::fs::write(&manifest_path, canonical_json(&manifest)?) + .map_err(|error| format!("cannot write {}: {error}", manifest_path.display()))?; + + // --- settings locale corpus ------------------------------------------ + let settings = settings_corpus(&export)?; + if let Some(parent) = settings_out.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("cannot create {}: {error}", parent.display()))?; + } + std::fs::write(settings_out, canonical_json(&settings)?) + .map_err(|error| format!("cannot write {}: {error}", settings_out.display()))?; + + Ok(format_report(Report { + coverage: &coverage, + total_matched, + total_entries, + excluded: &excluded, + settings: &settings, + catalog_file, + manifest_path: &manifest_path, + settings_out, + })) + } + + /// The export index for one catalog enum domain. + fn enum_domains(catalog: &Value) -> Result, String> { + let mut domains = Vec::new(); + for domain in catalog + .get("enums") + .and_then(Value::as_array) + .ok_or_else(|| "catalog without enums".to_string())? + { + domains.push( + domain + .get("domain") + .and_then(Value::as_str) + .ok_or_else(|| "enum domain without name".to_string())? + .to_string(), + ); + } + Ok(domains) + } + + fn merge_index(target: &mut Index, source: Index) { + for (en, candidates) in source.by_en { + target.by_en.entry(en).or_default().extend(candidates); + } + } + + /// The en-US alias of a catalog entry/member. + fn en_alias(entry: &Value) -> Result<&str, String> { + entry + .get("aliases") + .and_then(|aliases| aliases.get("en-US")) + .and_then(Value::as_str) + .ok_or_else(|| format!("catalog entry '{}' without en-US alias", entry)) + } + + /// Canonical (sorted-key, pretty) JSON serialization, byte-idempotent. + fn canonical_json(value: &Value) -> Result { + let mut out = serde_json::to_string_pretty(value) + .map_err(|error| format!("cannot serialize JSON: {error}"))?; + out.push('\n'); + Ok(out) + } + + /// Merge `zh-CN` aliases into the catalog data. Existing zh-CN aliases + /// must match the corpus; nothing else changes (data-only, ADR-0001). + fn merge_zh_aliases(catalog: &Value, matches: &[Match]) -> Result { + let mut merged = catalog.clone(); + let Some(object) = merged.as_object_mut() else { + return Err("catalog is not an object".to_string()); + }; + let mut by_identity: HashMap<(&str, &str), &Match> = HashMap::new(); + for matched in matches { + by_identity.insert((matched.kind.as_str(), matched.id.as_str()), matched); + } + for category in ["structural", "actions", "values", "events", "operators"] { + let Some(list) = object.get_mut(category).and_then(Value::as_array_mut) else { + continue; + }; + for entry in list { + let Some(id) = entry.get("id").and_then(Value::as_str) else { + continue; + }; + let kind = match category { + "structural" => "structural", + "actions" => "action", + "values" => "value", + "events" => "event", + "operators" => "operator", + _ => unreachable!(), + }; + if let Some(matched) = by_identity.get(&(kind, id)).copied() { + set_zh_alias(entry, &matched.zh)?; + } + } + } + if let Some(enums) = object.get_mut("enums").and_then(Value::as_array_mut) { + for domain in enums { + let domain_name = domain + .get("domain") + .and_then(Value::as_str) + .map(str::to_string); + let Some(members) = domain.get_mut("members").and_then(Value::as_array_mut) else { + continue; + }; + for member in members { + let Some(id) = member.get("id").and_then(Value::as_str) else { + continue; + }; + let Some(domain_name) = &domain_name else { + continue; + }; + let key = format!("{domain_name}.{id}"); + if let Some(matched) = by_identity.get(&("enum member", key.as_str())).copied() + { + set_zh_alias(member, &matched.zh)?; + } + } + } + } + Ok(merged) + } + + /// Set (or verify) the zh-CN alias of one catalog entry. + fn set_zh_alias(entry: &mut Value, zh: &str) -> Result<(), String> { + let Some(aliases) = entry.get_mut("aliases").and_then(Value::as_object_mut) else { + return Err("catalog entry without aliases object".to_string()); + }; + match aliases.get("zh-CN") { + Some(existing) if existing.as_str() == Some(zh) => {} + Some(existing) => { + return Err(format!( + "catalog already declares zh-CN '{existing}' but the corpus yields '{zh}'" + )); + } + None => { + aliases.insert("zh-CN".to_string(), Value::String(zh.to_string())); + } + } + Ok(()) + } + + /// The machine-readable corpus manifest (ADR-0001 Decision 6). + fn manifest( + meta: &Value, + matches: &[Match], + excluded: &[Exclusion], + coverage: &[(String, usize, usize)], + total_matched: usize, + total_entries: usize, + ) -> Value { + let mut matches_json: Vec = matches + .iter() + .map(|m| { + serde_json::json!({ + "kind": m.kind, + "id": m.id, + "en-US": m.en, + "zh-CN": m.zh, + "sources": m.sources, + }) + }) + .collect(); + matches_json.sort_by(|a, b| { + (a["kind"].as_str(), a["id"].as_str()).cmp(&(b["kind"].as_str(), b["id"].as_str())) + }); + let mut excluded_json: Vec = excluded + .iter() + .map(|e| { + serde_json::json!({ + "kind": e.kind, + "id": e.id, + "en-US": e.en, + "reason": e.reason, + }) + }) + .collect(); + excluded_json.sort_by(|a, b| { + (a["kind"].as_str(), a["id"].as_str()).cmp(&(b["kind"].as_str(), b["id"].as_str())) + }); + let mut coverage_json = serde_json::Map::new(); + for (category, matched, total) in coverage { + coverage_json.insert( + category.clone(), + serde_json::json!({ "matched": matched, "total": total }), + ); + } + let mut coverage_all = coverage_json; + coverage_all.insert( + "total".to_string(), + serde_json::json!({ "matched": total_matched, "total": total_entries }), + ); + serde_json::json!({ + "schemaVersion": 1, + "locale": "zh-CN", + "generator": "workshop-catalog-gen corpus", + "generatorVersion": env!("CARGO_PKG_VERSION"), + "source": { + "export": meta.get("commit").and_then(Value::as_str).map(|_| "workshop-data.json").unwrap_or(""), + "commit": meta.get("commit").and_then(Value::as_str).unwrap_or(""), + "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), + "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), + }, + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), zh-CN taken from the same export entry; entries without an exact match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + "coverage": Value::Object(coverage_all), + "matches": matches_json, + "excluded": excluded_json, + }) + } + + /// The settings locale corpus for the declared settings surface. + fn settings_corpus(export: &Value) -> Result { + let custom_game = { + let mut index = localized_index( + export, + &[ + "customGameSettings.", + "heroes.", + "maps.", + "gamemodes.", + "constants.", + ], + ); + merge_index( + &mut index, + localized_index(export, &["other.customGameSettings."]), + ); + index + }; + let gamemodes = localized_index(export, &["gamemodes."]); + let maps = localized_index(export, &["maps."]); + let heroes = localized_index(export, &["heroes."]); + let teams = localized_index(export, &["heroes.teams."]); + let tokens = localized_index(export, &["other.customGameSettings."]); + let surface = settings_surface(); + + let mut sections: Vec> = vec![ + ("labels", surface.labels, &custom_game), + ("modes", surface.modes, &gamemodes), + ("maps", surface.maps, &maps), + ("heroes", surface.heroes, &heroes), + ("teams", surface.teams, &teams), + ("enums", surface.enums, &custom_game), + ("tokens", surface.tokens, &tokens), + ]; + + let mut entries: Map = Map::new(); + let mut excluded: Vec = Vec::new(); + let mut coverage: Map = Map::new(); + for (section, surface_entries, index) in &mut sections { + let mut matched = 0; + let mut total = 0; + for (surface_id, en) in surface_entries { + total += 1; + // The mode-header `disabled` prefix (surface form) maps the + // export's capitalized `Disabled` token (__disabled__). + let export_en = if *section == "tokens" && en == "disabled" { + "Disabled" + } else { + en.as_str() + }; + match index.match_spelling(export_en) { + Ok(candidates) => { + matched += 1; + entries.insert( + en.clone(), + serde_json::json!({ + "en-US": en, + "zh-CN": candidates[0].zh_cn, + "sources": candidates.iter().map(|c| c.key.clone()).collect::>(), + }), + ); + } + Err(reason) => excluded.push(serde_json::json!({ + "surface": surface_id, + "en-US": en, + "reason": reason, + })), + } + } + coverage.insert( + section.to_string(), + serde_json::json!({ "matched": matched, "total": total }), + ); + } + + let meta = export.get("meta").cloned().unwrap_or(Value::Null); + // Split the flat matched entries into per-section maps mirroring the + // declared settings surface. + let mut labels = Map::new(); + let mut modes = Map::new(); + let mut maps_out = Map::new(); + let mut heroes_out = Map::new(); + let mut teams_out = Map::new(); + let mut enums_out = Map::new(); + let mut tokens_out = Map::new(); + for (section, surface_entries, _) in §ions { + let target = match *section { + "labels" => &mut labels, + "modes" => &mut modes, + "maps" => &mut maps_out, + "heroes" => &mut heroes_out, + "teams" => &mut teams_out, + "enums" => &mut enums_out, + "tokens" => &mut tokens_out, + _ => unreachable!(), + }; + for (_, en) in surface_entries { + if let Some(entry) = entries.get(en) { + target.insert(en.clone(), entry.clone()); + } + } + } + + Ok(serde_json::json!({ + "schemaVersion": 1, + "locale": "zh-CN", + "provenance": { + "generator": "workshop-catalog-gen corpus", + "generatorVersion": env!("CARGO_PKG_VERSION"), + "source": "user-provided workshop-data export (workshop-data.json)", + "commit": meta.get("commit").and_then(Value::as_str).unwrap_or(""), + "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), + "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), + "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", + "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + }, + "labels": labels, + "modes": modes, + "maps": maps_out, + "heroes": heroes_out, + "teams": teams_out, + "enums": enums_out, + "tokens": tokens_out, + "excluded": excluded, + "coverage": coverage, + })) + } + + fn format_report(report: Report<'_>) -> Vec { + let Report { + coverage, + total_matched, + total_entries, + excluded, + settings, + catalog_file, + manifest_path, + settings_out, + } = report; + let mut lines = vec![format!( + "corpus: zh-CN matched {total_matched}/{total_entries} canonical entries and enum members" + )]; + for (category, matched, total) in coverage { + lines.push(format!(" {category}: {matched}/{total}")); + } + lines.push(format!(" excluded (fail-explicit): {}", excluded.len())); + for exclusion in excluded { + lines.push(format!( + " {} {} ({}): {}", + exclusion.kind, exclusion.id, exclusion.en, exclusion.reason + )); + } + let settings_coverage = settings + .get("coverage") + .and_then(Value::as_object) + .map(|coverage| { + coverage + .iter() + .map(|(section, counts)| { + format!( + "{} {}/{}", + section, + counts["matched"].as_u64().unwrap_or(0), + counts["total"].as_u64().unwrap_or(0) + ) + }) + .collect::>() + .join(", ") + }) + .unwrap_or_default(); + lines.push(format!(" settings: {settings_coverage}")); + lines.push(format!("wrote {}", catalog_file.display())); + lines.push(format!("wrote {}", manifest_path.display())); + lines.push(format!("wrote {}", settings_out.display())); + lines.push( + "next: run 'workshop-catalog-gen build' (fresh digest) then 'check' (verify)" + .to_string(), + ); + lines + } +} diff --git a/crates/workshop-rs/src/catalog/data/catalog.json b/crates/workshop-rs/src/catalog/data/catalog.json index 0a69f79..7713076 100644 --- a/crates/workshop-rs/src/catalog/data/catalog.json +++ b/crates/workshop-rs/src/catalog/data/catalog.json @@ -2,14 +2,16 @@ "actions": [ { "aliases": { - "en-US": "Disable Inspector Recording" + "en-US": "Disable Inspector Recording", + "zh-CN": "禁用查看器录制" }, "id": "disableInspector", "params": [] }, { "aliases": { - "en-US": "Wait" + "en-US": "Wait", + "zh-CN": "等待" }, "id": "wait", "paramDefaults": [ @@ -27,7 +29,8 @@ }, { "aliases": { - "en-US": "Create Beam Effect" + "en-US": "Create Beam Effect", + "zh-CN": "创建光束效果" }, "id": "createBeamEffect", "paramDomains": [ @@ -49,7 +52,8 @@ }, { "aliases": { - "en-US": "Create HUD Text" + "en-US": "Create HUD Text", + "zh-CN": "创建HUD文本" }, "id": "createHudText", "paramDefaults": [ @@ -94,7 +98,8 @@ }, { "aliases": { - "en-US": "Play Effect" + "en-US": "Play Effect", + "zh-CN": "播放效果" }, "id": "playEffect", "paramDomains": [ @@ -114,7 +119,8 @@ }, { "aliases": { - "en-US": "Set Aim Speed" + "en-US": "Set Aim Speed", + "zh-CN": "设置瞄准速度" }, "id": "setAimSpeed", "params": [ @@ -124,7 +130,8 @@ }, { "aliases": { - "en-US": "Set Damage Dealt" + "en-US": "Set Damage Dealt", + "zh-CN": "设置造成伤害" }, "id": "setDamageDealt", "params": [ @@ -134,7 +141,8 @@ }, { "aliases": { - "en-US": "Set Damage Received" + "en-US": "Set Damage Received", + "zh-CN": "设置受到伤害" }, "id": "setDamageReceived", "params": [ @@ -144,7 +152,8 @@ }, { "aliases": { - "en-US": "Set Gravity" + "en-US": "Set Gravity", + "zh-CN": "设置引力" }, "id": "setGravity", "params": [ @@ -154,7 +163,8 @@ }, { "aliases": { - "en-US": "Set Player Health" + "en-US": "Set Player Health", + "zh-CN": "设置玩家生命值" }, "id": "setHealth", "params": [ @@ -164,7 +174,8 @@ }, { "aliases": { - "en-US": "Set Max Health" + "en-US": "Set Max Health", + "zh-CN": "设置最大生命值" }, "id": "setMaxHealth", "params": [ @@ -174,7 +185,8 @@ }, { "aliases": { - "en-US": "Set Move Speed" + "en-US": "Set Move Speed", + "zh-CN": "设置移动速度" }, "id": "setMoveSpeed", "params": [ @@ -184,7 +196,8 @@ }, { "aliases": { - "en-US": "Set Ultimate Charge" + "en-US": "Set Ultimate Charge", + "zh-CN": "设置终极技能充能" }, "id": "setUltCharge", "params": [ @@ -194,7 +207,8 @@ }, { "aliases": { - "en-US": "Teleport" + "en-US": "Teleport", + "zh-CN": "传送" }, "id": "teleport", "params": [ @@ -204,7 +218,8 @@ }, { "aliases": { - "en-US": "Chase Global Variable At Rate" + "en-US": "Chase Global Variable At Rate", + "zh-CN": "追踪全局变量频率" }, "id": "chaseAtRate", "paramDomains": [ @@ -222,7 +237,8 @@ }, { "aliases": { - "en-US": "Chase Global Variable Over Time" + "en-US": "Chase Global Variable Over Time", + "zh-CN": "持续追踪全局变量" }, "id": "chaseOverTime", "paramDomains": [ @@ -240,7 +256,8 @@ }, { "aliases": { - "en-US": "Chase Player Variable At Rate" + "en-US": "Chase Player Variable At Rate", + "zh-CN": "追踪玩家变量频率" }, "id": "chasePlayerVariableAtRate", "paramDomains": [ @@ -260,7 +277,8 @@ }, { "aliases": { - "en-US": "Chase Player Variable Over Time" + "en-US": "Chase Player Variable Over Time", + "zh-CN": "持续追踪玩家变量" }, "id": "chasePlayerVariableOverTime", "paramDomains": [ @@ -280,7 +298,8 @@ }, { "aliases": { - "en-US": "Set Invisible" + "en-US": "Set Invisible", + "zh-CN": "设置不可见" }, "id": "setInvisibility", "paramDefaults": [ @@ -298,7 +317,8 @@ }, { "aliases": { - "en-US": "Set Status" + "en-US": "Set Status", + "zh-CN": "设置状态" }, "id": "setStatusEffect", "params": [ @@ -310,7 +330,8 @@ }, { "aliases": { - "en-US": "Big Message" + "en-US": "Big Message", + "zh-CN": "大字体信息" }, "id": "bigMessage", "paramDefaults": [ @@ -324,7 +345,8 @@ }, { "aliases": { - "en-US": "Small Message" + "en-US": "Small Message", + "zh-CN": "小字体信息" }, "id": "smallMessage", "paramDefaults": [ @@ -338,7 +360,8 @@ }, { "aliases": { - "en-US": "Wait Until" + "en-US": "Wait Until", + "zh-CN": "等待直到 " }, "id": "waitUntil", "params": [ @@ -348,7 +371,8 @@ }, { "aliases": { - "en-US": "Skip" + "en-US": "Skip", + "zh-CN": "跳过" }, "id": "skip", "params": [ @@ -357,14 +381,16 @@ }, { "aliases": { - "en-US": "Loop If Condition Is True" + "en-US": "Loop If Condition Is True", + "zh-CN": "如条件为“真”则循环" }, "id": "loopIfConditionIsTrue", "params": [] }, { "aliases": { - "en-US": "Abort If" + "en-US": "Abort If", + "zh-CN": "根据条件中止" }, "id": "abortIf", "params": [ @@ -373,7 +399,8 @@ }, { "aliases": { - "en-US": "Modify Global Variable" + "en-US": "Modify Global Variable", + "zh-CN": "修改全局变量" }, "id": "modifyGlobalVariable", "paramDomains": [ @@ -389,7 +416,8 @@ }, { "aliases": { - "en-US": "Create Effect" + "en-US": "Create Effect", + "zh-CN": "创建效果" }, "id": "createEffect", "paramDomains": [ @@ -411,7 +439,8 @@ }, { "aliases": { - "en-US": "Create In-World Text" + "en-US": "Create In-World Text", + "zh-CN": "创建地图文本" }, "id": "createInWorldText", "paramDefaults": [ @@ -447,7 +476,8 @@ }, { "aliases": { - "en-US": "Create Progress Bar In-World Text" + "en-US": "Create Progress Bar In-World Text", + "zh-CN": "创建进度条地图文本" }, "id": "createProgressBarInWorldText", "paramDomains": [ @@ -477,7 +507,8 @@ }, { "aliases": { - "en-US": "Start Camera" + "en-US": "Start Camera", + "zh-CN": "开始镜头" }, "id": "startCamera", "paramDefaults": [ @@ -495,7 +526,8 @@ }, { "aliases": { - "en-US": "Stop Camera" + "en-US": "Stop Camera", + "zh-CN": "停止镜头" }, "id": "stopCamera", "params": [ @@ -504,7 +536,8 @@ }, { "aliases": { - "en-US": "Start Game Mode" + "en-US": "Start Game Mode", + "zh-CN": "开始游戏模式" }, "id": "startGameMode", "params": [] @@ -559,7 +592,8 @@ }, { "aliases": { - "en-US": "Stop Forcing Throttle" + "en-US": "Stop Forcing Throttle", + "zh-CN": "停止限制阈值" }, "id": "stopForcingThrottle", "params": [ @@ -568,7 +602,8 @@ }, { "aliases": { - "en-US": "Disable Game Mode HUD" + "en-US": "Disable Game Mode HUD", + "zh-CN": "隐藏游戏模式HUD" }, "id": "disableGameModeHud", "params": [ @@ -577,7 +612,8 @@ }, { "aliases": { - "en-US": "Disable Game Mode In-World UI" + "en-US": "Disable Game Mode In-World UI", + "zh-CN": "隐藏游戏模式地图UI" }, "id": "disableGameModeInworldUI", "params": [ @@ -586,7 +622,8 @@ }, { "aliases": { - "en-US": "Disable Hero HUD" + "en-US": "Disable Hero HUD", + "zh-CN": "隐藏英雄HUD" }, "id": "disableHeroHud", "params": [ @@ -595,7 +632,8 @@ }, { "aliases": { - "en-US": "Disable Scoreboard" + "en-US": "Disable Scoreboard", + "zh-CN": "隐藏计分板" }, "id": "disableScoreboard", "params": [ @@ -604,7 +642,8 @@ }, { "aliases": { - "en-US": "Enable Game Mode HUD" + "en-US": "Enable Game Mode HUD", + "zh-CN": "显示游戏模式HUD" }, "id": "enableGameModeHud", "params": [ @@ -613,7 +652,8 @@ }, { "aliases": { - "en-US": "Enable Game Mode In-World UI" + "en-US": "Enable Game Mode In-World UI", + "zh-CN": "显示游戏模式地图UI" }, "id": "enableGameModeInworldUI", "params": [ @@ -622,7 +662,8 @@ }, { "aliases": { - "en-US": "Enable Hero HUD" + "en-US": "Enable Hero HUD", + "zh-CN": "显示英雄HUD" }, "id": "enableHeroHud", "params": [ @@ -631,7 +672,8 @@ }, { "aliases": { - "en-US": "Enable Scoreboard" + "en-US": "Enable Scoreboard", + "zh-CN": "显示计分板" }, "id": "enableScoreboard", "params": [ @@ -640,14 +682,16 @@ }, { "aliases": { - "en-US": "Enable Inspector Recording" + "en-US": "Enable Inspector Recording", + "zh-CN": "启用查看器录制" }, "id": "enableInspectorRecording", "params": [] }, { "aliases": { - "en-US": "Disable Movement Collision With Environment" + "en-US": "Disable Movement Collision With Environment", + "zh-CN": "取消与环境的移动碰撞" }, "id": "disableMovementCollisionWithEnvironment", "params": [ @@ -657,7 +701,8 @@ }, { "aliases": { - "en-US": "Disable Movement Collision With Players" + "en-US": "Disable Movement Collision With Players", + "zh-CN": "取消与玩家的移动碰撞" }, "id": "disableMovementCollisionWithPlayers", "params": [ @@ -666,7 +711,8 @@ }, { "aliases": { - "en-US": "Enable Movement Collision With Environment" + "en-US": "Enable Movement Collision With Environment", + "zh-CN": "开启与环境的移动碰撞" }, "id": "enableMovementCollisionWithEnvironment", "params": [ @@ -675,7 +721,8 @@ }, { "aliases": { - "en-US": "Enable Movement Collision With Players" + "en-US": "Enable Movement Collision With Players", + "zh-CN": "开启与玩家的移动碰撞" }, "id": "enableMovementCollisionWithPlayers", "params": [ @@ -684,7 +731,8 @@ }, { "aliases": { - "en-US": "Disallow Button" + "en-US": "Disallow Button", + "zh-CN": "禁用按钮" }, "id": "disallowButton", "paramDomains": [ @@ -698,7 +746,8 @@ }, { "aliases": { - "en-US": "Allow Button" + "en-US": "Allow Button", + "zh-CN": "可用按钮" }, "id": "allowButton", "paramDomains": [ @@ -712,7 +761,8 @@ }, { "aliases": { - "en-US": "Destroy HUD Text" + "en-US": "Destroy HUD Text", + "zh-CN": "消除HUD文本" }, "id": "destroyHudText", "params": [ @@ -721,7 +771,8 @@ }, { "aliases": { - "en-US": "Destroy In-World Text" + "en-US": "Destroy In-World Text", + "zh-CN": "消除地图文本" }, "id": "destroyInWorldText", "params": [ @@ -730,7 +781,8 @@ }, { "aliases": { - "en-US": "Destroy Effect" + "en-US": "Destroy Effect", + "zh-CN": "消除效果" }, "id": "destroyEffect", "params": [ @@ -739,14 +791,16 @@ }, { "aliases": { - "en-US": "Destroy All Progress Bar HUD Text" + "en-US": "Destroy All Progress Bar HUD Text", + "zh-CN": "消除所有进度条HUD文本" }, "id": "destroyAllProgressBarHudText", "params": [] }, { "aliases": { - "en-US": "Destroy All Progress Bar In-World Text" + "en-US": "Destroy All Progress Bar In-World Text", + "zh-CN": "消除所有进度条地图文本" }, "id": "destroyAllProgressBarInWorldText", "params": [] @@ -781,116 +835,134 @@ }, { "aliases": { - "en-US": "Abort" + "en-US": "Abort", + "zh-CN": "中止" }, "id": "abort", "params": [] } ], - "digest": "75d8bb9c50e3ad0606656d58897b49e37934e04781fb2a510eefcd555dc2e29f", + "digest": "5a7f7ba75a81f52d33b039fb3f0f2d367959c66b23bc874deb2357514eb7815d", "enums": [ { "domain": "Color", "members": [ { "aliases": { - "en-US": "Yellow" + "en-US": "Yellow", + "zh-CN": "黄色" }, "id": "YELLOW" }, { "aliases": { - "en-US": "White" + "en-US": "White", + "zh-CN": "白色" }, "id": "WHITE" }, { "aliases": { - "en-US": "Red" + "en-US": "Red", + "zh-CN": "红色" }, "id": "RED" }, { "aliases": { - "en-US": "Orange" + "en-US": "Orange", + "zh-CN": "橙色" }, "id": "ORANGE" }, { "aliases": { - "en-US": "Green" + "en-US": "Green", + "zh-CN": "绿色" }, "id": "GREEN" }, { "aliases": { - "en-US": "Purple" + "en-US": "Purple", + "zh-CN": "亮紫色" }, "id": "PURPLE" }, { "aliases": { - "en-US": "Blue" + "en-US": "Blue", + "zh-CN": "蓝色" }, "id": "BLUE" }, { "aliases": { - "en-US": "Aqua" + "en-US": "Aqua", + "zh-CN": "水绿色" }, "id": "AQUA" }, { "aliases": { - "en-US": "Sky Blue" + "en-US": "Sky Blue", + "zh-CN": "天蓝色" }, "id": "SKY_BLUE" }, { "aliases": { - "en-US": "Turquoise" + "en-US": "Turquoise", + "zh-CN": "青绿色" }, "id": "TURQUOISE" }, { "aliases": { - "en-US": "Lime Green" + "en-US": "Lime Green", + "zh-CN": "灰绿色" }, "id": "LIME_GREEN" }, { "aliases": { - "en-US": "Gray" + "en-US": "Gray", + "zh-CN": "灰色" }, "id": "GRAY" }, { "aliases": { - "en-US": "Violet" + "en-US": "Violet", + "zh-CN": "紫色" }, "id": "VIOLET" }, { "aliases": { - "en-US": "Rose" + "en-US": "Rose", + "zh-CN": "玫红" }, "id": "ROSE" }, { "aliases": { - "en-US": "Black" + "en-US": "Black", + "zh-CN": "黑色" }, "id": "BLACK" }, { "aliases": { - "en-US": "Team 1" + "en-US": "Team 1", + "zh-CN": "队伍1" }, "id": "TEAM_1" }, { "aliases": { - "en-US": "Team 2" + "en-US": "Team 2", + "zh-CN": "队伍2" }, "id": "TEAM_2" } @@ -901,13 +973,15 @@ "members": [ { "aliases": { - "en-US": "Grapple Beam" + "en-US": "Grapple Beam", + "zh-CN": "抓钩光束" }, "id": "GRAPPLE" }, { "aliases": { - "en-US": "Good Beam" + "en-US": "Good Beam", + "zh-CN": "有益光束" }, "id": "GOOD" } @@ -918,37 +992,43 @@ "members": [ { "aliases": { - "en-US": "Bad Explosion" + "en-US": "Bad Explosion", + "zh-CN": "有害爆炸" }, "id": "BAD_EXPLOSION" }, { "aliases": { - "en-US": "Buff Impact Sound" + "en-US": "Buff Impact Sound", + "zh-CN": "正面状态施加声音" }, "id": "BUFF_IMPACT_SOUND" }, { "aliases": { - "en-US": "Debuff Impact Sound" + "en-US": "Debuff Impact Sound", + "zh-CN": "负面状态施加声音" }, "id": "DEBUFF_IMPACT_SOUND" }, { "aliases": { - "en-US": "Buff Explosion Sound" + "en-US": "Buff Explosion Sound", + "zh-CN": "状态爆炸声音" }, "id": "BUFF_EXPLOSION_SOUND" }, { "aliases": { - "en-US": "Explosion Sound" + "en-US": "Explosion Sound", + "zh-CN": "爆炸声音" }, "id": "EXPLOSION_SOUND" }, { "aliases": { - "en-US": "Ring Explosion Sound" + "en-US": "Ring Explosion Sound", + "zh-CN": "环状爆炸声音" }, "id": "RING_EXPLOSION" } @@ -959,13 +1039,15 @@ "members": [ { "aliases": { - "en-US": "Ignore Condition" + "en-US": "Ignore Condition", + "zh-CN": "无视条件" }, "id": "IGNORE_CONDITION" }, { "aliases": { - "en-US": "Abort When False" + "en-US": "Abort When False", + "zh-CN": "当为“假”时中止" }, "id": "ABORT_WHEN_FALSE" } @@ -976,7 +1058,8 @@ "members": [ { "aliases": { - "en-US": "Up" + "en-US": "Up", + "zh-CN": "上" }, "id": "UP" } @@ -987,13 +1070,15 @@ "members": [ { "aliases": { - "en-US": "Left" + "en-US": "Left", + "zh-CN": "左边" }, "id": "LEFT" }, { "aliases": { - "en-US": "Right" + "en-US": "Right", + "zh-CN": "右边" }, "id": "RIGHT" } @@ -1004,31 +1089,36 @@ "members": [ { "aliases": { - "en-US": "Visible To Sort Order String and Color" + "en-US": "Visible To Sort Order String and Color", + "zh-CN": "可见,排序规则,字符串和颜色" }, "id": "VISIBILITY_SORT_ORDER_STRING_AND_COLOR" }, { "aliases": { - "en-US": "Visible To and String" + "en-US": "Visible To and String", + "zh-CN": "可见和字符串" }, "id": "VISIBILITY_AND_STRING" }, { "aliases": { - "en-US": "Visible To" + "en-US": "Visible To", + "zh-CN": "可见" }, "id": "VISIBILITY" }, { "aliases": { - "en-US": "Visible To String and Color" + "en-US": "Visible To String and Color", + "zh-CN": "可见,字符串和颜色" }, "id": "VISIBLE_TO_STRING_AND_COLOR" }, { "aliases": { - "en-US": "Visible To and Color" + "en-US": "Visible To and Color", + "zh-CN": "可见和颜色" }, "id": "VISIBLE_TO_AND_COLOR" } @@ -1039,13 +1129,15 @@ "members": [ { "aliases": { - "en-US": "None" + "en-US": "None", + "zh-CN": "全部禁用" }, "id": "NONE" }, { "aliases": { - "en-US": "Destination and Duration" + "en-US": "Destination and Duration", + "zh-CN": "终点及持续时间" }, "id": "DESTINATION_AND_DURATION" } @@ -1056,13 +1148,15 @@ "members": [ { "aliases": { - "en-US": "None" + "en-US": "None", + "zh-CN": "全部禁用" }, "id": "NONE" }, { "aliases": { - "en-US": "Destination and Rate" + "en-US": "Destination and Rate", + "zh-CN": "速率及最终值" }, "id": "DESTINATION_AND_RATE" } @@ -1073,19 +1167,22 @@ "members": [ { "aliases": { - "en-US": "Default Visibility" + "en-US": "Default Visibility", + "zh-CN": "默认可见度" }, "id": "DEFAULT" }, { "aliases": { - "en-US": "Visible Always" + "en-US": "Visible Always", + "zh-CN": "始终可见" }, "id": "VISIBLE_ALWAYS" }, { "aliases": { - "en-US": "Visible Never" + "en-US": "Visible Never", + "zh-CN": "始终不可见" }, "id": "VISIBLE_NEVER" } @@ -1096,19 +1193,22 @@ "members": [ { "aliases": { - "en-US": "All Teams" + "en-US": "All Teams", + "zh-CN": "所有队伍" }, "id": "ALL" }, { "aliases": { - "en-US": "Team 1" + "en-US": "Team 1", + "zh-CN": "队伍1" }, "id": "TEAM_1" }, { "aliases": { - "en-US": "Team 2" + "en-US": "Team 2", + "zh-CN": "队伍2" }, "id": "TEAM_2" } @@ -1119,19 +1219,22 @@ "members": [ { "aliases": { - "en-US": "All" + "en-US": "All", + "zh-CN": "全部" }, "id": "ALL" }, { "aliases": { - "en-US": "Enemies" + "en-US": "Enemies", + "zh-CN": "敌人" }, "id": "ENEMIES" }, { "aliases": { - "en-US": "None" + "en-US": "None", + "zh-CN": "全部禁用" }, "id": "NONE" } @@ -1142,25 +1245,29 @@ "members": [ { "aliases": { - "en-US": "Off" + "en-US": "Off", + "zh-CN": "关闭" }, "id": "OFF" }, { "aliases": { - "en-US": "Surfaces" + "en-US": "Surfaces", + "zh-CN": "表面" }, "id": "SURFACES" }, { "aliases": { - "en-US": "Surfaces And All Barriers" + "en-US": "Surfaces And All Barriers", + "zh-CN": "表面及全部屏障" }, "id": "SURFACES_AND_ALL_BARRIERS" }, { "aliases": { - "en-US": "Surfaces And Enemy Barriers" + "en-US": "Surfaces And Enemy Barriers", + "zh-CN": "表面及敌方屏障" }, "id": "SURFACES_AND_ENEMY_BARRIERS" } @@ -1171,61 +1278,71 @@ "members": [ { "aliases": { - "en-US": "Asleep" + "en-US": "Asleep", + "zh-CN": "沉睡" }, "id": "ASLEEP" }, { "aliases": { - "en-US": "Burning" + "en-US": "Burning", + "zh-CN": "燃烧" }, "id": "BURNING" }, { "aliases": { - "en-US": "Frozen" + "en-US": "Frozen", + "zh-CN": "冰冻" }, "id": "FROZEN" }, { "aliases": { - "en-US": "Hacked" + "en-US": "Hacked", + "zh-CN": "被入侵" }, "id": "HACKED" }, { "aliases": { - "en-US": "Invincible" + "en-US": "Invincible", + "zh-CN": "无敌" }, "id": "INVINCIBLE" }, { "aliases": { - "en-US": "Knocked Down" + "en-US": "Knocked Down", + "zh-CN": "击倒" }, "id": "KNOCKED_DOWN" }, { "aliases": { - "en-US": "Phased Out" + "en-US": "Phased Out", + "zh-CN": "相移" }, "id": "PHASED_OUT" }, { "aliases": { - "en-US": "Rooted" + "en-US": "Rooted", + "zh-CN": "定身" }, "id": "ROOTED" }, { "aliases": { - "en-US": "Stunned" + "en-US": "Stunned", + "zh-CN": "击晕" }, "id": "STUNNED" }, { "aliases": { - "en-US": "Unkillable" + "en-US": "Unkillable", + "zh-CN": "无法杀死" }, "id": "UNKILLABLE" } @@ -1236,13 +1353,15 @@ "members": [ { "aliases": { - "en-US": "Rotation" + "en-US": "Rotation", + "zh-CN": "旋转" }, "id": "ROTATION" }, { "aliases": { - "en-US": "Rotation And Translation" + "en-US": "Rotation And Translation", + "zh-CN": "旋转并转换" }, "id": "ROTATION_AND_TRANSLATION" } @@ -1253,61 +1372,71 @@ "members": [ { "aliases": { - "en-US": "Primary Fire" + "en-US": "Primary Fire", + "zh-CN": "主要攻击模式" }, "id": "PRIMARY_FIRE" }, { "aliases": { - "en-US": "Secondary Fire" + "en-US": "Secondary Fire", + "zh-CN": "辅助攻击模式" }, "id": "SECONDARY_FIRE" }, { "aliases": { - "en-US": "Ability 1" + "en-US": "Ability 1", + "zh-CN": "技能1" }, "id": "ABILITY_1" }, { "aliases": { - "en-US": "Ability 2" + "en-US": "Ability 2", + "zh-CN": "技能2" }, "id": "ABILITY_2" }, { "aliases": { - "en-US": "Ultimate" + "en-US": "Ultimate", + "zh-CN": "终极技能" }, "id": "ULTIMATE" }, { "aliases": { - "en-US": "Crouch" + "en-US": "Crouch", + "zh-CN": "蹲下" }, "id": "CROUCH" }, { "aliases": { - "en-US": "Interact" + "en-US": "Interact", + "zh-CN": "互动" }, "id": "INTERACT" }, { "aliases": { - "en-US": "Jump" + "en-US": "Jump", + "zh-CN": "跳跃" }, "id": "JUMP" }, { "aliases": { - "en-US": "Melee" + "en-US": "Melee", + "zh-CN": "近身攻击" }, "id": "MELEE" }, { "aliases": { - "en-US": "Reload" + "en-US": "Reload", + "zh-CN": "装填" }, "id": "RELOAD" } @@ -1318,13 +1447,15 @@ "members": [ { "aliases": { - "en-US": "Do Not Clip" + "en-US": "Do Not Clip", + "zh-CN": "不要截取" }, "id": "DO_NOT_CLIP" }, { "aliases": { - "en-US": "Clip Against Surfaces" + "en-US": "Clip Against Surfaces", + "zh-CN": "根据表面截取" }, "id": "CLIP_AGAINST_SURFACES" } @@ -1335,7 +1466,8 @@ "members": [ { "aliases": { - "en-US": "Orb" + "en-US": "Orb", + "zh-CN": "球" }, "id": "ORB" } @@ -1346,25 +1478,29 @@ "members": [ { "aliases": { - "en-US": "Visible To Position and Radius" + "en-US": "Visible To Position and Radius", + "zh-CN": "可见,位置和半径" }, "id": "VISIBLE_TO_POSITION_AND_RADIUS" }, { "aliases": { - "en-US": "Visible To" + "en-US": "Visible To", + "zh-CN": "可见" }, "id": "VISIBILITY" }, { "aliases": { - "en-US": "Color" + "en-US": "Color", + "zh-CN": "颜色" }, "id": "COLOR" }, { "aliases": { - "en-US": "Visible To and Color" + "en-US": "Visible To and Color", + "zh-CN": "可见和颜色" }, "id": "VISIBILITY_AND_COLOR" } @@ -1375,193 +1511,225 @@ "members": [ { "aliases": { - "en-US": "D.Va" + "en-US": "D.Va", + "zh-CN": "D.Va" }, "id": "DVA" }, { "aliases": { - "en-US": "Orisa" + "en-US": "Orisa", + "zh-CN": "奥丽莎" }, "id": "ORISA" }, { "aliases": { - "en-US": "Reinhardt" + "en-US": "Reinhardt", + "zh-CN": "莱因哈特" }, "id": "REINHARDT" }, { "aliases": { - "en-US": "Roadhog" + "en-US": "Roadhog", + "zh-CN": "路霸" }, "id": "ROADHOG" }, { "aliases": { - "en-US": "Sigma" + "en-US": "Sigma", + "zh-CN": "西格玛" }, "id": "SIGMA" }, { "aliases": { - "en-US": "Wrecking Ball" + "en-US": "Wrecking Ball", + "zh-CN": "破坏球" }, "id": "WRECKING_BALL" }, { "aliases": { - "en-US": "Winston" + "en-US": "Winston", + "zh-CN": "温斯顿" }, "id": "WINSTON" }, { "aliases": { - "en-US": "Zarya" + "en-US": "Zarya", + "zh-CN": "查莉娅" }, "id": "ZARYA" }, { "aliases": { - "en-US": "Ashe" + "en-US": "Ashe", + "zh-CN": "艾什" }, "id": "ASHE" }, { "aliases": { - "en-US": "Bastion" + "en-US": "Bastion", + "zh-CN": "堡垒" }, "id": "BASTION" }, { "aliases": { - "en-US": "Cassidy" + "en-US": "Cassidy", + "zh-CN": "卡西迪" }, "id": "CASSIDY" }, { "aliases": { - "en-US": "Doomfist" + "en-US": "Doomfist", + "zh-CN": "末日铁拳" }, "id": "DOOMFIST" }, { "aliases": { - "en-US": "Echo" + "en-US": "Echo", + "zh-CN": "回声" }, "id": "ECHO" }, { "aliases": { - "en-US": "Genji" + "en-US": "Genji", + "zh-CN": "源氏" }, "id": "GENJI" }, { "aliases": { - "en-US": "Hanzo" + "en-US": "Hanzo", + "zh-CN": "半藏" }, "id": "HANZO" }, { "aliases": { - "en-US": "Junkrat" + "en-US": "Junkrat", + "zh-CN": "狂鼠" }, "id": "JUNKRAT" }, { "aliases": { - "en-US": "Mei" + "en-US": "Mei", + "zh-CN": "美" }, "id": "MEI" }, { "aliases": { - "en-US": "Pharah" + "en-US": "Pharah", + "zh-CN": "法老之鹰" }, "id": "PHARAH" }, { "aliases": { - "en-US": "Reaper" + "en-US": "Reaper", + "zh-CN": "死神" }, "id": "REAPER" }, { "aliases": { - "en-US": "Soldier: 76" + "en-US": "Soldier: 76", + "zh-CN": "士兵:76" }, "id": "SOLDIER_76" }, { "aliases": { - "en-US": "Symmetra" + "en-US": "Symmetra", + "zh-CN": "秩序之光" }, "id": "SYMMETRA" }, { "aliases": { - "en-US": "Sombra" + "en-US": "Sombra", + "zh-CN": "黑影" }, "id": "SOMBRA" }, { "aliases": { - "en-US": "Tracer" + "en-US": "Tracer", + "zh-CN": "猎空" }, "id": "TRACER" }, { "aliases": { - "en-US": "Torbjörn" + "en-US": "Torbjörn", + "zh-CN": "托比昂" }, "id": "TORBJORN" }, { "aliases": { - "en-US": "Widowmaker" + "en-US": "Widowmaker", + "zh-CN": "黑百合" }, "id": "WIDOWMAKER" }, { "aliases": { - "en-US": "Ana" + "en-US": "Ana", + "zh-CN": "安娜" }, "id": "ANA" }, { "aliases": { - "en-US": "Brigitte" + "en-US": "Brigitte", + "zh-CN": "布丽吉塔" }, "id": "BRIGITTE" }, { "aliases": { - "en-US": "Baptiste" + "en-US": "Baptiste", + "zh-CN": "巴蒂斯特" }, "id": "BAPTISTE" }, { "aliases": { - "en-US": "Lúcio" + "en-US": "Lúcio", + "zh-CN": "卢西奥" }, "id": "LUCIO" }, { "aliases": { - "en-US": "Moira" + "en-US": "Moira", + "zh-CN": "莫伊拉" }, "id": "MOIRA" }, { "aliases": { - "en-US": "Mercy" + "en-US": "Mercy", + "zh-CN": "天使" }, "id": "MERCY" }, { "aliases": { - "en-US": "Zenyatta" + "en-US": "Zenyatta", + "zh-CN": "禅雅塔" }, "id": "ZENYATTA" } @@ -1572,31 +1740,36 @@ "members": [ { "aliases": { - "en-US": "No" + "en-US": "No", + "zh-CN": "拒绝" }, "id": "NO" }, { "aliases": { - "en-US": "Question Mark" + "en-US": "Question Mark", + "zh-CN": "问号" }, "id": "QUESTION_MARK" }, { "aliases": { - "en-US": "Skull" + "en-US": "Skull", + "zh-CN": "骷髅" }, "id": "SKULL" }, { "aliases": { - "en-US": "Checkmark" + "en-US": "Checkmark", + "zh-CN": "对号" }, "id": "CHECKMARK" }, { "aliases": { - "en-US": "Ring Thin" + "en-US": "Ring Thin", + "zh-CN": "细环" }, "id": "RING_THIN" } @@ -1607,73 +1780,85 @@ "members": [ { "aliases": { - "en-US": "Hanamura" + "en-US": "Hanamura", + "zh-CN": "花村" }, "id": "HANAMURA" }, { "aliases": { - "en-US": "Hanamura Winter" + "en-US": "Hanamura Winter", + "zh-CN": "圣诞节花村" }, "id": "HANAMURA_WINTER" }, { "aliases": { - "en-US": "Horizon Lunar Colony" + "en-US": "Horizon Lunar Colony", + "zh-CN": "“地平线”月球基地" }, "id": "HORIZON_LUNAR_COLONY" }, { "aliases": { - "en-US": "Paris" + "en-US": "Paris", + "zh-CN": "巴黎" }, "id": "PARIS" }, { "aliases": { - "en-US": "Temple of Anubis" + "en-US": "Temple of Anubis", + "zh-CN": "阿努比斯神殿" }, "id": "TEMPLE_OF_ANUBIS" }, { "aliases": { - "en-US": "Volskaya Industries" + "en-US": "Volskaya Industries", + "zh-CN": "沃斯卡娅工业区" }, "id": "VOLSKAYA_INDUSTRIES" }, { "aliases": { - "en-US": "Hanaoka" + "en-US": "Hanaoka", + "zh-CN": "花冈" }, "id": "HANAOKA" }, { "aliases": { - "en-US": "Throne of Anubis" + "en-US": "Throne of Anubis", + "zh-CN": "阿努比斯王座" }, "id": "THRONE_OF_ANUBIS" }, { "aliases": { - "en-US": "Antarctic Peninsula" + "en-US": "Antarctic Peninsula", + "zh-CN": "南极半岛" }, "id": "ANTARCTIC_PENINSULA" }, { "aliases": { - "en-US": "Busan" + "en-US": "Busan", + "zh-CN": "釜山" }, "id": "BUSAN" }, { "aliases": { - "en-US": "Ilios" + "en-US": "Ilios", + "zh-CN": "伊利奥斯" }, "id": "ILIOS" }, { "aliases": { - "en-US": "Lijiang Tower" + "en-US": "Lijiang Tower", + "zh-CN": "漓江塔" }, "id": "LIJIANG_TOWER" }, @@ -1685,175 +1870,204 @@ }, { "aliases": { - "en-US": "Nepal" + "en-US": "Nepal", + "zh-CN": "尼泊尔" }, "id": "NEPAL" }, { "aliases": { - "en-US": "Oasis" + "en-US": "Oasis", + "zh-CN": "绿洲城" }, "id": "OASIS" }, { "aliases": { - "en-US": "Samoa" + "en-US": "Samoa", + "zh-CN": "萨摩亚" }, "id": "SAMOA" }, { "aliases": { - "en-US": "Circuit Royal" + "en-US": "Circuit Royal", + "zh-CN": "皇家赛道" }, "id": "CIRCUIT_ROYAL" }, { "aliases": { - "en-US": "Dorado" + "en-US": "Dorado", + "zh-CN": "多拉多" }, "id": "DORADO" }, { "aliases": { - "en-US": "Havana" + "en-US": "Havana", + "zh-CN": "哈瓦那" }, "id": "HAVANA" }, { "aliases": { - "en-US": "Junkertown" + "en-US": "Junkertown", + "zh-CN": "渣客镇" }, "id": "JUNKERTOWN" }, { "aliases": { - "en-US": "Rialto" + "en-US": "Rialto", + "zh-CN": "里阿尔托" }, "id": "RIALTO" }, { "aliases": { - "en-US": "Route 66" + "en-US": "Route 66", + "zh-CN": "66号公路" }, "id": "ROUTE_66" }, { "aliases": { - "en-US": "Shambali Monastery" + "en-US": "Shambali Monastery", + "zh-CN": "香巴里寺院" }, "id": "SHAMBALI_MONASTERY" }, { "aliases": { - "en-US": "Watchpoint: Gibraltar" + "en-US": "Watchpoint: Gibraltar", + "zh-CN": "监测站:直布罗陀" }, "id": "WATCHPOINT_GIBRALTAR" }, { "aliases": { - "en-US": "Aatlis" + "en-US": "Aatlis", + "zh-CN": "阿特利斯" }, "id": "AATLIS" }, { "aliases": { - "en-US": "New Junk City" + "en-US": "New Junk City", + "zh-CN": "新渣客城" }, "id": "NEW_JUNK_CITY" }, { "aliases": { - "en-US": "Suravasa" + "en-US": "Suravasa", + "zh-CN": "苏拉瓦萨" }, "id": "SURAVASA" }, { "aliases": { - "en-US": "Blizzard World" + "en-US": "Blizzard World", + "zh-CN": "暴雪世界" }, "id": "BLIZZARD_WORLD" }, { "aliases": { - "en-US": "Blizzard World Winter" + "en-US": "Blizzard World Winter", + "zh-CN": "圣诞节暴雪世界" }, "id": "BLIZZARD_WORLD_WINTER" }, { "aliases": { - "en-US": "Eichenwalde" + "en-US": "Eichenwalde", + "zh-CN": "艾兴瓦尔德" }, "id": "EICHENWALDE" }, { "aliases": { - "en-US": "Eichenwalde Halloween" + "en-US": "Eichenwalde Halloween", + "zh-CN": "万圣节艾兴瓦尔德" }, "id": "EICHENWALDE_HALLOWEEN" }, { "aliases": { - "en-US": "Hollywood" + "en-US": "Hollywood", + "zh-CN": "好莱坞" }, "id": "HOLLYWOOD" }, { "aliases": { - "en-US": "Hollywood Halloween" + "en-US": "Hollywood Halloween", + "zh-CN": "万圣节好莱坞" }, "id": "HOLLYWOOD_HALLOWEEN" }, { "aliases": { - "en-US": "King's Row" + "en-US": "King's Row", + "zh-CN": "国王大道" }, "id": "KINGS_ROW" }, { "aliases": { - "en-US": "King's Row Winter" + "en-US": "King's Row Winter", + "zh-CN": "圣诞节国王大道" }, "id": "KINGS_ROW_WINTER" }, { "aliases": { - "en-US": "Midtown" + "en-US": "Midtown", + "zh-CN": "中城" }, "id": "MIDTOWN" }, { "aliases": { - "en-US": "Numbani" + "en-US": "Numbani", + "zh-CN": "努巴尼" }, "id": "NUMBANI" }, { "aliases": { - "en-US": "Paraíso" + "en-US": "Paraíso", + "zh-CN": "帕拉伊苏" }, "id": "PARAISO" }, { "aliases": { - "en-US": "Colosseo" + "en-US": "Colosseo", + "zh-CN": "斗兽场" }, "id": "COLOSSEO" }, { "aliases": { - "en-US": "Esperança" + "en-US": "Esperança", + "zh-CN": "埃斯佩兰萨" }, "id": "ESPERANCA" }, { "aliases": { - "en-US": "New Queen Street" + "en-US": "New Queen Street", + "zh-CN": "新皇后街" }, "id": "NEW_QUEEN_STREET" }, { "aliases": { - "en-US": "Runasapi" + "en-US": "Runasapi", + "zh-CN": "鲁纳塞彼" }, "id": "RUNASAPI" } @@ -1864,55 +2078,64 @@ "members": [ { "aliases": { - "en-US": "Visible To" + "en-US": "Visible To", + "zh-CN": "可见" }, "id": "VISIBLE_TO" }, { "aliases": { - "en-US": "Visible To and Color" + "en-US": "Visible To and Color", + "zh-CN": "可见和颜色" }, "id": "VISIBLE_TO_AND_COLOR" }, { "aliases": { - "en-US": "Visible To and Position" + "en-US": "Visible To and Position", + "zh-CN": "可见和位置" }, "id": "VISIBLE_TO_AND_POSITION" }, { "aliases": { - "en-US": "Visible To and String" + "en-US": "Visible To and String", + "zh-CN": "可见和字符串" }, "id": "VISIBLE_TO_AND_STRING" }, { "aliases": { - "en-US": "Visible To Position and Color" + "en-US": "Visible To Position and Color", + "zh-CN": "可见,位置和颜色" }, "id": "VISIBLE_TO_POSITION_AND_COLOR" }, { "aliases": { - "en-US": "Visible To Position and String" + "en-US": "Visible To Position and String", + "zh-CN": "可见,位置和字符串" }, "id": "VISIBLE_TO_POSITION_AND_STRING" }, { "aliases": { - "en-US": "Visible To Position String and Color" + "en-US": "Visible To Position String and Color", + "zh-CN": "可见,位置,字符串和颜色" }, "id": "VISIBLE_TO_POSITION_STRING_AND_COLOR" }, { "aliases": { - "en-US": "Visible To String and Color" + "en-US": "Visible To String and Color", + "zh-CN": "可见,字符串和颜色" }, "id": "VISIBLE_TO_STRING_AND_COLOR" }, { "aliases": { - "en-US": "String" + "en-US": "String", + "zh-CN": "字符串" }, "id": "STRING" } @@ -1923,19 +2146,22 @@ "members": [ { "aliases": { - "en-US": "Append To Array" + "en-US": "Append To Array", + "zh-CN": "添加至数组" }, "id": "APPEND_TO_ARRAY" }, { "aliases": { - "en-US": "Remove From Array By Value" + "en-US": "Remove From Array By Value", + "zh-CN": "根据值从数组中移除" }, "id": "REMOVE_FROM_ARRAY_BY_VALUE" }, { "aliases": { - "en-US": "Remove From Array By Index" + "en-US": "Remove From Array By Index", + "zh-CN": "根据索引从数组中移除" }, "id": "REMOVE_FROM_ARRAY_BY_INDEX" } @@ -1957,13 +2183,15 @@ "members": [ { "aliases": { - "en-US": "Up" + "en-US": "Up", + "zh-CN": "上" }, "id": "UP" }, { "aliases": { - "en-US": "Down" + "en-US": "Down", + "zh-CN": "下" }, "id": "DOWN" }, @@ -1979,19 +2207,22 @@ "events": [ { "aliases": { - "en-US": "Ongoing - Global" + "en-US": "Ongoing - Global", + "zh-CN": "持续 - 全局" }, "id": "global" }, { "aliases": { - "en-US": "Ongoing - Each Player" + "en-US": "Ongoing - Each Player", + "zh-CN": "持续 - 每名玩家" }, "id": "eachPlayer" }, { "aliases": { - "en-US": "Subroutine" + "en-US": "Subroutine", + "zh-CN": "子程序" }, "id": "subroutine" } @@ -2039,49 +2270,57 @@ }, { "aliases": { - "en-US": "Add" + "en-US": "Add", + "zh-CN": "加" }, "id": "add" }, { "aliases": { - "en-US": "Subtract" + "en-US": "Subtract", + "zh-CN": "减" }, "id": "subtract" }, { "aliases": { - "en-US": "Multiply" + "en-US": "Multiply", + "zh-CN": "乘" }, "id": "multiply" }, { "aliases": { - "en-US": "Divide" + "en-US": "Divide", + "zh-CN": "除" }, "id": "divide" }, { "aliases": { - "en-US": "Modulo" + "en-US": "Modulo", + "zh-CN": "余数" }, "id": "modulo" }, { "aliases": { - "en-US": "Raise To Power" + "en-US": "Raise To Power", + "zh-CN": "乘方" }, "id": "raiseToPower" }, { "aliases": { - "en-US": "Append To Array" + "en-US": "Append To Array", + "zh-CN": "添加至数组" }, "id": "appendToArray" }, { "aliases": { - "en-US": "Remove From Array" + "en-US": "Remove From Array", + "zh-CN": "从数组中移除" }, "id": "removeFromArray" } @@ -2091,73 +2330,84 @@ "generatorVersion": "0.1.0", "license": "MIT (WrightKit-authored data, ownership transfer to workshop-rs per wright#136 direction; see docs/provenance.md)", "reviewed": true, - "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the OverPy Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned OverPy 9.7.10 en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned OverPy 9.7.10 enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the pinned OverPy 9.7.10 en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the pinned OverPy 9.7.10 en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; migrated to workshop-rs from the Wright-authored wright-workshop catalog (crates/wright-workshop/src/catalog/data/catalog.json) on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains (chaseOverTime arg 3 ChaseTimeReeval, chaseAtRate arg 3 ChaseRateReeval, chasePlayerVariableOverTime arg 4 ChaseTimeReeval, chasePlayerVariableAtRate arg 4 ChaseRateReeval) migrate the Wright-authored OPY semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN declared as a locale with an empty mapping set (0/344) pending a reviewed, MIT-permissible reference source per ADR-0001 Decision 6; no zh-CN compatibility claim is made and conversion into zh-CN fails explicitly until evidence-backed aliases are added" + "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the OverPy Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned OverPy 9.7.10 en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned OverPy 9.7.10 enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the pinned OverPy 9.7.10 en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the pinned OverPy 9.7.10 en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; migrated to workshop-rs from the Wright-authored wright-workshop catalog (crates/wright-workshop/src/catalog/data/catalog.json) on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains (chaseOverTime arg 3 ChaseTimeReeval, chaseAtRate arg 3 ChaseRateReeval, chasePlayerVariableOverTime arg 4 ChaseTimeReeval, chasePlayerVariableAtRate arg 4 ChaseRateReeval) migrate the Wright-authored OPY semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data export at commit d854bf01fc7bbf3b2169f67408c07a8da8989ad6 (commit date 2026-08-12, fetched 2026-08-17) by exact en-US spelling match; 327/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" }, "schemaVersion": 1, "structural": [ { "aliases": { - "en-US": "If" + "en-US": "If", + "zh-CN": "If" }, "id": "if" }, { "aliases": { - "en-US": "Else If" + "en-US": "Else If", + "zh-CN": "Else If" }, "id": "elseIf" }, { "aliases": { - "en-US": "Else" + "en-US": "Else", + "zh-CN": "Else" }, "id": "else" }, { "aliases": { - "en-US": "End" + "en-US": "End", + "zh-CN": "End" }, "id": "end" }, { "aliases": { - "en-US": "For Global Variable" + "en-US": "For Global Variable", + "zh-CN": "For 全局变量" }, "id": "forGlobalVariable" }, { "aliases": { - "en-US": "While" + "en-US": "While", + "zh-CN": "While" }, "id": "while" }, { "aliases": { - "en-US": "Set Global Variable" + "en-US": "Set Global Variable", + "zh-CN": "设置全局变量" }, "id": "setGlobalVariable" }, { "aliases": { - "en-US": "Modify Global Variable" + "en-US": "Modify Global Variable", + "zh-CN": "修改全局变量" }, "id": "modifyGlobalVariable" }, { "aliases": { - "en-US": "Set Player Variable" + "en-US": "Set Player Variable", + "zh-CN": "设置玩家变量" }, "id": "setPlayerVariable" }, { "aliases": { - "en-US": "Modify Player Variable" + "en-US": "Modify Player Variable", + "zh-CN": "修改玩家变量" }, "id": "modifyPlayerVariable" }, { "aliases": { - "en-US": "Call Subroutine" + "en-US": "Call Subroutine", + "zh-CN": "调用子程序" }, "id": "callSubroutine" } @@ -2170,7 +2420,8 @@ "values": [ { "aliases": { - "en-US": "Add" + "en-US": "Add", + "zh-CN": "加" }, "id": "add", "params": [ @@ -2180,7 +2431,8 @@ }, { "aliases": { - "en-US": "Subtract" + "en-US": "Subtract", + "zh-CN": "减" }, "id": "subtract", "params": [ @@ -2190,7 +2442,8 @@ }, { "aliases": { - "en-US": "Multiply" + "en-US": "Multiply", + "zh-CN": "乘" }, "id": "multiply", "params": [ @@ -2200,7 +2453,8 @@ }, { "aliases": { - "en-US": "Divide" + "en-US": "Divide", + "zh-CN": "除" }, "id": "divide", "params": [ @@ -2210,7 +2464,8 @@ }, { "aliases": { - "en-US": "Compare" + "en-US": "Compare", + "zh-CN": "比较" }, "id": "compare", "params": [ @@ -2221,25 +2476,29 @@ }, { "aliases": { - "en-US": "And" + "en-US": "And", + "zh-CN": "与" }, "id": "and" }, { "aliases": { - "en-US": "Or" + "en-US": "Or", + "zh-CN": "或" }, "id": "or" }, { "aliases": { - "en-US": "Not" + "en-US": "Not", + "zh-CN": "非" }, "id": "not" }, { "aliases": { - "en-US": "Count Of" + "en-US": "Count Of", + "zh-CN": "数量" }, "id": "countOf", "params": [ @@ -2248,19 +2507,22 @@ }, { "aliases": { - "en-US": "Absolute Value" + "en-US": "Absolute Value", + "zh-CN": "绝对值" }, "id": "absoluteValue" }, { "aliases": { - "en-US": "Array" + "en-US": "Array", + "zh-CN": "数组" }, "id": "array" }, { "aliases": { - "en-US": "Vector" + "en-US": "Vector", + "zh-CN": "矢量" }, "id": "vector", "params": [ @@ -2271,13 +2533,15 @@ }, { "aliases": { - "en-US": "Custom String" + "en-US": "Custom String", + "zh-CN": "自定义字符串" }, "id": "customString" }, { "aliases": { - "en-US": "Value In Array" + "en-US": "Value In Array", + "zh-CN": "数组中的值" }, "id": "valueInArray", "params": [ @@ -2287,7 +2551,8 @@ }, { "aliases": { - "en-US": "Mapped Array" + "en-US": "Mapped Array", + "zh-CN": "映射的数组" }, "id": "mappedArray", "params": [ @@ -2297,31 +2562,36 @@ }, { "aliases": { - "en-US": "First Of" + "en-US": "First Of", + "zh-CN": "首个" }, "id": "firstOf" }, { "aliases": { - "en-US": "String Replace" + "en-US": "String Replace", + "zh-CN": "字符串替换" }, "id": "stringReplace" }, { "aliases": { - "en-US": "String Slice" + "en-US": "String Slice", + "zh-CN": "截取字符串" }, "id": "stringSlice" }, { "aliases": { - "en-US": "String Split" + "en-US": "String Split", + "zh-CN": "字符串分割" }, "id": "stringSplit" }, { "aliases": { - "en-US": "All Players" + "en-US": "All Players", + "zh-CN": "所有玩家" }, "id": "allPlayers", "paramDefaults": [ @@ -2333,13 +2603,15 @@ }, { "aliases": { - "en-US": "Random Real" + "en-US": "Random Real", + "zh-CN": "随机实数" }, "id": "randomReal" }, { "aliases": { - "en-US": "Random Value In Array" + "en-US": "Random Value In Array", + "zh-CN": "数组随机取值" }, "id": "randomValueInArray", "params": [ @@ -2348,7 +2620,8 @@ }, { "aliases": { - "en-US": "Has Spawned" + "en-US": "Has Spawned", + "zh-CN": "已重生" }, "id": "hasSpawned", "params": [ @@ -2357,26 +2630,30 @@ }, { "aliases": { - "en-US": "Empty Array" + "en-US": "Empty Array", + "zh-CN": "空数组" }, "id": "emptyArray", "params": [] }, { "aliases": { - "en-US": "If-Then-Else" + "en-US": "If-Then-Else", + "zh-CN": "If-Then-Else" }, "id": "ifThenElse" }, { "aliases": { - "en-US": "Current Array Element" + "en-US": "Current Array Element", + "zh-CN": "当前数组元素" }, "id": "currentArrayElement" }, { "aliases": { - "en-US": "Append To Array" + "en-US": "Append To Array", + "zh-CN": "添加至数组" }, "id": "appendToArray", "params": [ @@ -2386,7 +2663,8 @@ }, { "aliases": { - "en-US": "Square Root" + "en-US": "Square Root", + "zh-CN": "平方根" }, "id": "squareRoot", "params": [ @@ -2395,7 +2673,8 @@ }, { "aliases": { - "en-US": "Health" + "en-US": "Health", + "zh-CN": "生命值" }, "id": "getHealth", "params": [ @@ -2404,7 +2683,8 @@ }, { "aliases": { - "en-US": "Position Of" + "en-US": "Position Of", + "zh-CN": "所选位置" }, "id": "getPosition", "params": [ @@ -2413,7 +2693,8 @@ }, { "aliases": { - "en-US": "Is Alive" + "en-US": "Is Alive", + "zh-CN": "存活" }, "id": "isAlive", "params": [ @@ -2422,7 +2703,8 @@ }, { "aliases": { - "en-US": "Throttle Of" + "en-US": "Throttle Of", + "zh-CN": "阈值" }, "id": "getThrottle", "params": [ @@ -2431,7 +2713,8 @@ }, { "aliases": { - "en-US": "Players Within Radius" + "en-US": "Players Within Radius", + "zh-CN": "范围内玩家" }, "id": "getPlayersInRadius", "params": [ @@ -2443,13 +2726,15 @@ }, { "aliases": { - "en-US": "Is Game In Progress" + "en-US": "Is Game In Progress", + "zh-CN": "游戏正在进行中" }, "id": "isGameInProgress" }, { "aliases": { - "en-US": "World Vector Of" + "en-US": "World Vector Of", + "zh-CN": "地图矢量" }, "id": "worldVector", "params": [ @@ -2460,7 +2745,8 @@ }, { "aliases": { - "en-US": "Workshop Setting Integer" + "en-US": "Workshop Setting Integer", + "zh-CN": "地图工坊设置整数" }, "id": "workshopSettingInteger", "params": [ @@ -2474,7 +2760,8 @@ }, { "aliases": { - "en-US": "Workshop Setting Toggle" + "en-US": "Workshop Setting Toggle", + "zh-CN": "地图工坊设置开关" }, "id": "workshopSettingToggle", "params": [ @@ -2486,7 +2773,8 @@ }, { "aliases": { - "en-US": "Workshop Setting Combo" + "en-US": "Workshop Setting Combo", + "zh-CN": "地图工坊设置组合" }, "id": "workshopSettingCombo", "params": [ @@ -2499,35 +2787,40 @@ }, { "aliases": { - "en-US": "All Heroes" + "en-US": "All Heroes", + "zh-CN": "全部英雄" }, "id": "allHeroes", "params": [] }, { "aliases": { - "en-US": "All Tank Heroes" + "en-US": "All Tank Heroes", + "zh-CN": "所有重装英雄" }, "id": "allTankHeroes", "params": [] }, { "aliases": { - "en-US": "All Damage Heroes" + "en-US": "All Damage Heroes", + "zh-CN": "所有输出英雄" }, "id": "allDamageHeroes", "params": [] }, { "aliases": { - "en-US": "All Support Heroes" + "en-US": "All Support Heroes", + "zh-CN": "所有支援英雄" }, "id": "allSupportHeroes", "params": [] }, { "aliases": { - "en-US": "Allowed Heroes" + "en-US": "Allowed Heroes", + "zh-CN": "可用英雄" }, "id": "allowedHeroes", "params": [ @@ -2536,21 +2829,24 @@ }, { "aliases": { - "en-US": "Event Player" + "en-US": "Event Player", + "zh-CN": "事件玩家" }, "id": "eventPlayer", "params": [] }, { "aliases": { - "en-US": "Local Player" + "en-US": "Local Player", + "zh-CN": "本地玩家" }, "id": "localPlayer", "params": [] }, { "aliases": { - "en-US": "Team Of" + "en-US": "Team Of", + "zh-CN": "所在队伍" }, "id": "teamOf", "params": [ @@ -2559,7 +2855,8 @@ }, { "aliases": { - "en-US": "Opposite Team Of" + "en-US": "Opposite Team Of", + "zh-CN": "对方队伍" }, "id": "oppositeTeamOf", "params": [ @@ -2568,7 +2865,8 @@ }, { "aliases": { - "en-US": "Number Of Players" + "en-US": "Number Of Players", + "zh-CN": "玩家数量" }, "id": "numberOfPlayers", "params": [ @@ -2577,7 +2875,8 @@ }, { "aliases": { - "en-US": "Array Contains" + "en-US": "Array Contains", + "zh-CN": "数组包含" }, "id": "arrayContains", "params": [ @@ -2594,14 +2893,16 @@ }, { "aliases": { - "en-US": "Current Array Index" + "en-US": "Current Array Index", + "zh-CN": "当前数组索引" }, "id": "currentArrayIndex", "params": [] }, { "aliases": { - "en-US": "Index Of Array Value" + "en-US": "Index Of Array Value", + "zh-CN": "数组值的索引" }, "id": "indexOfArrayValue", "params": [ @@ -2611,7 +2912,8 @@ }, { "aliases": { - "en-US": "Filtered Array" + "en-US": "Filtered Array", + "zh-CN": "已过滤的数组" }, "id": "filteredArray", "params": [ @@ -2621,7 +2923,8 @@ }, { "aliases": { - "en-US": "Sorted Array" + "en-US": "Sorted Array", + "zh-CN": "已排序的数组" }, "id": "sortedArray", "params": [ @@ -2631,7 +2934,8 @@ }, { "aliases": { - "en-US": "Last Of" + "en-US": "Last Of", + "zh-CN": "最后" }, "id": "lastOf", "params": [ @@ -2640,7 +2944,8 @@ }, { "aliases": { - "en-US": "Remove From Array" + "en-US": "Remove From Array", + "zh-CN": "从数组中移除" }, "id": "removeFromArray", "params": [ @@ -2650,7 +2955,8 @@ }, { "aliases": { - "en-US": "Max" + "en-US": "Max", + "zh-CN": "较大" }, "id": "max", "params": [ @@ -2660,7 +2966,8 @@ }, { "aliases": { - "en-US": "Min" + "en-US": "Min", + "zh-CN": "较小" }, "id": "min", "params": [ @@ -2670,7 +2977,8 @@ }, { "aliases": { - "en-US": "Round To Integer" + "en-US": "Round To Integer", + "zh-CN": "取整" }, "id": "roundToInteger", "paramDomains": [ @@ -2684,7 +2992,8 @@ }, { "aliases": { - "en-US": "Cross Product" + "en-US": "Cross Product", + "zh-CN": "矢量积" }, "id": "crossProduct", "params": [ @@ -2694,7 +3003,8 @@ }, { "aliases": { - "en-US": "Direction From Angles" + "en-US": "Direction From Angles", + "zh-CN": "与此角度的相对方向" }, "id": "directionFromAngles", "params": [ @@ -2704,7 +3014,8 @@ }, { "aliases": { - "en-US": "Horizontal Angle From Direction" + "en-US": "Horizontal Angle From Direction", + "zh-CN": "与此方向的水平角度" }, "id": "horizontalAngleFromDirection", "params": [ @@ -2713,7 +3024,8 @@ }, { "aliases": { - "en-US": "Vertical Angle From Direction" + "en-US": "Vertical Angle From Direction", + "zh-CN": "与此方向的垂直角度" }, "id": "verticalAngleFromDirection", "params": [ @@ -2722,14 +3034,16 @@ }, { "aliases": { - "en-US": "Forward" + "en-US": "Forward", + "zh-CN": "前" }, "id": "forward", "params": [] }, { "aliases": { - "en-US": "Custom Color" + "en-US": "Custom Color", + "zh-CN": "自定义颜色" }, "id": "customColor", "params": [ @@ -2741,7 +3055,8 @@ }, { "aliases": { - "en-US": "Is Button Held" + "en-US": "Is Button Held", + "zh-CN": "按钮被按下" }, "id": "isButtonHeld", "paramDefaults": [ @@ -2759,7 +3074,8 @@ }, { "aliases": { - "en-US": "Is In Spawn Room" + "en-US": "Is In Spawn Room", + "zh-CN": "在重生室中" }, "id": "isInSpawnRoom", "params": [ @@ -2768,7 +3084,8 @@ }, { "aliases": { - "en-US": "Is True For All" + "en-US": "Is True For All", + "zh-CN": "对全部为“真”" }, "id": "isTrueForAll", "params": [ @@ -2778,21 +3095,24 @@ }, { "aliases": { - "en-US": "Is Waiting For Players" + "en-US": "Is Waiting For Players", + "zh-CN": "正在等待玩家" }, "id": "isWaitingForPlayers", "params": [] }, { "aliases": { - "en-US": "Current Map" + "en-US": "Current Map", + "zh-CN": "当前地图" }, "id": "currentMap", "params": [] }, { "aliases": { - "en-US": "Evaluate Once" + "en-US": "Evaluate Once", + "zh-CN": "单次赋值" }, "id": "evaluateOnce", "params": [ @@ -2801,7 +3121,8 @@ }, { "aliases": { - "en-US": "Update Every Frame" + "en-US": "Update Every Frame", + "zh-CN": "逐帧更新" }, "id": "updateEveryFrame", "params": [ @@ -2810,21 +3131,24 @@ }, { "aliases": { - "en-US": "Last Created Entity" + "en-US": "Last Created Entity", + "zh-CN": "最后创建的实体" }, "id": "lastCreatedEntity", "params": [] }, { "aliases": { - "en-US": "Last Text ID" + "en-US": "Last Text ID", + "zh-CN": "上一个文本ID" }, "id": "lastTextId", "params": [] }, { "aliases": { - "en-US": "Hero Icon String" + "en-US": "Hero Icon String", + "zh-CN": "英雄图标字符串" }, "id": "heroIconString", "paramDomains": [ @@ -2836,7 +3160,8 @@ }, { "aliases": { - "en-US": "Ability Icon String" + "en-US": "Ability Icon String", + "zh-CN": "技能图标字符串" }, "id": "abilityIconString", "paramDomains": [ @@ -2850,7 +3175,8 @@ }, { "aliases": { - "en-US": "Icon String" + "en-US": "Icon String", + "zh-CN": "图标字符串" }, "id": "iconString", "paramDomains": [ @@ -2862,7 +3188,8 @@ }, { "aliases": { - "en-US": "Input Binding String" + "en-US": "Input Binding String", + "zh-CN": "输入绑定字符串" }, "id": "inputBindingString", "paramDomains": [ diff --git a/crates/workshop-rs/src/emitter.rs b/crates/workshop-rs/src/emitter.rs index 1780800..43dbee4 100644 --- a/crates/workshop-rs/src/emitter.rs +++ b/crates/workshop-rs/src/emitter.rs @@ -10,9 +10,8 @@ //! presentation-canonical, so the same WIR/config emits byte-stable text that //! reparses to equivalent WIR — except for the `settings` section: //! settings-bearing emissions are deliberately rejected by the Workshop -//! parser (a `.ws` decompiler is a non-goal). The settings emission table is -//! en-US fixture-evidenced data; emitting settings for another locale -//! requires an explicit fallback to `en-US`. +//! parser (a `.ws` decompiler is a non-goal). Settings names are resolved from +//! the generated locale corpus, with an explicit `en-US` fallback when needed. use std::fmt::Write; @@ -142,24 +141,6 @@ impl Emitter<'_> { /// carrier, table-driven (fixture-evidenced names). Only runs on /// validated programs, so unknown keys cannot reach this point. fn emit_settings(&mut self, settings: &SettingsTree) -> Result<()> { - // The settings emission table is en-US fixture-evidenced data; a - // target locale without settings spellings fails explicitly unless a - // fallback to en-US is opted into (missing mapping, ADR-0001). - let en_us = Locale::new("en-US"); - if self.locale != en_us { - match &self.fallback { - Some(fallback) if *fallback == en_us => { - self.fallback_ids.push("settings".to_string()); - } - _ => { - return Err(WorkshopError::MissingMapping { - kind: "setting", - id: "settings emission table".to_string(), - locale: self.locale.clone(), - }); - } - } - } self.line(0, "settings {")?; for child in &settings.children { let SettingsNode::Group { name, children, .. } = child else { @@ -193,8 +174,9 @@ impl Emitter<'_> { let SettingsNode::Group { name, children, .. } = mode else { return Err(self.malformed("mode entries must be groups")); }; - let display = table::mode_name(name) + let english = table::mode_name(name) .ok_or_else(|| self.malformed(format!("unknown game mode '{name}'")))?; + let display = self.setting_name("modes", english, &format!("mode.{name}"))?; // `enabled: false` prefixes the mode header; true renders with no // prefix (only false is evidenced in the corpus, #86). let disabled = children.iter().any(|member| { @@ -206,7 +188,7 @@ impl Emitter<'_> { let header = if disabled { format!("disabled {display}") } else { - display.to_string() + display }; self.line(2, &format!("{header} {{"))?; for member in children { @@ -232,14 +214,16 @@ impl Emitter<'_> { let SettingsNode::Group { name, children, .. } = team else { return Err(self.malformed("team entries must be groups")); }; - let display = table::team_name(name) + let english = table::team_name(name) .ok_or_else(|| self.malformed(format!("unknown team '{name}'")))?; + let display = self.setting_name("teams", english, &format!("team.{name}"))?; self.line(2, &format!("{display} {{"))?; for member in children { match member { SettingsNode::Group { name, children, .. } => { - let hero = table::hero_name(name) + let english = table::hero_name(name) .ok_or_else(|| self.malformed(format!("unknown hero '{name}'")))?; + let hero = self.setting_name("heroes", english, &format!("hero.{name}"))?; self.line(3, &format!("{hero} {{"))?; for inner in children { self.settings_member( @@ -277,62 +261,70 @@ impl Emitter<'_> { table::path_string(&full) )) })?; + let display_name = + self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?; match (node, &entry.kind) { (SettingsNode::String { value, .. }, KeyKind::String) => { self.line( level, - &format!( - "{}: \"{}\"", - entry.workshop_name, - escape_settings_string(value) - ), + &format!("{}: \"{}\"", display_name, escape_settings_string(value)), )?; } (SettingsNode::String { value, .. }, KeyKind::Enum(domain)) => { - let display = table::enum_name(domain, value).ok_or_else(|| { + let english = table::enum_name(domain, value).ok_or_else(|| { self.malformed(format!("unknown value '{value}' for settings key '{name}'")) })?; - self.line(level, &format!("{}: {display}", entry.workshop_name))?; + let display = + self.setting_name("enums", english, &format!("enum.{domain}.{value}"))?; + self.line(level, &format!("{display_name}: {display}"))?; } (SettingsNode::Number { value, .. }, KeyKind::Number) => { - self.line( - level, - &format!("{}: {}", entry.workshop_name, format_number(*value)), - )?; + self.line(level, &format!("{display_name}: {}", format_number(*value)))?; } (SettingsNode::Number { value, .. }, KeyKind::Percent) => { self.line( level, - &format!("{}: {}%", entry.workshop_name, format_number(*value)), + &format!("{display_name}: {}%", format_number(*value)), )?; } (SettingsNode::Bool { value, .. }, KeyKind::Bool) => { - let rendered = if *value { "On" } else { "Off" }; - self.line(level, &format!("{}: {rendered}", entry.workshop_name))?; + let rendered = self.setting_name( + "tokens", + if *value { "On" } else { "Off" }, + if *value { "token.on" } else { "token.off" }, + )?; + self.line(level, &format!("{display_name}: {rendered}"))?; } (SettingsNode::List { elements, .. }, KeyKind::ListMap) => { - self.line(level, &format!("{} {{", entry.workshop_name))?; + self.line(level, &format!("{display_name} {{"))?; for element in elements { - let display = table::map_name(&element.value).ok_or_else(|| { + let english = table::map_name(&element.value).ok_or_else(|| { self.malformed(format!( "unknown map '{}' in settings list '{name}'", element.value )) })?; - self.line(level + 1, display)?; + let display = + self.setting_name("maps", english, &format!("map.{}.name", element.value))?; + self.line(level + 1, &display)?; } self.line(level, "}")?; } (SettingsNode::List { elements, .. }, KeyKind::ListHero) => { - self.line(level, &format!("{} {{", entry.workshop_name))?; + self.line(level, &format!("{display_name} {{"))?; for element in elements { - let display = table::hero_name(&element.value).ok_or_else(|| { + let english = table::hero_name(&element.value).ok_or_else(|| { self.malformed(format!( "unknown hero '{}' in settings list '{name}'", element.value )) })?; - self.line(level + 1, display)?; + let display = self.setting_name( + "heroes", + english, + &format!("hero.{}.name", element.value), + )?; + self.line(level + 1, &display)?; } self.line(level, "}")?; } @@ -345,6 +337,32 @@ impl Emitter<'_> { Ok(()) } + /// Resolve a settings spelling from the generated locale corpus. The + /// English table remains the explicit fallback only when the caller opts + /// into `en-US`, matching the catalog's missing-mapping contract. + fn setting_name(&mut self, section: &str, english: &str, id: &str) -> Result { + let en_us = Locale::new("en-US"); + if self.locale == en_us { + return Ok(english.to_string()); + } + if let Some(spelling) = table::localized_name(self.locale.as_str(), section, english) { + return Ok(spelling.to_string()); + } + if let Some(fallback) = &self.fallback { + if *fallback == en_us { + if !self.fallback_ids.iter().any(|value| value == "settings") { + self.fallback_ids.push("settings".to_string()); + } + return Ok(english.to_string()); + } + } + Err(WorkshopError::MissingMapping { + kind: "setting", + id: id.to_string(), + locale: self.locale.clone(), + }) + } + fn malformed(&self, message: impl Into) -> WorkshopError { WorkshopError::Malformed { message: message.into(), @@ -435,10 +453,8 @@ impl Emitter<'_> { let name = self.global_name(*variable)?; let mut value_text = String::new(); self.value(*value, &mut value_text)?; - self.line( - level, - &format!("Set Global Variable({name}, {value_text});"), - )?; + let keyword = self.spelling(Kind::Structural, "setGlobalVariable")?; + self.line(level, &format!("{keyword}({name}, {value_text});"))?; } wir::Action::ModifyGlobalVariable { variable, @@ -450,10 +466,8 @@ impl Emitter<'_> { let op = self.modify_op_spelling(*op)?; let mut value_text = String::new(); self.value(*value, &mut value_text)?; - self.line( - level, - &format!("Modify Global Variable({name}, {op}, {value_text});"), - )?; + let keyword = self.spelling(Kind::Structural, "modifyGlobalVariable")?; + self.line(level, &format!("{keyword}({name}, {op}, {value_text});"))?; } wir::Action::SetPlayerVariable { player, @@ -466,9 +480,10 @@ impl Emitter<'_> { let name = self.player_name(*variable)?; let mut value_text = String::new(); self.value(*value, &mut value_text)?; + let keyword = self.spelling(Kind::Structural, "setPlayerVariable")?; self.line( level, - &format!("Set Player Variable({player_text}, {name}, {value_text});"), + &format!("{keyword}({player_text}, {name}, {value_text});"), )?; } wir::Action::ModifyPlayerVariable { @@ -484,9 +499,10 @@ impl Emitter<'_> { let op = self.modify_op_spelling(*op)?; let mut value_text = String::new(); self.value(*value, &mut value_text)?; + let keyword = self.spelling(Kind::Structural, "modifyPlayerVariable")?; self.line( level, - &format!("Modify Player Variable({player_text}, {name}, {op}, {value_text});"), + &format!("{keyword}({player_text}, {name}, {op}, {value_text});"), )?; } wir::Action::CallSubroutine { subroutine, .. } => { @@ -501,7 +517,8 @@ impl Emitter<'_> { locale: self.locale.clone(), span: None, })?; - self.line(level, &format!("Call Subroutine({name});"))?; + let keyword = self.spelling(Kind::Structural, "callSubroutine")?; + self.line(level, &format!("{keyword}({name});"))?; } wir::Action::If { branches, @@ -511,14 +528,16 @@ impl Emitter<'_> { for (index, branch) in branches.iter().enumerate() { let mut condition = String::new(); self.value(branch.condition, &mut condition)?; - let keyword = if index == 0 { "If" } else { "Else If" }; + let keyword = + self.spelling(Kind::Structural, if index == 0 { "if" } else { "elseIf" })?; self.line(level, &format!("{keyword}({condition});"))?; for action in &branch.body { self.action(*action, level + 1, false)?; } } if let Some(else_body) = else_body { - self.line(level, "Else;")?; + let keyword = self.spelling(Kind::Structural, "else")?; + self.line(level, &format!("{keyword};"))?; for action in else_body { self.action(*action, level + 1, false)?; } @@ -526,7 +545,8 @@ impl Emitter<'_> { // A rule-final if closes the rule without `End;` (oracle // spelling); nested and middle-of-rule ifs keep it. if !rule_final { - self.line(level, "End;")?; + let keyword = self.spelling(Kind::Structural, "end")?; + self.line(level, &format!("{keyword};"))?; } } wir::Action::While { @@ -534,11 +554,13 @@ impl Emitter<'_> { } => { let mut text = String::new(); self.value(*condition, &mut text)?; - self.line(level, &format!("While({text});"))?; + let keyword = self.spelling(Kind::Structural, "while")?; + self.line(level, &format!("{keyword}({text});"))?; for action in body { self.action(*action, level + 1, false)?; } - self.line(level, "End;")?; + let end = self.spelling(Kind::Structural, "end")?; + self.line(level, &format!("{end};"))?; } wir::Action::ForGlobalVariable { variable, @@ -555,16 +577,16 @@ impl Emitter<'_> { self.value(*start, &mut start_text)?; self.value(*stop, &mut stop_text)?; self.value(*step, &mut step_text)?; + let keyword = self.spelling(Kind::Structural, "forGlobalVariable")?; self.line( level, - &format!( - "For Global Variable({name}, {start_text}, {stop_text}, {step_text});" - ), + &format!("{keyword}({name}, {start_text}, {stop_text}, {step_text});"), )?; for action in body { self.action(*action, level + 1, false)?; } - self.line(level, "End;")?; + let end = self.spelling(Kind::Structural, "end")?; + self.line(level, &format!("{end};"))?; } wir::Action::ForPlayerVariable { player, @@ -593,7 +615,8 @@ impl Emitter<'_> { for action in body { self.action(*action, level + 1, false)?; } - self.line(level, "End;")?; + let end = self.spelling(Kind::Structural, "end")?; + self.line(level, &format!("{end};"))?; } wir::Action::Debug { value, .. } => { // `debug(value)` displays the value as HUD text. The diff --git a/crates/workshop-rs/src/settings/data/zh-cn.json b/crates/workshop-rs/src/settings/data/zh-cn.json new file mode 100644 index 0000000..a0714eb --- /dev/null +++ b/crates/workshop-rs/src/settings/data/zh-cn.json @@ -0,0 +1,373 @@ +{ + "coverage": { + "enums": { + "matched": 2, + "total": 2 + }, + "heroes": { + "matched": 10, + "total": 10 + }, + "labels": { + "matched": 17, + "total": 19 + }, + "maps": { + "matched": 2, + "total": 2 + }, + "modes": { + "matched": 6, + "total": 7 + }, + "teams": { + "matched": 0, + "total": 1 + }, + "tokens": { + "matched": 3, + "total": 3 + } + }, + "enums": { + "2 Of Each Role Per Team": { + "en-US": "2 Of Each Role Per Team", + "sources": [ + "customGameSettings.gamemodes.values.general.values.roleLimit.values.2OfEachRolePerTeam" + ], + "zh-CN": "每队同一职责最多2名" + }, + "Off": { + "en-US": "Off", + "sources": [ + "other.customGameSettings.__off__" + ], + "zh-CN": "关闭" + } + }, + "excluded": [ + { + "en-US": "Ultimate Generation - Passive Blizzard", + "reason": "no exact en-US match in the export", + "surface": "heroes...passiveUltGen%" + }, + { + "en-US": "Ultimate Generation - Combat Blizzard", + "reason": "no exact en-US match in the export", + "surface": "heroes...combatUltGen%" + }, + { + "en-US": "General", + "reason": "no exact en-US match in the export", + "surface": "mode.general.name" + }, + { + "en-US": "General", + "reason": "no exact en-US match in the export", + "surface": "team.allTeams.name" + } + ], + "heroes": { + "Ashe": { + "en-US": "Ashe", + "sources": [ + "heroes.ashe" + ], + "zh-CN": "艾什" + }, + "Bastion": { + "en-US": "Bastion", + "sources": [ + "heroes.bastion" + ], + "zh-CN": "堡垒" + }, + "D.Va": { + "en-US": "D.Va", + "sources": [ + "heroes.dva" + ], + "zh-CN": "D.Va" + }, + "Doomfist": { + "en-US": "Doomfist", + "sources": [ + "heroes.doomfist" + ], + "zh-CN": "末日铁拳" + }, + "Echo": { + "en-US": "Echo", + "sources": [ + "heroes.echo" + ], + "zh-CN": "回声" + }, + "Mei": { + "en-US": "Mei", + "sources": [ + "heroes.mei" + ], + "zh-CN": "美" + }, + "Moira": { + "en-US": "Moira", + "sources": [ + "heroes.moira" + ], + "zh-CN": "莫伊拉" + }, + "Reinhardt": { + "en-US": "Reinhardt", + "sources": [ + "heroes.reinhardt" + ], + "zh-CN": "莱因哈特" + }, + "Wrecking Ball": { + "en-US": "Wrecking Ball", + "sources": [ + "heroes.wreckingBall" + ], + "zh-CN": "破坏球" + }, + "Zenyatta": { + "en-US": "Zenyatta", + "sources": [ + "heroes.zenyatta" + ], + "zh-CN": "禅雅塔" + } + }, + "labels": { + "Allow Hero Switching": { + "en-US": "Allow Hero Switching", + "sources": [ + "customGameSettings.gamemodes.values.general.values.enableHeroSwitching" + ], + "zh-CN": "允许切换英雄" + }, + "Competitive Rules": { + "en-US": "Competitive Rules", + "sources": [ + "customGameSettings.gamemodes.values.assault.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.clash.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.control.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.controlAprilFools.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.escort.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.escortAprilFools.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.flashpoint.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.flashpointAprilFools.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.hybrid.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.hybridAprilFools.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.push.values.enableCompetitiveRules", + "customGameSettings.gamemodes.values.pushAprilFools.values.enableCompetitiveRules" + ], + "zh-CN": "竞技比赛规则" + }, + "Cryo-Freeze": { + "en-US": "Cryo-Freeze", + "sources": [ + "heroes.mei.ability1" + ], + "zh-CN": "急冻" + }, + "Description": { + "en-US": "Description", + "sources": [ + "customGameSettings.main.values.description" + ], + "zh-CN": "描述" + }, + "Health": { + "en-US": "Health", + "sources": [ + "constants.Health.NORMAL", + "customGameSettings.heroes.values.__generalAndEachHero__.health%" + ], + "zh-CN": "生命值" + }, + "Hero Limit": { + "en-US": "Hero Limit", + "sources": [ + "customGameSettings.gamemodes.values.general.values.heroLimit" + ], + "zh-CN": "英雄限制" + }, + "Ice Wall": { + "en-US": "Ice Wall", + "sources": [ + "heroes.mei.ability2" + ], + "zh-CN": "冰墙" + }, + "Limit Roles": { + "en-US": "Limit Roles", + "sources": [ + "customGameSettings.gamemodes.values.general.values.roleLimit" + ], + "zh-CN": "职责限制" + }, + "Max FFA Players": { + "en-US": "Max FFA Players", + "sources": [ + "customGameSettings.lobby.values.ffaSlots" + ], + "zh-CN": "自由混战人数上限" + }, + "Mode Name": { + "en-US": "Mode Name", + "sources": [ + "customGameSettings.main.values.modeName" + ], + "zh-CN": "模式名称" + }, + "Primary Fire": { + "en-US": "Primary Fire", + "sources": [ + "constants.ButtonLiteral.PRIMARY_FIRE", + "customGameSettings.heroes.values.__generalAndEachHero__.enablePrimaryFire" + ], + "zh-CN": "主要攻击模式" + }, + "Respawn As Random Hero": { + "en-US": "Respawn As Random Hero", + "sources": [ + "customGameSettings.gamemodes.values.general.values.enableRandomHeroes" + ], + "zh-CN": "随机英雄复生" + }, + "Respawn Time Scalar": { + "en-US": "Respawn Time Scalar", + "sources": [ + "customGameSettings.gamemodes.values.general.values.respawnTime%" + ], + "zh-CN": "复生时间" + }, + "Secondary Fire": { + "en-US": "Secondary Fire", + "sources": [ + "constants.ButtonLiteral.SECONDARY_FIRE", + "customGameSettings.heroes.values.__eachHero__.enableGenericSecondaryFire" + ], + "zh-CN": "辅助攻击模式" + }, + "disabled heroes": { + "en-US": "disabled heroes", + "sources": [ + "customGameSettings.heroes.values.disabledHeroes" + ], + "zh-CN": "禁用英雄" + }, + "enabled heroes": { + "en-US": "enabled heroes", + "sources": [ + "customGameSettings.heroes.values.enabledHeroes" + ], + "zh-CN": "启用英雄" + }, + "enabled maps": { + "en-US": "enabled maps", + "sources": [ + "customGameSettings.gamemodes.values.general.values.enabledMaps" + ], + "zh-CN": "启用地图" + } + }, + "locale": "zh-CN", + "maps": { + "King's Row Winter": { + "en-US": "King's Row Winter", + "sources": [ + "maps.kingsRowWinter" + ], + "zh-CN": "圣诞节国王大道" + }, + "Workshop Island": { + "en-US": "Workshop Island", + "sources": [ + "maps.workshopIsland" + ], + "zh-CN": "地图工坊岛屿" + } + }, + "modes": { + "Assault": { + "en-US": "Assault", + "sources": [ + "gamemodes.assault" + ], + "zh-CN": "攻防作战" + }, + "Control": { + "en-US": "Control", + "sources": [ + "gamemodes.control" + ], + "zh-CN": "占领要点" + }, + "Deathmatch": { + "en-US": "Deathmatch", + "sources": [ + "gamemodes.ffa" + ], + "zh-CN": "死斗" + }, + "Escort": { + "en-US": "Escort", + "sources": [ + "gamemodes.escort" + ], + "zh-CN": "运载目标" + }, + "Hybrid": { + "en-US": "Hybrid", + "sources": [ + "gamemodes.hybrid" + ], + "zh-CN": "攻击护送" + }, + "Skirmish": { + "en-US": "Skirmish", + "sources": [ + "gamemodes.skirmish" + ], + "zh-CN": "突击模式" + } + }, + "provenance": { + "commit": "d854bf01fc7bbf3b2169f67408c07a8da8989ad6", + "commitDate": "2026-08-12T15:26:38Z", + "fetchedAt": "2026-08-17T02:52:59Z", + "generator": "workshop-catalog-gen corpus", + "generatorVersion": "0.1.0", + "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", + "source": "user-provided workshop-data export (workshop-data.json)" + }, + "schemaVersion": 1, + "teams": {}, + "tokens": { + "Off": { + "en-US": "Off", + "sources": [ + "other.customGameSettings.__off__" + ], + "zh-CN": "关闭" + }, + "On": { + "en-US": "On", + "sources": [ + "other.customGameSettings.__on__" + ], + "zh-CN": "开启" + }, + "disabled": { + "en-US": "disabled", + "sources": [ + "other.customGameSettings.__disabled__" + ], + "zh-CN": "禁用" + } + } +} diff --git a/crates/workshop-rs/src/settings/table.rs b/crates/workshop-rs/src/settings/table.rs index 77c66d4..db8f1e1 100644 --- a/crates/workshop-rs/src/settings/table.rs +++ b/crates/workshop-rs/src/settings/table.rs @@ -8,6 +8,35 @@ //! (LICENSE-BOUNDARY policy). Additions to the table (e.g. the acquired //! candidate snapshots) are data-only. +use serde_json::Value; +use std::sync::OnceLock; + +/// Locale-specific settings names generated from the reviewed Workshop data +/// export. English remains the table's canonical spelling; additional locale +/// spellings are data, not semantic branches. +const LOCALE_DATA: &str = include_str!("data/zh-cn.json"); + +fn locale_data() -> &'static Value { + static DATA: OnceLock = OnceLock::new(); + DATA.get_or_init(|| { + serde_json::from_str(LOCALE_DATA).expect("generated settings locale data is valid JSON") + }) +} + +/// Resolve a settings display name from the generated locale corpus. +/// +/// The English table names are intentionally not duplicated in the locale +/// data. A missing entry means the target locale is not covered and callers +/// must preserve the explicit missing-mapping contract. +pub fn localized_name(locale: &str, section: &str, english: &str) -> Option<&'static str> { + let data = locale_data(); + let data_locale = data.get("locale")?.as_str()?; + if !data_locale.eq_ignore_ascii_case(locale) { + return None; + } + data.get(section)?.get(english)?.get(data_locale)?.as_str() +} + /// A leaf key kind: how a settings leaf renders and validates. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyKind { diff --git a/crates/workshop-rs/tests/catalog.rs b/crates/workshop-rs/tests/catalog.rs index e0e4186..855b775 100644 --- a/crates/workshop-rs/tests/catalog.rs +++ b/crates/workshop-rs/tests/catalog.rs @@ -25,10 +25,10 @@ fn builtin_catalog_loads_and_declares_en_us_and_zh_cn() { catalog.locale_coverage(&en()).mapped, catalog.locale_coverage(&en()).total ); - // zh-CN is declared with an empty mapping set pending reviewed evidence: - // no compatibility claim is made, and conversion fails explicitly. + // zh-CN is the evidence-backed corpus locale; its exclusions remain + // explicitly unmapped and therefore still fail closed. assert!(catalog.supports(&Locale::new("zh-CN"))); - assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 0); + assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 327); assert_eq!( catalog.locale_coverage(&Locale::new("zh-CN")).total, catalog.locale_coverage(&en()).total diff --git a/crates/workshop-rs/tests/corpus.rs b/crates/workshop-rs/tests/corpus.rs new file mode 100644 index 0000000..f6775ff --- /dev/null +++ b/crates/workshop-rs/tests/corpus.rs @@ -0,0 +1,77 @@ +//! Corpus-backed zh-CN conversion evidence. + +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::convert::{self, ConvertOptions}; + +fn catalog() -> Catalog { + Catalog::builtin().expect("catalog validates") +} + +fn en() -> Locale { + Locale::new("en-US") +} + +fn zh() -> Locale { + Locale::new("zh-CN") +} + +const REPRESENTATIVE: &str = "variables { + global: + 0: result +} + +rule (\"corpus\") { + event { + Ongoing - Global; + } + actions { + Set Global Variable(result, Add(1, 2)); + Wait(1, Ignore Condition); + } +} +"; + +#[test] +fn manifest_pins_the_export_and_exact_match_coverage() { + let manifest: serde_json::Value = + serde_json::from_str(include_str!("../../../tools/corpus/zh-cn-corpus.json")) + .expect("generated corpus manifest is valid JSON"); + assert_eq!(manifest["locale"], "zh-CN"); + assert_eq!( + manifest["source"]["commit"], + "d854bf01fc7bbf3b2169f67408c07a8da8989ad6" + ); + assert_eq!(manifest["coverage"]["total"]["matched"], 327); + assert_eq!(manifest["coverage"]["total"]["total"], 344); + assert_eq!(manifest["matches"].as_array().unwrap().len(), 327); + assert_eq!(manifest["excluded"].as_array().unwrap().len(), 17); +} + +#[test] +fn representative_corpus_converts_in_both_directions() { + let catalog = catalog(); + let to_zh = convert::convert( + REPRESENTATIVE, + &catalog, + &en(), + &zh(), + &ConvertOptions::default(), + ) + .expect("en-US corpus converts to zh-CN"); + assert!(to_zh.fallback_ids.is_empty()); + assert!(to_zh.text.contains("持续 - 全局"), "{}", to_zh.text); + assert!(to_zh.text.contains("设置全局变量"), "{}", to_zh.text); + assert!(to_zh.text.contains("加(1, 2)"), "{}", to_zh.text); + assert!(to_zh.text.contains("等待(1, 无视条件)"), "{}", to_zh.text); + + let back_to_en = convert::convert( + &to_zh.text, + &catalog, + &zh(), + &en(), + &ConvertOptions::default(), + ) + .expect("zh-CN corpus converts back to en-US"); + assert_eq!(back_to_en.fallback_ids, Vec::::new()); + assert_eq!(back_to_en.text.trim_end(), REPRESENTATIVE.trim_end()); +} diff --git a/crates/workshop-rs/tests/identity.rs b/crates/workshop-rs/tests/identity.rs index 86c08f6..984e684 100644 --- a/crates/workshop-rs/tests/identity.rs +++ b/crates/workshop-rs/tests/identity.rs @@ -12,7 +12,7 @@ use workshop_rs::catalog::{Catalog, Locale}; /// (`workshop-catalog-gen build`) recomputes it and the pin is updated /// deliberately together with the data. const PINNED_CATALOG_DIGEST: &str = - "75d8bb9c50e3ad0606656d58897b49e37934e04781fb2a510eefcd555dc2e29f"; + "5a7f7ba75a81f52d33b039fb3f0f2d367959c66b23bc874deb2357514eb7815d"; #[test] fn committed_catalog_digest_is_pinned() { @@ -75,7 +75,7 @@ fn locale_coverage_is_exact_and_primary_is_complete() { "declared en-US surface (168 entries + 176 members)" ); let zh = catalog.locale_coverage(&Locale::new("zh-CN")); - assert_eq!(zh.mapped, 0, "zh-CN declares no mappings yet"); + assert_eq!(zh.mapped, 327, "zh-CN corpus coverage is pinned"); assert_eq!(zh.total, en.total); let all = catalog.locale_coverage_all(); assert_eq!(all.len(), 2); diff --git a/crates/workshop-rs/tests/locale.rs b/crates/workshop-rs/tests/locale.rs index 43cdba9..71efac1 100644 --- a/crates/workshop-rs/tests/locale.rs +++ b/crates/workshop-rs/tests/locale.rs @@ -3,11 +3,9 @@ //! target-locale mappings fail explicitly by default; fallback is opt-in and //! visible; settings follow the same contract. //! -//! The committed catalog declares `zh-CN` with an empty mapping set (0/344) -//! pending a reviewed, MIT-permissible reference source. This suite pins the -//! honest behavior that follows: conversion into zh-CN fails explicitly for -//! every entry, and the full conversion machinery is proven end-to-end with -//! a synthetic declared locale carrying clearly synthetic test spellings. +//! The committed catalog declares an evidence-backed `zh-CN` corpus (327/344). +//! This suite pins both successful corpus conversion and the fail-explicit +//! behavior for the 17 entries excluded by the exact-match pipeline. use workshop_rs::catalog::{Catalog, Kind, Locale}; use workshop_rs::convert::{self, ConvertOptions}; @@ -37,105 +35,86 @@ const BASIC_RULE: &str = "rule (\"setup\") { "; #[test] -fn emission_into_zh_cn_fails_explicitly_on_missing_mappings() { - // The zh-CN locale is declared with zero mappings: emitting any builtin - // into zh-CN is a missing-mapping diagnostic, never a silent passthrough - // of the en-US spelling. The first missing mapping (the event line, - // emitted before the actions) surfaces first. +fn emission_into_zh_cn_uses_evidence_backed_mappings() { let catalog = builtin(); let program = parser::parse(BASIC_RULE, &catalog, &en()).expect("parses"); - let error = emitter::emit(&program, &catalog, &zh()).expect_err("must fail explicitly"); - assert!( - matches!( - error, - workshop_rs::WorkshopError::MissingMapping { kind: "event", .. } - ), - "expected a structured missing-mapping diagnostic: {error}" - ); - assert!( - error.to_string().contains("zh-cn") && error.to_string().contains("global"), - "{error}" - ); + let output = emitter::emit(&program, &catalog, &zh()).expect("corpus mappings emit"); + assert!(output.contains("持续 - 全局"), "{output}"); + assert!(output.contains("禁用查看器录制"), "{output}"); } #[test] -fn conversion_en_to_zh_cn_fails_explicitly_without_fallback() { +fn conversion_en_to_zh_cn_uses_evidence_backed_mappings() { let catalog = builtin(); - let error = convert::convert( + let output = convert::convert( BASIC_RULE, &catalog, &en(), &zh(), &ConvertOptions::default(), ) - .expect_err("missing mappings must fail"); - assert!(error.to_string().contains("missing"), "{error}"); + .expect("corpus conversion succeeds"); + assert!(output.text.contains("持续 - 全局"), "{}", output.text); + assert!(output.fallback_ids.is_empty()); } +const UNMAPPED_RULE: &str = "rule (\"setup\") { + event { + Ongoing - Global; + } + actions { + Force Player Hero(Event Player, Ana); + } +} +"; + #[test] fn opt_in_fallback_emits_with_recorded_fallback_ids() { // Fallback is opt-in: with a fallback locale the emission succeeds and // the fell-back identities are recorded (visible in tooling output). let catalog = builtin(); - let program = parser::parse(BASIC_RULE, &catalog, &en()).expect("parses"); + let program = parser::parse(UNMAPPED_RULE, &catalog, &en()).expect("parses"); let options = EmitOptions { fallback_locale: Some(en()), }; let output = emitter::emit_with_options(&program, &catalog, &zh(), &options).expect("fallback emits"); - assert_eq!( - output.text.trim_end(), - BASIC_RULE.trim_end(), - "fallback text is the en-US spelling" - ); + assert!(output.text.contains("持续 - 全局"), "{}", output.text); + assert!(output.text.contains("Force Player Hero"), "{}", output.text); assert_eq!( output.fallback_ids, - vec!["global".to_string(), "disableInspector".to_string()], - "every fell-back canonical id is recorded (event then action)" + vec!["forcePlayerHero".to_string()], + "only the excluded action uses the explicit fallback" ); } #[test] fn opt_in_fallback_conversion_round_trips_through_zh_cn() { - // convert en -> zh-CN with fallback to en-US: the output is the - // fallback-locale (en-US) spelling surface, the fallback choice is - // recorded, and the output parses and emits identically in en-US. + // convert en -> zh-CN with fallback to en-US: mapped identities use the + // corpus while the excluded action uses the explicit fallback. let catalog = builtin(); let options = ConvertOptions { fallback_locale: Some(en()), }; - let out = convert::convert(BASIC_RULE, &catalog, &en(), &zh(), &options) + let out = convert::convert(UNMAPPED_RULE, &catalog, &en(), &zh(), &options) .expect("fallback conversion emits"); assert!(!out.fallback_ids.is_empty(), "fallback is recorded"); - assert_eq!( - out.text.trim_end(), - BASIC_RULE.trim_end(), - "the fallback output is the en-US spelling surface" - ); - // The output is en-US text: it parses in en-US and re-emits identically. - let reparsed = parser::parse(&out.text, &catalog, &en()).expect("fallback output parses"); - let reemitted = emitter::emit(&reparsed, &catalog, &en()).expect("re-emits"); - assert_eq!(out.text, reemitted, "fallback output is a fixed point"); + assert!(out.text.contains("持续 - 全局"), "{}", out.text); + assert!(out.text.contains("Force Player Hero"), "{}", out.text); + assert!(out.fallback_ids.contains(&"forcePlayerHero".to_string())); } #[test] -fn parsing_zh_cn_input_fails_explicitly_without_data() { - // With zero zh-CN aliases, zh-CN Workshop text cannot resolve any - // builtin: the parse fails with a structured Unknown diagnostic at the - // first spelling. No guessing, no fallback. +fn parsing_zh_cn_input_uses_corpus_aliases() { let catalog = builtin(); - let synthetic_zh = "rule (\"x\") { event { Ongoing - Global; } actions { Synthetic Action; } }"; - let error = parser::parse(synthetic_zh, &catalog, &zh()).expect_err("no zh-CN data yet"); - assert!( - matches!(error, workshop_rs::WorkshopError::Unknown { .. }), - "expected an Unknown diagnostic: {error}" - ); + let localized = "rule (\"x\") { event { 持续 - 全局; } actions { 禁用查看器录制; } }"; + parser::parse(localized, &catalog, &zh()).expect("corpus aliases parse"); } #[test] fn explicit_zh_cn_override_passes_locale_support() { // The locale machinery accepts an explicit override to a declared - // locale; the parse then fails on data, not on locale support. + // locale, independently of the corpus coverage. use workshop_rs::detect; let catalog = builtin(); let locale = detect::resolve_locale("garbage", &catalog, Some(&zh())).expect("override wins"); @@ -143,7 +122,7 @@ fn explicit_zh_cn_override_passes_locale_support() { } #[test] -fn detection_is_unaffected_by_the_empty_zh_cn_locale() { +fn detection_ranks_zh_cn_after_en_us_for_en_us_input() { use workshop_rs::detect; let catalog = builtin(); let detection = detect::detect(BASIC_RULE, &catalog); @@ -154,12 +133,12 @@ fn detection_is_unaffected_by_the_empty_zh_cn_locale() { .last() .map(|(locale, _)| locale.clone()), Some(zh()), - "zh-CN ranks last with zero matches" + "zh-CN remains behind en-US for en-US input" ); } #[test] -fn settings_emission_into_zh_cn_fails_without_fallback_and_works_with_it() { +fn settings_emission_into_zh_cn_uses_the_generated_locale_corpus() { use workshop_rs::settings::{Settings, SettingsNode}; let catalog = builtin(); let program = workshop_rs::wir::Program { @@ -177,28 +156,8 @@ fn settings_emission_into_zh_cn_fails_without_fallback_and_works_with_it() { }), ..workshop_rs::wir::Program::default() }; - let error = emitter::emit(&program, &catalog, &zh()).expect_err("settings must fail"); - assert!( - matches!( - error, - workshop_rs::WorkshopError::MissingMapping { - kind: "setting", - .. - } - ), - "settings emission into zh-CN fails explicitly: {error}" - ); - let options = EmitOptions { - fallback_locale: Some(en()), - }; - let output = emitter::emit_with_options(&program, &catalog, &zh(), &options) - .expect("settings fallback emits"); - assert!( - output.text.contains("Max FFA Players: 6"), - "{}", - output.text - ); - assert!(output.fallback_ids.contains(&"settings".to_string())); + let output = emitter::emit(&program, &catalog, &zh()).expect("settings corpus emits"); + assert!(output.contains("自由混战人数上限: 6"), "{}", output); } /// A test-only catalog with a second declared locale carrying clearly @@ -334,12 +293,11 @@ fn canonical_ids_are_locale_independent_in_wir() { } #[test] -fn catalog_spelling_lookup_answers_none_for_unmapped_locales() { +fn catalog_spelling_lookup_distinguishes_mapped_and_unmapped_locales() { let catalog = builtin(); assert_eq!( catalog.spelling(Kind::Action, &zh(), "disableInspector"), - None, - "no zh-CN mapping exists" + Some("禁用查看器录制") ); assert_eq!( catalog.spelling(Kind::Action, &en(), "disableInspector"), diff --git a/docs/provenance.md b/docs/provenance.md index fd6ee7b..da6d0ae 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -38,14 +38,24 @@ license, reviewed) is embedded in the dataset itself and surfaced by * `en-US` is the primary locale and is complete (344/344 canonical entries: 168 builtins + 176 enum members). The committed catalog validates that the primary locale is complete. -* `zh-CN` is declared as a locale with an **empty mapping set (0/344)**. No - zh-CN compatibility claim is made. Adding zh-CN aliases requires a - reviewed, MIT-permissible reference source (ADR-0001 Decision 6, open - question: permissibility of candidate zh-CN reference sources). The - workspace evidence hierarchy permits *documented community evidence* - (level 6) for spellings, but every added alias must carry a provenance - note identifying its source; unverifiable spellings must not be added — - missing mappings fail explicitly by design. +* `zh-CN` has an evidence-backed corpus of **327/344** canonical entries: + structural 11/11, actions 55/62, values 77/78, events 3/3, operators 8/14, + and enum members 173/176. The reproducible manifest is + `tools/corpus/zh-cn-corpus.json`; it records exact en-US spelling matches, + every exclusion, and the export provenance. The source is the user-provided + `workshop-data/workshop-data.json` export at commit + `d854bf01fc7bbf3b2169f67408c07a8da8989ad6`, commit date 2026-08-12, fetched + 2026-08-17. The export is not committed to this repository. +* The generated settings corpus covers labels 17/19, modes 6/7, maps 2/2, + heroes 10/10, enum values 2/2, tokens 3/3, and teams 0/1. Its exact-match + exclusions are recorded in + `crates/workshop-rs/src/settings/data/zh-cn.json`; settings without a + mapping continue to fail explicitly. The data's license review remains + marked pending until the Blizzard-content redistribution review is recorded. + +All committed zh-CN spellings come from the export through the corpus +pipeline; no OverPy translation table is used. The complete catalog coverage +and settings gate remains open for the recorded exclusions. ## Test fixtures (`tests/fixtures/`) diff --git a/tools/corpus/zh-cn-corpus.json b/tools/corpus/zh-cn-corpus.json new file mode 100644 index 0000000..05baeef --- /dev/null +++ b/tools/corpus/zh-cn-corpus.json @@ -0,0 +1,3173 @@ +{ + "coverage": { + "actions": { + "matched": 55, + "total": 62 + }, + "enums": { + "matched": 173, + "total": 176 + }, + "events": { + "matched": 3, + "total": 3 + }, + "operators": { + "matched": 8, + "total": 14 + }, + "structural": { + "matched": 11, + "total": 11 + }, + "total": { + "matched": 327, + "total": 344 + }, + "values": { + "matched": 77, + "total": 78 + } + }, + "excluded": [ + { + "en-US": "Chase Variable At Rate", + "id": "chaseVariableAtRate", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Delete All Classes", + "id": "deleteAllClasses", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Force Player Hero", + "id": "forcePlayerHero", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Force Throttle", + "id": "forceThrottle", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Set Allowed Heroes", + "id": "setAllowedHeroes", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Stop Chasing Variable", + "id": "stopChasingVariable", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Stop Forcing Hero", + "id": "stopForcingHero", + "kind": "action", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Lijiang Tower Lunar", + "id": "Map.LIJIANG_TOWER_LUNAR", + "kind": "enum member", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Visible To And Values", + "id": "ProgressBarWorldReeval.VISIBLE_TO_AND_VALUES", + "kind": "enum member", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Nearest", + "id": "Rounding.NEAREST", + "kind": "enum member", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "!=", + "id": "!=", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "<", + "id": "<", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "<=", + "id": "<=", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "==", + "id": "==", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": ">", + "id": ">", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": ">=", + "id": ">=", + "kind": "operator", + "reason": "no exact en-US match in the export" + }, + { + "en-US": "Array Element", + "id": "arrayElement", + "kind": "value", + "reason": "no exact en-US match in the export" + } + ], + "generator": "workshop-catalog-gen corpus", + "generatorVersion": "0.1.0", + "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + "locale": "zh-CN", + "matches": [ + { + "en-US": "Abort", + "id": "abort", + "kind": "action", + "sources": [ + "actions.return" + ], + "zh-CN": "中止" + }, + { + "en-US": "Abort If", + "id": "abortIf", + "kind": "action", + "sources": [ + "actions.__abortIf__" + ], + "zh-CN": "根据条件中止" + }, + { + "en-US": "Allow Button", + "id": "allowButton", + "kind": "action", + "sources": [ + "actions..allowButton" + ], + "zh-CN": "可用按钮" + }, + { + "en-US": "Big Message", + "id": "bigMessage", + "kind": "action", + "sources": [ + "actions.bigMessage" + ], + "zh-CN": "大字体信息" + }, + { + "en-US": "Chase Global Variable At Rate", + "id": "chaseAtRate", + "kind": "action", + "sources": [ + "actions.__chaseGlobalVariableAtRate__" + ], + "zh-CN": "追踪全局变量频率" + }, + { + "en-US": "Chase Global Variable Over Time", + "id": "chaseOverTime", + "kind": "action", + "sources": [ + "actions.__chaseGlobalVariableOverTime__" + ], + "zh-CN": "持续追踪全局变量" + }, + { + "en-US": "Chase Player Variable At Rate", + "id": "chasePlayerVariableAtRate", + "kind": "action", + "sources": [ + "actions.__chasePlayerVariableAtRate__" + ], + "zh-CN": "追踪玩家变量频率" + }, + { + "en-US": "Chase Player Variable Over Time", + "id": "chasePlayerVariableOverTime", + "kind": "action", + "sources": [ + "actions.__chasePlayerVariableOverTime__" + ], + "zh-CN": "持续追踪玩家变量" + }, + { + "en-US": "Create Beam Effect", + "id": "createBeamEffect", + "kind": "action", + "sources": [ + "actions.createBeam" + ], + "zh-CN": "创建光束效果" + }, + { + "en-US": "Create Effect", + "id": "createEffect", + "kind": "action", + "sources": [ + "actions.createEffect" + ], + "zh-CN": "创建效果" + }, + { + "en-US": "Create HUD Text", + "id": "createHudText", + "kind": "action", + "sources": [ + "actions.hudText" + ], + "zh-CN": "创建HUD文本" + }, + { + "en-US": "Create In-World Text", + "id": "createInWorldText", + "kind": "action", + "sources": [ + "actions.createInWorldText" + ], + "zh-CN": "创建地图文本" + }, + { + "en-US": "Create Progress Bar In-World Text", + "id": "createProgressBarInWorldText", + "kind": "action", + "sources": [ + "actions.createProgressBarInWorldText" + ], + "zh-CN": "创建进度条地图文本" + }, + { + "en-US": "Destroy All Progress Bar HUD Text", + "id": "destroyAllProgressBarHudText", + "kind": "action", + "sources": [ + "actions.destroyAllProgressBarHuds" + ], + "zh-CN": "消除所有进度条HUD文本" + }, + { + "en-US": "Destroy All Progress Bar In-World Text", + "id": "destroyAllProgressBarInWorldText", + "kind": "action", + "sources": [ + "actions.destroyAllProgressBarInWorldTexts" + ], + "zh-CN": "消除所有进度条地图文本" + }, + { + "en-US": "Destroy Effect", + "id": "destroyEffect", + "kind": "action", + "sources": [ + "actions.destroyEffect" + ], + "zh-CN": "消除效果" + }, + { + "en-US": "Destroy HUD Text", + "id": "destroyHudText", + "kind": "action", + "sources": [ + "actions.destroyHudText" + ], + "zh-CN": "消除HUD文本" + }, + { + "en-US": "Destroy In-World Text", + "id": "destroyInWorldText", + "kind": "action", + "sources": [ + "actions.destroyInWorldText" + ], + "zh-CN": "消除地图文本" + }, + { + "en-US": "Disable Game Mode HUD", + "id": "disableGameModeHud", + "kind": "action", + "sources": [ + "actions..disableGamemodeHud" + ], + "zh-CN": "隐藏游戏模式HUD" + }, + { + "en-US": "Disable Game Mode In-World UI", + "id": "disableGameModeInworldUI", + "kind": "action", + "sources": [ + "actions..disableGamemodeInWorldUi" + ], + "zh-CN": "隐藏游戏模式地图UI" + }, + { + "en-US": "Disable Hero HUD", + "id": "disableHeroHud", + "kind": "action", + "sources": [ + "actions..disableHeroHud" + ], + "zh-CN": "隐藏英雄HUD" + }, + { + "en-US": "Disable Inspector Recording", + "id": "disableInspector", + "kind": "action", + "sources": [ + "actions.disableInspector" + ], + "zh-CN": "禁用查看器录制" + }, + { + "en-US": "Disable Movement Collision With Environment", + "id": "disableMovementCollisionWithEnvironment", + "kind": "action", + "sources": [ + "actions..disableEnvironmentCollision" + ], + "zh-CN": "取消与环境的移动碰撞" + }, + { + "en-US": "Disable Movement Collision With Players", + "id": "disableMovementCollisionWithPlayers", + "kind": "action", + "sources": [ + "actions..disablePlayerCollision" + ], + "zh-CN": "取消与玩家的移动碰撞" + }, + { + "en-US": "Disable Scoreboard", + "id": "disableScoreboard", + "kind": "action", + "sources": [ + "actions..disableScoreboard" + ], + "zh-CN": "隐藏计分板" + }, + { + "en-US": "Disallow Button", + "id": "disallowButton", + "kind": "action", + "sources": [ + "actions..disallowButton" + ], + "zh-CN": "禁用按钮" + }, + { + "en-US": "Enable Game Mode HUD", + "id": "enableGameModeHud", + "kind": "action", + "sources": [ + "actions..enableGamemodeHud" + ], + "zh-CN": "显示游戏模式HUD" + }, + { + "en-US": "Enable Game Mode In-World UI", + "id": "enableGameModeInworldUI", + "kind": "action", + "sources": [ + "actions..enableGamemodeInWorldUi" + ], + "zh-CN": "显示游戏模式地图UI" + }, + { + "en-US": "Enable Hero HUD", + "id": "enableHeroHud", + "kind": "action", + "sources": [ + "actions..enableHeroHud" + ], + "zh-CN": "显示英雄HUD" + }, + { + "en-US": "Enable Inspector Recording", + "id": "enableInspectorRecording", + "kind": "action", + "sources": [ + "actions.enableInspector" + ], + "zh-CN": "启用查看器录制" + }, + { + "en-US": "Enable Movement Collision With Environment", + "id": "enableMovementCollisionWithEnvironment", + "kind": "action", + "sources": [ + "actions..enableEnvironmentCollision" + ], + "zh-CN": "开启与环境的移动碰撞" + }, + { + "en-US": "Enable Movement Collision With Players", + "id": "enableMovementCollisionWithPlayers", + "kind": "action", + "sources": [ + "actions..enablePlayerCollision" + ], + "zh-CN": "开启与玩家的移动碰撞" + }, + { + "en-US": "Enable Scoreboard", + "id": "enableScoreboard", + "kind": "action", + "sources": [ + "actions..enableScoreboard" + ], + "zh-CN": "显示计分板" + }, + { + "en-US": "Loop If Condition Is True", + "id": "loopIfConditionIsTrue", + "kind": "action", + "sources": [ + "actions.__loopIfConditionIsTrue__" + ], + "zh-CN": "如条件为“真”则循环" + }, + { + "en-US": "Modify Global Variable", + "id": "modifyGlobalVariable", + "kind": "action", + "sources": [ + "actions.__modifyGlobalVariable__" + ], + "zh-CN": "修改全局变量" + }, + { + "en-US": "Play Effect", + "id": "playEffect", + "kind": "action", + "sources": [ + "actions.playEffect" + ], + "zh-CN": "播放效果" + }, + { + "en-US": "Set Aim Speed", + "id": "setAimSpeed", + "kind": "action", + "sources": [ + "actions..setAimSpeed" + ], + "zh-CN": "设置瞄准速度" + }, + { + "en-US": "Set Damage Dealt", + "id": "setDamageDealt", + "kind": "action", + "sources": [ + "actions..setDamageDealt" + ], + "zh-CN": "设置造成伤害" + }, + { + "en-US": "Set Damage Received", + "id": "setDamageReceived", + "kind": "action", + "sources": [ + "actions..setDamageReceived" + ], + "zh-CN": "设置受到伤害" + }, + { + "en-US": "Set Gravity", + "id": "setGravity", + "kind": "action", + "sources": [ + "actions..setGravity" + ], + "zh-CN": "设置引力" + }, + { + "en-US": "Set Player Health", + "id": "setHealth", + "kind": "action", + "sources": [ + "actions..setHealth" + ], + "zh-CN": "设置玩家生命值" + }, + { + "en-US": "Set Invisible", + "id": "setInvisibility", + "kind": "action", + "sources": [ + "actions..setInvisibility" + ], + "zh-CN": "设置不可见" + }, + { + "en-US": "Set Max Health", + "id": "setMaxHealth", + "kind": "action", + "sources": [ + "actions..setMaxHealth" + ], + "zh-CN": "设置最大生命值" + }, + { + "en-US": "Set Move Speed", + "id": "setMoveSpeed", + "kind": "action", + "sources": [ + "actions..setMoveSpeed" + ], + "zh-CN": "设置移动速度" + }, + { + "en-US": "Set Status", + "id": "setStatusEffect", + "kind": "action", + "sources": [ + "actions..setStatusEffect" + ], + "zh-CN": "设置状态" + }, + { + "en-US": "Set Ultimate Charge", + "id": "setUltCharge", + "kind": "action", + "sources": [ + "actions..setUltCharge" + ], + "zh-CN": "设置终极技能充能" + }, + { + "en-US": "Skip", + "id": "skip", + "kind": "action", + "sources": [ + "actions.__skip__" + ], + "zh-CN": "跳过" + }, + { + "en-US": "Small Message", + "id": "smallMessage", + "kind": "action", + "sources": [ + "actions.smallMessage" + ], + "zh-CN": "小字体信息" + }, + { + "en-US": "Start Camera", + "id": "startCamera", + "kind": "action", + "sources": [ + "actions..startCamera" + ], + "zh-CN": "开始镜头" + }, + { + "en-US": "Start Game Mode", + "id": "startGameMode", + "kind": "action", + "sources": [ + "actions.startGamemode" + ], + "zh-CN": "开始游戏模式" + }, + { + "en-US": "Stop Camera", + "id": "stopCamera", + "kind": "action", + "sources": [ + "actions..stopCamera" + ], + "zh-CN": "停止镜头" + }, + { + "en-US": "Stop Forcing Throttle", + "id": "stopForcingThrottle", + "kind": "action", + "sources": [ + "actions..stopForcingThrottle" + ], + "zh-CN": "停止限制阈值" + }, + { + "en-US": "Teleport", + "id": "teleport", + "kind": "action", + "sources": [ + "actions..teleport" + ], + "zh-CN": "传送" + }, + { + "en-US": "Wait", + "id": "wait", + "kind": "action", + "sources": [ + "actions.wait" + ], + "zh-CN": "等待" + }, + { + "en-US": "Wait Until", + "id": "waitUntil", + "kind": "action", + "sources": [ + "actions.waitUntil" + ], + "zh-CN": "等待直到 " + }, + { + "en-US": "Good Beam", + "id": "Beam.GOOD", + "kind": "enum member", + "sources": [ + "constants.Beam.GOOD" + ], + "zh-CN": "有益光束" + }, + { + "en-US": "Grapple Beam", + "id": "Beam.GRAPPLE", + "kind": "enum member", + "sources": [ + "constants.Beam.GRAPPLE" + ], + "zh-CN": "抓钩光束" + }, + { + "en-US": "Ability 1", + "id": "Button.ABILITY_1", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.ABILITY_1" + ], + "zh-CN": "技能1" + }, + { + "en-US": "Ability 2", + "id": "Button.ABILITY_2", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.ABILITY_2" + ], + "zh-CN": "技能2" + }, + { + "en-US": "Crouch", + "id": "Button.CROUCH", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.CROUCH" + ], + "zh-CN": "蹲下" + }, + { + "en-US": "Interact", + "id": "Button.INTERACT", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.INTERACT" + ], + "zh-CN": "互动" + }, + { + "en-US": "Jump", + "id": "Button.JUMP", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.JUMP" + ], + "zh-CN": "跳跃" + }, + { + "en-US": "Melee", + "id": "Button.MELEE", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.MELEE" + ], + "zh-CN": "近身攻击" + }, + { + "en-US": "Primary Fire", + "id": "Button.PRIMARY_FIRE", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.PRIMARY_FIRE" + ], + "zh-CN": "主要攻击模式" + }, + { + "en-US": "Reload", + "id": "Button.RELOAD", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.RELOAD" + ], + "zh-CN": "装填" + }, + { + "en-US": "Secondary Fire", + "id": "Button.SECONDARY_FIRE", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.SECONDARY_FIRE" + ], + "zh-CN": "辅助攻击模式" + }, + { + "en-US": "Ultimate", + "id": "Button.ULTIMATE", + "kind": "enum member", + "sources": [ + "constants.ButtonLiteral.ULTIMATE" + ], + "zh-CN": "终极技能" + }, + { + "en-US": "Destination and Rate", + "id": "ChaseRateReeval.DESTINATION_AND_RATE", + "kind": "enum member", + "sources": [ + "constants.ChaseRateReeval.DESTINATION_AND_RATE" + ], + "zh-CN": "速率及最终值" + }, + { + "en-US": "None", + "id": "ChaseRateReeval.NONE", + "kind": "enum member", + "sources": [ + "constants.ChaseRateReeval.NONE" + ], + "zh-CN": "全部禁用" + }, + { + "en-US": "Destination and Duration", + "id": "ChaseTimeReeval.DESTINATION_AND_DURATION", + "kind": "enum member", + "sources": [ + "constants.ChaseTimeReeval.DESTINATION_AND_DURATION" + ], + "zh-CN": "终点及持续时间" + }, + { + "en-US": "None", + "id": "ChaseTimeReeval.NONE", + "kind": "enum member", + "sources": [ + "constants.ChaseTimeReeval.NONE" + ], + "zh-CN": "全部禁用" + }, + { + "en-US": "Clip Against Surfaces", + "id": "Clipping.CLIP_AGAINST_SURFACES", + "kind": "enum member", + "sources": [ + "constants.Clip.SURFACES" + ], + "zh-CN": "根据表面截取" + }, + { + "en-US": "Do Not Clip", + "id": "Clipping.DO_NOT_CLIP", + "kind": "enum member", + "sources": [ + "constants.Clip.NONE" + ], + "zh-CN": "不要截取" + }, + { + "en-US": "Aqua", + "id": "Color.AQUA", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.AQUA" + ], + "zh-CN": "水绿色" + }, + { + "en-US": "Black", + "id": "Color.BLACK", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.BLACK" + ], + "zh-CN": "黑色" + }, + { + "en-US": "Blue", + "id": "Color.BLUE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.BLUE" + ], + "zh-CN": "蓝色" + }, + { + "en-US": "Gray", + "id": "Color.GRAY", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.GRAY" + ], + "zh-CN": "灰色" + }, + { + "en-US": "Green", + "id": "Color.GREEN", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.GREEN" + ], + "zh-CN": "绿色" + }, + { + "en-US": "Lime Green", + "id": "Color.LIME_GREEN", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.LIME_GREEN" + ], + "zh-CN": "灰绿色" + }, + { + "en-US": "Orange", + "id": "Color.ORANGE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.ORANGE" + ], + "zh-CN": "橙色" + }, + { + "en-US": "Purple", + "id": "Color.PURPLE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.PURPLE" + ], + "zh-CN": "亮紫色" + }, + { + "en-US": "Red", + "id": "Color.RED", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.RED" + ], + "zh-CN": "红色" + }, + { + "en-US": "Rose", + "id": "Color.ROSE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.ROSE" + ], + "zh-CN": "玫红" + }, + { + "en-US": "Sky Blue", + "id": "Color.SKY_BLUE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.SKY_BLUE" + ], + "zh-CN": "天蓝色" + }, + { + "en-US": "Team 1", + "id": "Color.TEAM_1", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.TEAM_1" + ], + "zh-CN": "队伍1" + }, + { + "en-US": "Team 2", + "id": "Color.TEAM_2", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.TEAM_2" + ], + "zh-CN": "队伍2" + }, + { + "en-US": "Turquoise", + "id": "Color.TURQUOISE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.TURQUOISE" + ], + "zh-CN": "青绿色" + }, + { + "en-US": "Violet", + "id": "Color.VIOLET", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.VIOLET" + ], + "zh-CN": "紫色" + }, + { + "en-US": "White", + "id": "Color.WHITE", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.WHITE" + ], + "zh-CN": "白色" + }, + { + "en-US": "Yellow", + "id": "Color.YELLOW", + "kind": "enum member", + "sources": [ + "constants.ColorLiteral.YELLOW" + ], + "zh-CN": "黄色" + }, + { + "en-US": "Bad Explosion", + "id": "DynamicEffect.BAD_EXPLOSION", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.BAD_EXPLOSION" + ], + "zh-CN": "有害爆炸" + }, + { + "en-US": "Buff Explosion Sound", + "id": "DynamicEffect.BUFF_EXPLOSION_SOUND", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.BUFF_EXPLOSION_SOUND" + ], + "zh-CN": "状态爆炸声音" + }, + { + "en-US": "Buff Impact Sound", + "id": "DynamicEffect.BUFF_IMPACT_SOUND", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.BUFF_IMPACT_SOUND" + ], + "zh-CN": "正面状态施加声音" + }, + { + "en-US": "Debuff Impact Sound", + "id": "DynamicEffect.DEBUFF_IMPACT_SOUND", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.DEBUFF_IMPACT_SOUND" + ], + "zh-CN": "负面状态施加声音" + }, + { + "en-US": "Explosion Sound", + "id": "DynamicEffect.EXPLOSION_SOUND", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.EXPLOSION_SOUND" + ], + "zh-CN": "爆炸声音" + }, + { + "en-US": "Ring Explosion Sound", + "id": "DynamicEffect.RING_EXPLOSION", + "kind": "enum member", + "sources": [ + "constants.DynamicEffect.RING_EXPLOSION_SOUND" + ], + "zh-CN": "环状爆炸声音" + }, + { + "en-US": "Orb", + "id": "Effect.ORB", + "kind": "enum member", + "sources": [ + "constants.Effect.ORB" + ], + "zh-CN": "球" + }, + { + "en-US": "Color", + "id": "EffectReeval.COLOR", + "kind": "enum member", + "sources": [ + "constants.EffectReeval.COLOR" + ], + "zh-CN": "颜色" + }, + { + "en-US": "Visible To", + "id": "EffectReeval.VISIBILITY", + "kind": "enum member", + "sources": [ + "constants.EffectReeval.VISIBILITY" + ], + "zh-CN": "可见" + }, + { + "en-US": "Visible To and Color", + "id": "EffectReeval.VISIBILITY_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.EffectReeval.VISIBILITY_AND_COLOR" + ], + "zh-CN": "可见和颜色" + }, + { + "en-US": "Visible To Position and Radius", + "id": "EffectReeval.VISIBLE_TO_POSITION_AND_RADIUS", + "kind": "enum member", + "sources": [ + "constants.EffectReeval.VISIBILITY_POSITION_AND_RADIUS" + ], + "zh-CN": "可见,位置和半径" + }, + { + "en-US": "Ana", + "id": "Hero.ANA", + "kind": "enum member", + "sources": [ + "heroes.ana", + "data.heroes.ana" + ], + "zh-CN": "安娜" + }, + { + "en-US": "Ashe", + "id": "Hero.ASHE", + "kind": "enum member", + "sources": [ + "heroes.ashe", + "data.heroes.ashe" + ], + "zh-CN": "艾什" + }, + { + "en-US": "Baptiste", + "id": "Hero.BAPTISTE", + "kind": "enum member", + "sources": [ + "heroes.baptiste", + "data.heroes.baptiste" + ], + "zh-CN": "巴蒂斯特" + }, + { + "en-US": "Bastion", + "id": "Hero.BASTION", + "kind": "enum member", + "sources": [ + "heroes.bastion", + "data.heroes.bastion" + ], + "zh-CN": "堡垒" + }, + { + "en-US": "Brigitte", + "id": "Hero.BRIGITTE", + "kind": "enum member", + "sources": [ + "heroes.brigitte", + "data.heroes.brigitte" + ], + "zh-CN": "布丽吉塔" + }, + { + "en-US": "Cassidy", + "id": "Hero.CASSIDY", + "kind": "enum member", + "sources": [ + "heroes.cassidy", + "data.heroes.cassidy" + ], + "zh-CN": "卡西迪" + }, + { + "en-US": "Doomfist", + "id": "Hero.DOOMFIST", + "kind": "enum member", + "sources": [ + "heroes.doomfist", + "data.heroes.doomfist" + ], + "zh-CN": "末日铁拳" + }, + { + "en-US": "D.Va", + "id": "Hero.DVA", + "kind": "enum member", + "sources": [ + "heroes.dva", + "data.heroes.dva" + ], + "zh-CN": "D.Va" + }, + { + "en-US": "Echo", + "id": "Hero.ECHO", + "kind": "enum member", + "sources": [ + "heroes.echo", + "data.heroes.echo" + ], + "zh-CN": "回声" + }, + { + "en-US": "Genji", + "id": "Hero.GENJI", + "kind": "enum member", + "sources": [ + "heroes.genji", + "data.heroes.genji" + ], + "zh-CN": "源氏" + }, + { + "en-US": "Hanzo", + "id": "Hero.HANZO", + "kind": "enum member", + "sources": [ + "heroes.hanzo", + "data.heroes.hanzo" + ], + "zh-CN": "半藏" + }, + { + "en-US": "Junkrat", + "id": "Hero.JUNKRAT", + "kind": "enum member", + "sources": [ + "heroes.junkrat", + "data.heroes.junkrat" + ], + "zh-CN": "狂鼠" + }, + { + "en-US": "Lúcio", + "id": "Hero.LUCIO", + "kind": "enum member", + "sources": [ + "heroes.lucio", + "data.heroes.lucio" + ], + "zh-CN": "卢西奥" + }, + { + "en-US": "Mei", + "id": "Hero.MEI", + "kind": "enum member", + "sources": [ + "heroes.mei", + "data.heroes.mei" + ], + "zh-CN": "美" + }, + { + "en-US": "Mercy", + "id": "Hero.MERCY", + "kind": "enum member", + "sources": [ + "heroes.mercy", + "data.heroes.mercy" + ], + "zh-CN": "天使" + }, + { + "en-US": "Moira", + "id": "Hero.MOIRA", + "kind": "enum member", + "sources": [ + "heroes.moira", + "data.heroes.moira" + ], + "zh-CN": "莫伊拉" + }, + { + "en-US": "Orisa", + "id": "Hero.ORISA", + "kind": "enum member", + "sources": [ + "heroes.orisa", + "data.heroes.orisa" + ], + "zh-CN": "奥丽莎" + }, + { + "en-US": "Pharah", + "id": "Hero.PHARAH", + "kind": "enum member", + "sources": [ + "heroes.pharah", + "data.heroes.pharah" + ], + "zh-CN": "法老之鹰" + }, + { + "en-US": "Reaper", + "id": "Hero.REAPER", + "kind": "enum member", + "sources": [ + "heroes.reaper", + "data.heroes.reaper" + ], + "zh-CN": "死神" + }, + { + "en-US": "Reinhardt", + "id": "Hero.REINHARDT", + "kind": "enum member", + "sources": [ + "heroes.reinhardt", + "data.heroes.reinhardt" + ], + "zh-CN": "莱因哈特" + }, + { + "en-US": "Roadhog", + "id": "Hero.ROADHOG", + "kind": "enum member", + "sources": [ + "heroes.roadhog", + "data.heroes.roadhog" + ], + "zh-CN": "路霸" + }, + { + "en-US": "Sigma", + "id": "Hero.SIGMA", + "kind": "enum member", + "sources": [ + "heroes.sigma", + "data.heroes.sigma" + ], + "zh-CN": "西格玛" + }, + { + "en-US": "Soldier: 76", + "id": "Hero.SOLDIER_76", + "kind": "enum member", + "sources": [ + "heroes.soldier", + "data.heroes.soldier" + ], + "zh-CN": "士兵:76" + }, + { + "en-US": "Sombra", + "id": "Hero.SOMBRA", + "kind": "enum member", + "sources": [ + "heroes.sombra", + "data.heroes.sombra" + ], + "zh-CN": "黑影" + }, + { + "en-US": "Symmetra", + "id": "Hero.SYMMETRA", + "kind": "enum member", + "sources": [ + "heroes.symmetra", + "data.heroes.symmetra" + ], + "zh-CN": "秩序之光" + }, + { + "en-US": "Torbjörn", + "id": "Hero.TORBJORN", + "kind": "enum member", + "sources": [ + "heroes.torbjorn", + "data.heroes.torbjorn" + ], + "zh-CN": "托比昂" + }, + { + "en-US": "Tracer", + "id": "Hero.TRACER", + "kind": "enum member", + "sources": [ + "heroes.tracer", + "data.heroes.tracer" + ], + "zh-CN": "猎空" + }, + { + "en-US": "Widowmaker", + "id": "Hero.WIDOWMAKER", + "kind": "enum member", + "sources": [ + "heroes.widowmaker", + "data.heroes.widowmaker" + ], + "zh-CN": "黑百合" + }, + { + "en-US": "Winston", + "id": "Hero.WINSTON", + "kind": "enum member", + "sources": [ + "heroes.winston", + "data.heroes.winston" + ], + "zh-CN": "温斯顿" + }, + { + "en-US": "Wrecking Ball", + "id": "Hero.WRECKING_BALL", + "kind": "enum member", + "sources": [ + "heroes.wreckingBall", + "data.heroes.wreckingBall" + ], + "zh-CN": "破坏球" + }, + { + "en-US": "Zarya", + "id": "Hero.ZARYA", + "kind": "enum member", + "sources": [ + "heroes.zarya", + "data.heroes.zarya" + ], + "zh-CN": "查莉娅" + }, + { + "en-US": "Zenyatta", + "id": "Hero.ZENYATTA", + "kind": "enum member", + "sources": [ + "heroes.zenyatta", + "data.heroes.zenyatta" + ], + "zh-CN": "禅雅塔" + }, + { + "en-US": "Left", + "id": "HudPosition.LEFT", + "kind": "enum member", + "sources": [ + "constants.HudPosition.ACTUALLY_LEFT", + "constants.HudPosition.LEFT" + ], + "zh-CN": "左边" + }, + { + "en-US": "Right", + "id": "HudPosition.RIGHT", + "kind": "enum member", + "sources": [ + "constants.HudPosition.RIGHT" + ], + "zh-CN": "右边" + }, + { + "en-US": "Visible To", + "id": "HudReeval.VISIBILITY", + "kind": "enum member", + "sources": [ + "constants.HudReeval.VISIBILITY" + ], + "zh-CN": "可见" + }, + { + "en-US": "Visible To and String", + "id": "HudReeval.VISIBILITY_AND_STRING", + "kind": "enum member", + "sources": [ + "constants.HudReeval.VISIBILITY_AND_STRING" + ], + "zh-CN": "可见和字符串" + }, + { + "en-US": "Visible To Sort Order String and Color", + "id": "HudReeval.VISIBILITY_SORT_ORDER_STRING_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.HudReeval.VISIBILITY_SORT_ORDER_STRING_AND_COLOR" + ], + "zh-CN": "可见,排序规则,字符串和颜色" + }, + { + "en-US": "Visible To and Color", + "id": "HudReeval.VISIBLE_TO_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.HudReeval.VISIBILITY_AND_COLOR" + ], + "zh-CN": "可见和颜色" + }, + { + "en-US": "Visible To String and Color", + "id": "HudReeval.VISIBLE_TO_STRING_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.HudReeval.VISIBILITY_STRING_AND_COLOR" + ], + "zh-CN": "可见,字符串和颜色" + }, + { + "en-US": "Checkmark", + "id": "Icon.CHECKMARK", + "kind": "enum member", + "sources": [ + "constants.Icon.CHECKMARK" + ], + "zh-CN": "对号" + }, + { + "en-US": "No", + "id": "Icon.NO", + "kind": "enum member", + "sources": [ + "constants.Icon.NO" + ], + "zh-CN": "拒绝" + }, + { + "en-US": "Question Mark", + "id": "Icon.QUESTION_MARK", + "kind": "enum member", + "sources": [ + "constants.Icon.QUESTION_MARK" + ], + "zh-CN": "问号" + }, + { + "en-US": "Ring Thin", + "id": "Icon.RING_THIN", + "kind": "enum member", + "sources": [ + "constants.Icon.RING_THIN" + ], + "zh-CN": "细环" + }, + { + "en-US": "Skull", + "id": "Icon.SKULL", + "kind": "enum member", + "sources": [ + "constants.Icon.SKULL" + ], + "zh-CN": "骷髅" + }, + { + "en-US": "All", + "id": "Invis.ALL", + "kind": "enum member", + "sources": [ + "constants.Invis.ALL" + ], + "zh-CN": "全部" + }, + { + "en-US": "Enemies", + "id": "Invis.ENEMIES", + "kind": "enum member", + "sources": [ + "constants.Invis.ENEMIES" + ], + "zh-CN": "敌人" + }, + { + "en-US": "None", + "id": "Invis.NONE", + "kind": "enum member", + "sources": [ + "constants.Invis.NONE" + ], + "zh-CN": "全部禁用" + }, + { + "en-US": "String", + "id": "InworldTextReeval.STRING", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.STRING" + ], + "zh-CN": "字符串" + }, + { + "en-US": "Visible To", + "id": "InworldTextReeval.VISIBLE_TO", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY" + ], + "zh-CN": "可见" + }, + { + "en-US": "Visible To and Color", + "id": "InworldTextReeval.VISIBLE_TO_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_AND_COLOR" + ], + "zh-CN": "可见和颜色" + }, + { + "en-US": "Visible To and Position", + "id": "InworldTextReeval.VISIBLE_TO_AND_POSITION", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_AND_POSITION" + ], + "zh-CN": "可见和位置" + }, + { + "en-US": "Visible To and String", + "id": "InworldTextReeval.VISIBLE_TO_AND_STRING", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_AND_STRING" + ], + "zh-CN": "可见和字符串" + }, + { + "en-US": "Visible To Position and Color", + "id": "InworldTextReeval.VISIBLE_TO_POSITION_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_POSITION_AND_COLOR" + ], + "zh-CN": "可见,位置和颜色" + }, + { + "en-US": "Visible To Position and String", + "id": "InworldTextReeval.VISIBLE_TO_POSITION_AND_STRING", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_POSITION_AND_STRING" + ], + "zh-CN": "可见,位置和字符串" + }, + { + "en-US": "Visible To Position String and Color", + "id": "InworldTextReeval.VISIBLE_TO_POSITION_STRING_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_POSITION_STRING_AND_COLOR" + ], + "zh-CN": "可见,位置,字符串和颜色" + }, + { + "en-US": "Visible To String and Color", + "id": "InworldTextReeval.VISIBLE_TO_STRING_AND_COLOR", + "kind": "enum member", + "sources": [ + "constants.WorldTextReeval.VISIBILITY_STRING_AND_COLOR" + ], + "zh-CN": "可见,字符串和颜色" + }, + { + "en-US": "Off", + "id": "LosCheck.OFF", + "kind": "enum member", + "sources": [ + "constants.LosCheck.OFF" + ], + "zh-CN": "关闭" + }, + { + "en-US": "Surfaces", + "id": "LosCheck.SURFACES", + "kind": "enum member", + "sources": [ + "constants.LosCheck.SURFACES" + ], + "zh-CN": "表面" + }, + { + "en-US": "Surfaces And All Barriers", + "id": "LosCheck.SURFACES_AND_ALL_BARRIERS", + "kind": "enum member", + "sources": [ + "constants.LosCheck.SURFACES_AND_ALL_BARRIERS" + ], + "zh-CN": "表面及全部屏障" + }, + { + "en-US": "Surfaces And Enemy Barriers", + "id": "LosCheck.SURFACES_AND_ENEMY_BARRIERS", + "kind": "enum member", + "sources": [ + "constants.LosCheck.SURFACES_AND_ENEMY_BARRIERS" + ], + "zh-CN": "表面及敌方屏障" + }, + { + "en-US": "Aatlis", + "id": "Map.AATLIS", + "kind": "enum member", + "sources": [ + "data.maps.aatlis" + ], + "zh-CN": "阿特利斯" + }, + { + "en-US": "Antarctic Peninsula", + "id": "Map.ANTARCTIC_PENINSULA", + "kind": "enum member", + "sources": [ + "maps.antarcticPeninsula", + "data.maps.antarcticPeninsula" + ], + "zh-CN": "南极半岛" + }, + { + "en-US": "Blizzard World", + "id": "Map.BLIZZARD_WORLD", + "kind": "enum member", + "sources": [ + "maps.blizzWorld", + "data.maps.blizzWorld" + ], + "zh-CN": "暴雪世界" + }, + { + "en-US": "Blizzard World Winter", + "id": "Map.BLIZZARD_WORLD_WINTER", + "kind": "enum member", + "sources": [ + "maps.blizzWorldWinter", + "data.maps.blizzWorldWinter" + ], + "zh-CN": "圣诞节暴雪世界" + }, + { + "en-US": "Busan", + "id": "Map.BUSAN", + "kind": "enum member", + "sources": [ + "maps.busan", + "data.maps.busan" + ], + "zh-CN": "釜山" + }, + { + "en-US": "Circuit Royal", + "id": "Map.CIRCUIT_ROYAL", + "kind": "enum member", + "sources": [ + "maps.circuitRoyal", + "data.maps.circuitRoyal" + ], + "zh-CN": "皇家赛道" + }, + { + "en-US": "Colosseo", + "id": "Map.COLOSSEO", + "kind": "enum member", + "sources": [ + "maps.colosseo", + "data.maps.colosseo" + ], + "zh-CN": "斗兽场" + }, + { + "en-US": "Dorado", + "id": "Map.DORADO", + "kind": "enum member", + "sources": [ + "maps.dorado", + "data.maps.dorado" + ], + "zh-CN": "多拉多" + }, + { + "en-US": "Eichenwalde", + "id": "Map.EICHENWALDE", + "kind": "enum member", + "sources": [ + "maps.eichenwalde", + "data.maps.eichenwalde" + ], + "zh-CN": "艾兴瓦尔德" + }, + { + "en-US": "Eichenwalde Halloween", + "id": "Map.EICHENWALDE_HALLOWEEN", + "kind": "enum member", + "sources": [ + "maps.eichenwaldeHalloween", + "data.maps.eichenwaldeHalloween" + ], + "zh-CN": "万圣节艾兴瓦尔德" + }, + { + "en-US": "Esperança", + "id": "Map.ESPERANCA", + "kind": "enum member", + "sources": [ + "maps.esperanca", + "data.maps.esperanca" + ], + "zh-CN": "埃斯佩兰萨" + }, + { + "en-US": "Hanamura", + "id": "Map.HANAMURA", + "kind": "enum member", + "sources": [ + "maps.hanamura", + "data.maps.hanamura" + ], + "zh-CN": "花村" + }, + { + "en-US": "Hanamura Winter", + "id": "Map.HANAMURA_WINTER", + "kind": "enum member", + "sources": [ + "maps.hanamuraWinter", + "data.maps.hanamuraWinter" + ], + "zh-CN": "圣诞节花村" + }, + { + "en-US": "Hanaoka", + "id": "Map.HANAOKA", + "kind": "enum member", + "sources": [ + "maps.hanaoka", + "data.maps.hanaoka" + ], + "zh-CN": "花冈" + }, + { + "en-US": "Havana", + "id": "Map.HAVANA", + "kind": "enum member", + "sources": [ + "maps.havana", + "data.maps.havana" + ], + "zh-CN": "哈瓦那" + }, + { + "en-US": "Hollywood", + "id": "Map.HOLLYWOOD", + "kind": "enum member", + "sources": [ + "maps.hollywood", + "data.maps.hollywood" + ], + "zh-CN": "好莱坞" + }, + { + "en-US": "Hollywood Halloween", + "id": "Map.HOLLYWOOD_HALLOWEEN", + "kind": "enum member", + "sources": [ + "maps.hollywoodHalloween", + "data.maps.hollywoodHalloween" + ], + "zh-CN": "万圣节好莱坞" + }, + { + "en-US": "Horizon Lunar Colony", + "id": "Map.HORIZON_LUNAR_COLONY", + "kind": "enum member", + "sources": [ + "maps.horizonLunarColony", + "data.maps.horizonLunarColony" + ], + "zh-CN": "“地平线”月球基地" + }, + { + "en-US": "Ilios", + "id": "Map.ILIOS", + "kind": "enum member", + "sources": [ + "maps.ilios", + "data.maps.ilios" + ], + "zh-CN": "伊利奥斯" + }, + { + "en-US": "Junkertown", + "id": "Map.JUNKERTOWN", + "kind": "enum member", + "sources": [ + "maps.junkertown", + "data.maps.junkertown" + ], + "zh-CN": "渣客镇" + }, + { + "en-US": "King's Row", + "id": "Map.KINGS_ROW", + "kind": "enum member", + "sources": [ + "maps.kingsRow", + "data.maps.kingsRow" + ], + "zh-CN": "国王大道" + }, + { + "en-US": "King's Row Winter", + "id": "Map.KINGS_ROW_WINTER", + "kind": "enum member", + "sources": [ + "maps.kingsRowWinter", + "data.maps.kingsRowWinter" + ], + "zh-CN": "圣诞节国王大道" + }, + { + "en-US": "Lijiang Tower", + "id": "Map.LIJIANG_TOWER", + "kind": "enum member", + "sources": [ + "maps.lijiangTower", + "data.maps.lijiangTower" + ], + "zh-CN": "漓江塔" + }, + { + "en-US": "Midtown", + "id": "Map.MIDTOWN", + "kind": "enum member", + "sources": [ + "maps.midtown", + "data.maps.midtown" + ], + "zh-CN": "中城" + }, + { + "en-US": "Nepal", + "id": "Map.NEPAL", + "kind": "enum member", + "sources": [ + "maps.nepal", + "data.maps.nepal" + ], + "zh-CN": "尼泊尔" + }, + { + "en-US": "New Junk City", + "id": "Map.NEW_JUNK_CITY", + "kind": "enum member", + "sources": [ + "maps.newJunkCity", + "data.maps.newJunkCity" + ], + "zh-CN": "新渣客城" + }, + { + "en-US": "New Queen Street", + "id": "Map.NEW_QUEEN_STREET", + "kind": "enum member", + "sources": [ + "maps.newQueenStreet", + "data.maps.newQueenStreet" + ], + "zh-CN": "新皇后街" + }, + { + "en-US": "Numbani", + "id": "Map.NUMBANI", + "kind": "enum member", + "sources": [ + "maps.numbani", + "data.maps.numbani" + ], + "zh-CN": "努巴尼" + }, + { + "en-US": "Oasis", + "id": "Map.OASIS", + "kind": "enum member", + "sources": [ + "maps.oasis", + "data.maps.oasis" + ], + "zh-CN": "绿洲城" + }, + { + "en-US": "Paraíso", + "id": "Map.PARAISO", + "kind": "enum member", + "sources": [ + "maps.paraiso", + "data.maps.paraiso" + ], + "zh-CN": "帕拉伊苏" + }, + { + "en-US": "Paris", + "id": "Map.PARIS", + "kind": "enum member", + "sources": [ + "maps.paris", + "data.maps.paris" + ], + "zh-CN": "巴黎" + }, + { + "en-US": "Rialto", + "id": "Map.RIALTO", + "kind": "enum member", + "sources": [ + "maps.rialto", + "data.maps.rialto" + ], + "zh-CN": "里阿尔托" + }, + { + "en-US": "Route 66", + "id": "Map.ROUTE_66", + "kind": "enum member", + "sources": [ + "maps.route66", + "data.maps.route66" + ], + "zh-CN": "66号公路" + }, + { + "en-US": "Runasapi", + "id": "Map.RUNASAPI", + "kind": "enum member", + "sources": [ + "maps.runasapi", + "data.maps.runasapi" + ], + "zh-CN": "鲁纳塞彼" + }, + { + "en-US": "Samoa", + "id": "Map.SAMOA", + "kind": "enum member", + "sources": [ + "maps.samoa", + "data.maps.samoa" + ], + "zh-CN": "萨摩亚" + }, + { + "en-US": "Shambali Monastery", + "id": "Map.SHAMBALI_MONASTERY", + "kind": "enum member", + "sources": [ + "maps.shambaliMonastery", + "data.maps.shambaliMonastery" + ], + "zh-CN": "香巴里寺院" + }, + { + "en-US": "Suravasa", + "id": "Map.SURAVASA", + "kind": "enum member", + "sources": [ + "maps.suravasa", + "data.maps.suravasa" + ], + "zh-CN": "苏拉瓦萨" + }, + { + "en-US": "Temple of Anubis", + "id": "Map.TEMPLE_OF_ANUBIS", + "kind": "enum member", + "sources": [ + "maps.templeOfAnubis", + "data.maps.templeOfAnubis" + ], + "zh-CN": "阿努比斯神殿" + }, + { + "en-US": "Throne of Anubis", + "id": "Map.THRONE_OF_ANUBIS", + "kind": "enum member", + "sources": [ + "maps.throneOfAnubis", + "data.maps.throneOfAnubis" + ], + "zh-CN": "阿努比斯王座" + }, + { + "en-US": "Volskaya Industries", + "id": "Map.VOLSKAYA_INDUSTRIES", + "kind": "enum member", + "sources": [ + "maps.volskaya", + "data.maps.volskaya" + ], + "zh-CN": "沃斯卡娅工业区" + }, + { + "en-US": "Watchpoint: Gibraltar", + "id": "Map.WATCHPOINT_GIBRALTAR", + "kind": "enum member", + "sources": [ + "maps.watchpointGibraltar", + "data.maps.watchpointGibraltar" + ], + "zh-CN": "监测站:直布罗陀" + }, + { + "en-US": "Append To Array", + "id": "Operation.APPEND_TO_ARRAY", + "kind": "enum member", + "sources": [ + "constants.__Operation__.__appendToArray__" + ], + "zh-CN": "添加至数组" + }, + { + "en-US": "Remove From Array By Index", + "id": "Operation.REMOVE_FROM_ARRAY_BY_INDEX", + "kind": "enum member", + "sources": [ + "constants.__Operation__.__removeFromArrayByIndex__" + ], + "zh-CN": "根据索引从数组中移除" + }, + { + "en-US": "Remove From Array By Value", + "id": "Operation.REMOVE_FROM_ARRAY_BY_VALUE", + "kind": "enum member", + "sources": [ + "constants.__Operation__.__removeFromArrayByValue__" + ], + "zh-CN": "根据值从数组中移除" + }, + { + "en-US": "Down", + "id": "Rounding.DOWN", + "kind": "enum member", + "sources": [ + "constants.__Rounding__.__roundDown__" + ], + "zh-CN": "下" + }, + { + "en-US": "Up", + "id": "Rounding.UP", + "kind": "enum member", + "sources": [ + "constants.__Rounding__.__roundUp__" + ], + "zh-CN": "上" + }, + { + "en-US": "Default Visibility", + "id": "SpecVisibility.DEFAULT", + "kind": "enum member", + "sources": [ + "constants.SpecVisibility.DEFAULT" + ], + "zh-CN": "默认可见度" + }, + { + "en-US": "Visible Always", + "id": "SpecVisibility.VISIBLE_ALWAYS", + "kind": "enum member", + "sources": [ + "constants.SpecVisibility.ALWAYS" + ], + "zh-CN": "始终可见" + }, + { + "en-US": "Visible Never", + "id": "SpecVisibility.VISIBLE_NEVER", + "kind": "enum member", + "sources": [ + "constants.SpecVisibility.NEVER" + ], + "zh-CN": "始终不可见" + }, + { + "en-US": "Asleep", + "id": "Status.ASLEEP", + "kind": "enum member", + "sources": [ + "constants.Status.ASLEEP" + ], + "zh-CN": "沉睡" + }, + { + "en-US": "Burning", + "id": "Status.BURNING", + "kind": "enum member", + "sources": [ + "constants.Status.BURNING" + ], + "zh-CN": "燃烧" + }, + { + "en-US": "Frozen", + "id": "Status.FROZEN", + "kind": "enum member", + "sources": [ + "constants.Status.FROZEN" + ], + "zh-CN": "冰冻" + }, + { + "en-US": "Hacked", + "id": "Status.HACKED", + "kind": "enum member", + "sources": [ + "constants.Status.HACKED" + ], + "zh-CN": "被入侵" + }, + { + "en-US": "Invincible", + "id": "Status.INVINCIBLE", + "kind": "enum member", + "sources": [ + "constants.Status.INVINCIBLE" + ], + "zh-CN": "无敌" + }, + { + "en-US": "Knocked Down", + "id": "Status.KNOCKED_DOWN", + "kind": "enum member", + "sources": [ + "constants.Status.KNOCKED_DOWN" + ], + "zh-CN": "击倒" + }, + { + "en-US": "Phased Out", + "id": "Status.PHASED_OUT", + "kind": "enum member", + "sources": [ + "constants.Status.PHASED_OUT" + ], + "zh-CN": "相移" + }, + { + "en-US": "Rooted", + "id": "Status.ROOTED", + "kind": "enum member", + "sources": [ + "constants.Status.ROOTED" + ], + "zh-CN": "定身" + }, + { + "en-US": "Stunned", + "id": "Status.STUNNED", + "kind": "enum member", + "sources": [ + "constants.Status.STUNNED" + ], + "zh-CN": "击晕" + }, + { + "en-US": "Unkillable", + "id": "Status.UNKILLABLE", + "kind": "enum member", + "sources": [ + "constants.Status.UNKILLABLE" + ], + "zh-CN": "无法杀死" + }, + { + "en-US": "All Teams", + "id": "Team.ALL", + "kind": "enum member", + "sources": [ + "constants.TeamLiteral.ALL" + ], + "zh-CN": "所有队伍" + }, + { + "en-US": "Team 1", + "id": "Team.TEAM_1", + "kind": "enum member", + "sources": [ + "constants.TeamLiteral.1" + ], + "zh-CN": "队伍1" + }, + { + "en-US": "Team 2", + "id": "Team.TEAM_2", + "kind": "enum member", + "sources": [ + "constants.TeamLiteral.2" + ], + "zh-CN": "队伍2" + }, + { + "en-US": "Rotation", + "id": "Transform.ROTATION", + "kind": "enum member", + "sources": [ + "constants.Transform.ROTATION" + ], + "zh-CN": "旋转" + }, + { + "en-US": "Rotation And Translation", + "id": "Transform.ROTATION_AND_TRANSLATION", + "kind": "enum member", + "sources": [ + "constants.Transform.ROTATION_AND_TRANSLATION" + ], + "zh-CN": "旋转并转换" + }, + { + "en-US": "Up", + "id": "Vector.UP", + "kind": "enum member", + "sources": [ + "values.Vector.UP" + ], + "zh-CN": "上" + }, + { + "en-US": "Abort When False", + "id": "Wait.ABORT_WHEN_FALSE", + "kind": "enum member", + "sources": [ + "constants.Wait.ABORT_WHEN_FALSE" + ], + "zh-CN": "当为“假”时中止" + }, + { + "en-US": "Ignore Condition", + "id": "Wait.IGNORE_CONDITION", + "kind": "enum member", + "sources": [ + "constants.Wait.IGNORE_CONDITION" + ], + "zh-CN": "无视条件" + }, + { + "en-US": "Ongoing - Each Player", + "id": "eachPlayer", + "kind": "event", + "sources": [ + "other.events.eachPlayer" + ], + "zh-CN": "持续 - 每名玩家" + }, + { + "en-US": "Ongoing - Global", + "id": "global", + "kind": "event", + "sources": [ + "other.events.global" + ], + "zh-CN": "持续 - 全局" + }, + { + "en-US": "Subroutine", + "id": "subroutine", + "kind": "event", + "sources": [ + "other.events.__subroutine__" + ], + "zh-CN": "子程序" + }, + { + "en-US": "Add", + "id": "add", + "kind": "operator", + "sources": [ + "constants.__Operation__.__add__", + "values.__add__" + ], + "zh-CN": "加" + }, + { + "en-US": "Append To Array", + "id": "appendToArray", + "kind": "operator", + "sources": [ + "constants.__Operation__.__appendToArray__", + "values..concat" + ], + "zh-CN": "添加至数组" + }, + { + "en-US": "Divide", + "id": "divide", + "kind": "operator", + "sources": [ + "constants.__Operation__.__divide__", + "values.__divide__" + ], + "zh-CN": "除" + }, + { + "en-US": "Modulo", + "id": "modulo", + "kind": "operator", + "sources": [ + "constants.__Operation__.__modulo__", + "values.__modulo__" + ], + "zh-CN": "余数" + }, + { + "en-US": "Multiply", + "id": "multiply", + "kind": "operator", + "sources": [ + "constants.__Operation__.__multiply__", + "values.__multiply__" + ], + "zh-CN": "乘" + }, + { + "en-US": "Raise To Power", + "id": "raiseToPower", + "kind": "operator", + "sources": [ + "constants.__Operation__.__raiseToPower__", + "values.__raiseToPower__" + ], + "zh-CN": "乘方" + }, + { + "en-US": "Remove From Array", + "id": "removeFromArray", + "kind": "operator", + "sources": [ + "values..exclude" + ], + "zh-CN": "从数组中移除" + }, + { + "en-US": "Subtract", + "id": "subtract", + "kind": "operator", + "sources": [ + "constants.__Operation__.__subtract__", + "values.__subtract__" + ], + "zh-CN": "减" + }, + { + "en-US": "Call Subroutine", + "id": "callSubroutine", + "kind": "structural", + "sources": [ + "actions.__callSubroutine__" + ], + "zh-CN": "调用子程序" + }, + { + "en-US": "Else", + "id": "else", + "kind": "structural", + "sources": [ + "actions.__else__" + ], + "zh-CN": "Else" + }, + { + "en-US": "Else If", + "id": "elseIf", + "kind": "structural", + "sources": [ + "actions.__elif__" + ], + "zh-CN": "Else If" + }, + { + "en-US": "End", + "id": "end", + "kind": "structural", + "sources": [ + "actions.__end__" + ], + "zh-CN": "End" + }, + { + "en-US": "For Global Variable", + "id": "forGlobalVariable", + "kind": "structural", + "sources": [ + "actions.__forGlobalVariable__" + ], + "zh-CN": "For 全局变量" + }, + { + "en-US": "If", + "id": "if", + "kind": "structural", + "sources": [ + "actions.__if__" + ], + "zh-CN": "If" + }, + { + "en-US": "Modify Global Variable", + "id": "modifyGlobalVariable", + "kind": "structural", + "sources": [ + "actions.__modifyGlobalVariable__" + ], + "zh-CN": "修改全局变量" + }, + { + "en-US": "Modify Player Variable", + "id": "modifyPlayerVariable", + "kind": "structural", + "sources": [ + "actions.__modifyPlayerVariable__" + ], + "zh-CN": "修改玩家变量" + }, + { + "en-US": "Set Global Variable", + "id": "setGlobalVariable", + "kind": "structural", + "sources": [ + "actions.__setGlobalVariable__" + ], + "zh-CN": "设置全局变量" + }, + { + "en-US": "Set Player Variable", + "id": "setPlayerVariable", + "kind": "structural", + "sources": [ + "actions.__setPlayerVariable__" + ], + "zh-CN": "设置玩家变量" + }, + { + "en-US": "While", + "id": "while", + "kind": "structural", + "sources": [ + "actions.__while__" + ], + "zh-CN": "While" + }, + { + "en-US": "Ability Icon String", + "id": "abilityIconString", + "kind": "value", + "sources": [ + "values.abilityIconString" + ], + "zh-CN": "技能图标字符串" + }, + { + "en-US": "Absolute Value", + "id": "absoluteValue", + "kind": "value", + "sources": [ + "values.abs" + ], + "zh-CN": "绝对值" + }, + { + "en-US": "Add", + "id": "add", + "kind": "value", + "sources": [ + "values.__add__" + ], + "zh-CN": "加" + }, + { + "en-US": "All Damage Heroes", + "id": "allDamageHeroes", + "kind": "value", + "sources": [ + "values.getDamageHeroes" + ], + "zh-CN": "所有输出英雄" + }, + { + "en-US": "All Heroes", + "id": "allHeroes", + "kind": "value", + "sources": [ + "values.getAllHeroes" + ], + "zh-CN": "全部英雄" + }, + { + "en-US": "All Players", + "id": "allPlayers", + "kind": "value", + "sources": [ + "values.getPlayers" + ], + "zh-CN": "所有玩家" + }, + { + "en-US": "All Support Heroes", + "id": "allSupportHeroes", + "kind": "value", + "sources": [ + "values.getSupportHeroes" + ], + "zh-CN": "所有支援英雄" + }, + { + "en-US": "All Tank Heroes", + "id": "allTankHeroes", + "kind": "value", + "sources": [ + "values.getTankHeroes" + ], + "zh-CN": "所有重装英雄" + }, + { + "en-US": "Allowed Heroes", + "id": "allowedHeroes", + "kind": "value", + "sources": [ + "values..getAllowedHeroes" + ], + "zh-CN": "可用英雄" + }, + { + "en-US": "And", + "id": "and", + "kind": "value", + "sources": [ + "values.__and__" + ], + "zh-CN": "与" + }, + { + "en-US": "Append To Array", + "id": "appendToArray", + "kind": "value", + "sources": [ + "values..concat" + ], + "zh-CN": "添加至数组" + }, + { + "en-US": "Array", + "id": "array", + "kind": "value", + "sources": [ + "values.__array__" + ], + "zh-CN": "数组" + }, + { + "en-US": "Array Contains", + "id": "arrayContains", + "kind": "value", + "sources": [ + "values.__arrayContains__" + ], + "zh-CN": "数组包含" + }, + { + "en-US": "Compare", + "id": "compare", + "kind": "value", + "sources": [ + "values.__compare__" + ], + "zh-CN": "比较" + }, + { + "en-US": "Count Of", + "id": "countOf", + "kind": "value", + "sources": [ + "values.len" + ], + "zh-CN": "数量" + }, + { + "en-US": "Cross Product", + "id": "crossProduct", + "kind": "value", + "sources": [ + "values.crossProduct" + ], + "zh-CN": "矢量积" + }, + { + "en-US": "Current Array Element", + "id": "currentArrayElement", + "kind": "value", + "sources": [ + "values.__currentArrayElement__" + ], + "zh-CN": "当前数组元素" + }, + { + "en-US": "Current Array Index", + "id": "currentArrayIndex", + "kind": "value", + "sources": [ + "values.__currentArrayIndex__" + ], + "zh-CN": "当前数组索引" + }, + { + "en-US": "Current Map", + "id": "currentMap", + "kind": "value", + "sources": [ + "values.__getCurrentMap__" + ], + "zh-CN": "当前地图" + }, + { + "en-US": "Custom Color", + "id": "customColor", + "kind": "value", + "sources": [ + "values.rgb" + ], + "zh-CN": "自定义颜色" + }, + { + "en-US": "Custom String", + "id": "customString", + "kind": "value", + "sources": [ + "values.__customString__" + ], + "zh-CN": "自定义字符串" + }, + { + "en-US": "Direction From Angles", + "id": "directionFromAngles", + "kind": "value", + "sources": [ + "values.directionFromAngles" + ], + "zh-CN": "与此角度的相对方向" + }, + { + "en-US": "Divide", + "id": "divide", + "kind": "value", + "sources": [ + "values.__divide__" + ], + "zh-CN": "除" + }, + { + "en-US": "Empty Array", + "id": "emptyArray", + "kind": "value", + "sources": [ + "values.__emptyArray__" + ], + "zh-CN": "空数组" + }, + { + "en-US": "Evaluate Once", + "id": "evaluateOnce", + "kind": "value", + "sources": [ + "values.evalOnce" + ], + "zh-CN": "单次赋值" + }, + { + "en-US": "Event Player", + "id": "eventPlayer", + "kind": "value", + "sources": [ + "values.eventPlayer" + ], + "zh-CN": "事件玩家" + }, + { + "en-US": "Filtered Array", + "id": "filteredArray", + "kind": "value", + "sources": [ + "values.__filteredArray__" + ], + "zh-CN": "已过滤的数组" + }, + { + "en-US": "First Of", + "id": "firstOf", + "kind": "value", + "sources": [ + "values.__firstOf__" + ], + "zh-CN": "首个" + }, + { + "en-US": "Forward", + "id": "forward", + "kind": "value", + "sources": [ + "values.Vector.FORWARD" + ], + "zh-CN": "前" + }, + { + "en-US": "Health", + "id": "getHealth", + "kind": "value", + "sources": [ + "values..getHealth" + ], + "zh-CN": "生命值" + }, + { + "en-US": "Players Within Radius", + "id": "getPlayersInRadius", + "kind": "value", + "sources": [ + "values.getPlayersInRadius" + ], + "zh-CN": "范围内玩家" + }, + { + "en-US": "Position Of", + "id": "getPosition", + "kind": "value", + "sources": [ + "values..getPosition" + ], + "zh-CN": "所选位置" + }, + { + "en-US": "Throttle Of", + "id": "getThrottle", + "kind": "value", + "sources": [ + "values..getThrottle" + ], + "zh-CN": "阈值" + }, + { + "en-US": "Has Spawned", + "id": "hasSpawned", + "kind": "value", + "sources": [ + "values..hasSpawned" + ], + "zh-CN": "已重生" + }, + { + "en-US": "Hero Icon String", + "id": "heroIconString", + "kind": "value", + "sources": [ + "values.heroIcon" + ], + "zh-CN": "英雄图标字符串" + }, + { + "en-US": "Horizontal Angle From Direction", + "id": "horizontalAngleFromDirection", + "kind": "value", + "sources": [ + "values.horizontalAngleOfDirection" + ], + "zh-CN": "与此方向的水平角度" + }, + { + "en-US": "Icon String", + "id": "iconString", + "kind": "value", + "sources": [ + "values.iconString" + ], + "zh-CN": "图标字符串" + }, + { + "en-US": "If-Then-Else", + "id": "ifThenElse", + "kind": "value", + "sources": [ + "values.__ifThenElse__" + ], + "zh-CN": "If-Then-Else" + }, + { + "en-US": "Index Of Array Value", + "id": "indexOfArrayValue", + "kind": "value", + "sources": [ + "values..index" + ], + "zh-CN": "数组值的索引" + }, + { + "en-US": "Input Binding String", + "id": "inputBindingString", + "kind": "value", + "sources": [ + "values.inputBindingString" + ], + "zh-CN": "输入绑定字符串" + }, + { + "en-US": "Is Alive", + "id": "isAlive", + "kind": "value", + "sources": [ + "values..isAlive" + ], + "zh-CN": "存活" + }, + { + "en-US": "Is Button Held", + "id": "isButtonHeld", + "kind": "value", + "sources": [ + "values..isHoldingButton" + ], + "zh-CN": "按钮被按下" + }, + { + "en-US": "Is Game In Progress", + "id": "isGameInProgress", + "kind": "value", + "sources": [ + "values.isGameInProgress" + ], + "zh-CN": "游戏正在进行中" + }, + { + "en-US": "Is In Spawn Room", + "id": "isInSpawnRoom", + "kind": "value", + "sources": [ + "values..isInSpawnRoom" + ], + "zh-CN": "在重生室中" + }, + { + "en-US": "Is True For All", + "id": "isTrueForAll", + "kind": "value", + "sources": [ + "values.__all__" + ], + "zh-CN": "对全部为“真”" + }, + { + "en-US": "Is Waiting For Players", + "id": "isWaitingForPlayers", + "kind": "value", + "sources": [ + "values.isWaitingForPlayers" + ], + "zh-CN": "正在等待玩家" + }, + { + "en-US": "Last Created Entity", + "id": "lastCreatedEntity", + "kind": "value", + "sources": [ + "values.getLastCreatedEntity" + ], + "zh-CN": "最后创建的实体" + }, + { + "en-US": "Last Of", + "id": "lastOf", + "kind": "value", + "sources": [ + "values..last" + ], + "zh-CN": "最后" + }, + { + "en-US": "Last Text ID", + "id": "lastTextId", + "kind": "value", + "sources": [ + "values.getLastCreatedText" + ], + "zh-CN": "上一个文本ID" + }, + { + "en-US": "Local Player", + "id": "localPlayer", + "kind": "value", + "sources": [ + "values.localPlayer" + ], + "zh-CN": "本地玩家" + }, + { + "en-US": "Mapped Array", + "id": "mappedArray", + "kind": "value", + "sources": [ + "values.__mappedArray__" + ], + "zh-CN": "映射的数组" + }, + { + "en-US": "Max", + "id": "max", + "kind": "value", + "sources": [ + "values.max" + ], + "zh-CN": "较大" + }, + { + "en-US": "Min", + "id": "min", + "kind": "value", + "sources": [ + "values.min" + ], + "zh-CN": "较小" + }, + { + "en-US": "Multiply", + "id": "multiply", + "kind": "value", + "sources": [ + "values.__multiply__" + ], + "zh-CN": "乘" + }, + { + "en-US": "Not", + "id": "not", + "kind": "value", + "sources": [ + "values.__not__" + ], + "zh-CN": "非" + }, + { + "en-US": "Number Of Players", + "id": "numberOfPlayers", + "kind": "value", + "sources": [ + "values.getNumberOfPlayers" + ], + "zh-CN": "玩家数量" + }, + { + "en-US": "Opposite Team Of", + "id": "oppositeTeamOf", + "kind": "value", + "sources": [ + "values.getOppositeTeam" + ], + "zh-CN": "对方队伍" + }, + { + "en-US": "Or", + "id": "or", + "kind": "value", + "sources": [ + "values.__or__" + ], + "zh-CN": "或" + }, + { + "en-US": "Random Real", + "id": "randomReal", + "kind": "value", + "sources": [ + "values.random.uniform" + ], + "zh-CN": "随机实数" + }, + { + "en-US": "Random Value In Array", + "id": "randomValueInArray", + "kind": "value", + "sources": [ + "values.random.choice" + ], + "zh-CN": "数组随机取值" + }, + { + "en-US": "Remove From Array", + "id": "removeFromArray", + "kind": "value", + "sources": [ + "values..exclude" + ], + "zh-CN": "从数组中移除" + }, + { + "en-US": "Round To Integer", + "id": "roundToInteger", + "kind": "value", + "sources": [ + "values.__round__" + ], + "zh-CN": "取整" + }, + { + "en-US": "Sorted Array", + "id": "sortedArray", + "kind": "value", + "sources": [ + "values.__sortedArray__" + ], + "zh-CN": "已排序的数组" + }, + { + "en-US": "Square Root", + "id": "squareRoot", + "kind": "value", + "sources": [ + "values.sqrt" + ], + "zh-CN": "平方根" + }, + { + "en-US": "String Replace", + "id": "stringReplace", + "kind": "value", + "sources": [ + "values..replace" + ], + "zh-CN": "字符串替换" + }, + { + "en-US": "String Slice", + "id": "stringSlice", + "kind": "value", + "sources": [ + "values..substring" + ], + "zh-CN": "截取字符串" + }, + { + "en-US": "String Split", + "id": "stringSplit", + "kind": "value", + "sources": [ + "values..split" + ], + "zh-CN": "字符串分割" + }, + { + "en-US": "Subtract", + "id": "subtract", + "kind": "value", + "sources": [ + "values.__subtract__" + ], + "zh-CN": "减" + }, + { + "en-US": "Team Of", + "id": "teamOf", + "kind": "value", + "sources": [ + "values..getTeam" + ], + "zh-CN": "所在队伍" + }, + { + "en-US": "Update Every Frame", + "id": "updateEveryFrame", + "kind": "value", + "sources": [ + "values.updateEveryFrame" + ], + "zh-CN": "逐帧更新" + }, + { + "en-US": "Value In Array", + "id": "valueInArray", + "kind": "value", + "sources": [ + "values.__valueInArray__" + ], + "zh-CN": "数组中的值" + }, + { + "en-US": "Vector", + "id": "vector", + "kind": "value", + "sources": [ + "values.vect" + ], + "zh-CN": "矢量" + }, + { + "en-US": "Vertical Angle From Direction", + "id": "verticalAngleFromDirection", + "kind": "value", + "sources": [ + "values.verticalAngleOfDirection" + ], + "zh-CN": "与此方向的垂直角度" + }, + { + "en-US": "Workshop Setting Combo", + "id": "workshopSettingCombo", + "kind": "value", + "sources": [ + "values.createWorkshopSettingEnum" + ], + "zh-CN": "地图工坊设置组合" + }, + { + "en-US": "Workshop Setting Integer", + "id": "workshopSettingInteger", + "kind": "value", + "sources": [ + "values.createWorkshopSettingInt" + ], + "zh-CN": "地图工坊设置整数" + }, + { + "en-US": "Workshop Setting Toggle", + "id": "workshopSettingToggle", + "kind": "value", + "sources": [ + "values.createWorkshopSettingBool" + ], + "zh-CN": "地图工坊设置开关" + }, + { + "en-US": "World Vector Of", + "id": "worldVector", + "kind": "value", + "sources": [ + "values.worldVector" + ], + "zh-CN": "地图矢量" + } + ], + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), zh-CN taken from the same export entry; entries without an exact match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "schemaVersion": 1, + "source": { + "commit": "d854bf01fc7bbf3b2169f67408c07a8da8989ad6", + "commitDate": "2026-08-12T15:26:38Z", + "export": "workshop-data.json", + "fetchedAt": "2026-08-17T02:52:59Z" + } +} From 38ba22ccb407e05a58b4313c4e26b74dd2928964 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 12:42:23 +0800 Subject: [PATCH 2/6] fix(corpus): include custom settings label sources Index the export paths that carry the General mode and team labels, and pin their zh-CN coverage in the corpus test. Keep the remaining exact-match exclusions and release-gate blockers explicit. Refs #2 --- .../src/bin/workshop-catalog-gen.rs | 7 +++-- .../workshop-rs/src/settings/data/zh-cn.json | 31 +++++++++++-------- crates/workshop-rs/tests/corpus.rs | 17 ++++++++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs index c484f31..f59641f 100644 --- a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs +++ b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs @@ -827,10 +827,13 @@ mod corpus { ); index }; - let gamemodes = localized_index(export, &["gamemodes."]); + let gamemodes = localized_index(export, &["gamemodes.", "customGameSettings.gamemodes."]); let maps = localized_index(export, &["maps."]); let heroes = localized_index(export, &["heroes."]); - let teams = localized_index(export, &["heroes.teams."]); + let teams = localized_index( + export, + &["heroes.teams.", "customGameSettings.heroes.teams."], + ); let tokens = localized_index(export, &["other.customGameSettings."]); let surface = settings_surface(); diff --git a/crates/workshop-rs/src/settings/data/zh-cn.json b/crates/workshop-rs/src/settings/data/zh-cn.json index a0714eb..dc3dba7 100644 --- a/crates/workshop-rs/src/settings/data/zh-cn.json +++ b/crates/workshop-rs/src/settings/data/zh-cn.json @@ -17,11 +17,11 @@ "total": 2 }, "modes": { - "matched": 6, + "matched": 7, "total": 7 }, "teams": { - "matched": 0, + "matched": 1, "total": 1 }, "tokens": { @@ -55,16 +55,6 @@ "en-US": "Ultimate Generation - Combat Blizzard", "reason": "no exact en-US match in the export", "surface": "heroes...combatUltGen%" - }, - { - "en-US": "General", - "reason": "no exact en-US match in the export", - "surface": "mode.general.name" - }, - { - "en-US": "General", - "reason": "no exact en-US match in the export", - "surface": "team.allTeams.name" } ], "heroes": { @@ -320,6 +310,13 @@ ], "zh-CN": "运载目标" }, + "General": { + "en-US": "General", + "sources": [ + "customGameSettings.heroes.teams.allTeams" + ], + "zh-CN": "综合" + }, "Hybrid": { "en-US": "Hybrid", "sources": [ @@ -346,7 +343,15 @@ "source": "user-provided workshop-data export (workshop-data.json)" }, "schemaVersion": 1, - "teams": {}, + "teams": { + "General": { + "en-US": "General", + "sources": [ + "customGameSettings.heroes.teams.allTeams" + ], + "zh-CN": "综合" + } + }, "tokens": { "Off": { "en-US": "Off", diff --git a/crates/workshop-rs/tests/corpus.rs b/crates/workshop-rs/tests/corpus.rs index f6775ff..2e00b97 100644 --- a/crates/workshop-rs/tests/corpus.rs +++ b/crates/workshop-rs/tests/corpus.rs @@ -47,6 +47,23 @@ fn manifest_pins_the_export_and_exact_match_coverage() { assert_eq!(manifest["excluded"].as_array().unwrap().len(), 17); } +#[test] +fn settings_corpus_includes_general_mode_and_team_labels() { + let settings: serde_json::Value = + serde_json::from_str(include_str!("../src/settings/data/zh-cn.json")) + .expect("generated settings corpus is valid JSON"); + assert_eq!( + settings["coverage"]["modes"], + serde_json::json!({"matched": 7, "total": 7}) + ); + assert_eq!( + settings["coverage"]["teams"], + serde_json::json!({"matched": 1, "total": 1}) + ); + assert_eq!(settings["modes"]["General"]["zh-CN"], "综合"); + assert_eq!(settings["teams"]["General"]["zh-CN"], "综合"); +} + #[test] fn representative_corpus_converts_in_both_directions() { let catalog = catalog(); From c6b4cc1ee39fd3c413fe12ec274a78d8b9d049ef Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 13:47:59 +0800 Subject: [PATCH 3/6] feat(workshop): parse settings in raw Workshop Complete settings-bearing raw Workshop parse, canonical round-trip, and reviewed en-US/zh-CN conversion evidence. Refs #2 --- README.md | 6 +- crates/workshop-rs/src/emitter.rs | 3 +- crates/workshop-rs/src/lexer.rs | 2 +- crates/workshop-rs/src/parser.rs | 352 +++++++++++++++++- crates/workshop-rs/src/roundtrip.rs | 98 ++++- crates/workshop-rs/tests/emitter.rs | 10 +- crates/workshop-rs/tests/fixtures/README.md | 1 + .../settings/pixelart.zh-CN.settings.ws | 41 ++ crates/workshop-rs/tests/settings_pipeline.rs | 81 ++++ docs/provenance.md | 4 +- 10 files changed, 574 insertions(+), 24 deletions(-) create mode 100644 crates/workshop-rs/tests/fixtures/settings/pixelart.zh-CN.settings.ws create mode 100644 crates/workshop-rs/tests/settings_pipeline.rs diff --git a/README.md b/README.md index 01658bf..bc9537d 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ let out = convert(text, &catalog, &Locale::new("en-US"), &Locale::new("zh-CN"), let identity = catalog.identity(); ``` -Note: settings-bearing programs cannot be parsed from raw text (a `.ws` -decompiler is a non-goal); settings are carried in WIR and emitted by the -library. Locale detection is available via `workshop_rs::detect`. +Settings-bearing programs are parsed into the canonical WIR settings carrier +and emitted by the library. Locale detection is available via +`workshop_rs::detect`. ## CLI usage diff --git a/crates/workshop-rs/src/emitter.rs b/crates/workshop-rs/src/emitter.rs index 43dbee4..1e7ca9c 100644 --- a/crates/workshop-rs/src/emitter.rs +++ b/crates/workshop-rs/src/emitter.rs @@ -186,7 +186,8 @@ impl Emitter<'_> { ) }); let header = if disabled { - format!("disabled {display}") + let disabled_name = self.setting_name("tokens", "disabled", "token.disabled")?; + format!("{disabled_name} {display}") } else { display }; diff --git a/crates/workshop-rs/src/lexer.rs b/crates/workshop-rs/src/lexer.rs index 7293bd8..746ec72 100644 --- a/crates/workshop-rs/src/lexer.rs +++ b/crates/workshop-rs/src/lexer.rs @@ -303,7 +303,7 @@ pub fn tokenize(input: &str) -> Result, LexError> { let interior_dash = c == '-' && index + 1 < chars.len() && (chars[index + 1].is_alphanumeric() || chars[index + 1] == '_'); - if c.is_alphanumeric() || c == '_' || interior_dash { + if c.is_alphanumeric() || c == '_' || c == '\'' || interior_dash { word.push(c); advance(&mut index, &mut line, &mut col, &chars); } else { diff --git a/crates/workshop-rs/src/parser.rs b/crates/workshop-rs/src/parser.rs index c79624e..3e9b673 100644 --- a/crates/workshop-rs/src/parser.rs +++ b/crates/workshop-rs/src/parser.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; +use crate::settings::table::{self, KeyKind, PathPart}; +use crate::settings::{Settings, SettingsListElement, SettingsNode}; use crate::signatures::{ExpectedDomain, NoExpectedDomain}; use crate::source::{Position, SourceFile, Span}; use crate::wir::{self, Action, Event, ModifyOp, Value, ValueNode}; @@ -113,6 +115,7 @@ impl Parser<'_> { None => break, }; match phrase.as_str() { + "settings" => self.settings_section()?, "variables" => self.variables_section()?, "subroutines" => self.subroutines_section()?, "rule" => self.rule()?, @@ -124,6 +127,306 @@ impl Parser<'_> { Ok(self.target) } + fn settings_section(&mut self) -> Result<()> { + let start = self.expect_word("settings")?; + self.expect(TokenKind::LBrace, "expected '{' after 'settings'")?; + let mut children = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let (name, child_start, _) = self.phrase()?; + self.expect(TokenKind::LBrace, "expected '{' after settings group")?; + let node = match name.as_str() { + "main" | "lobby" => SettingsNode::Group { + name: name.clone(), + children: self.settings_members(&[PathPart::Part(if name == "main" { + "main" + } else { + "lobby" + })])?, + span: Some(self.settings_span(child_start)), + }, + "modes" => self.settings_modes(child_start)?, + "heroes" => self.settings_heroes(child_start)?, + _ => return Err(self.unknown("settings group", &name)), + }; + children.push(node); + } + let end = match self.next() { + Some(Token { + kind: TokenKind::RBrace, + end, + .. + }) => end, + _ => unreachable!("settings loop checks for closing brace"), + }; + self.target.settings = Some(Settings { + span: Some(Span::new(self.file(), start, end)), + children, + }); + Ok(()) + } + + fn settings_modes(&mut self, start: Position) -> Result { + let mut children = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let mut disabled = false; + if let Some(Token { + kind: TokenKind::Word(word), + .. + }) = self.peek() + { + if self.settings_name_matches("tokens", "disabled", &word) { + self.pos += 1; + disabled = true; + } + } + let (display, mode_start, _) = self.phrase_on_line()?; + let mode = self.resolve_settings_name(table::MODE_NAMES, "modes", &display)?; + self.expect(TokenKind::LBrace, "expected '{' after game mode")?; + let mut mode_children = + self.settings_members(&[PathPart::Part("gamemodes"), PathPart::Part(mode)])?; + if disabled { + mode_children.insert( + 0, + SettingsNode::Bool { + name: "enabled".to_string(), + value: false, + span: None, + }, + ); + } + children.push(SettingsNode::Group { + name: mode.to_string(), + children: mode_children, + span: Some(self.settings_span(mode_start)), + }); + } + self.expect(TokenKind::RBrace, "expected '}' after modes")?; + Ok(SettingsNode::Group { + name: "gamemodes".to_string(), + children, + span: Some(self.settings_span(start)), + }) + } + + fn settings_heroes(&mut self, start: Position) -> Result { + let mut teams = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let (team_display, team_start, _) = self.phrase_on_line()?; + let team = self.resolve_settings_name(table::TEAM_NAMES, "teams", &team_display)?; + self.expect(TokenKind::LBrace, "expected '{' after team settings group")?; + let mut team_children = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let (display, child_start, child_end) = self.phrase_on_line()?; + if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) + && self + .resolve_settings_name(table::HERO_NAMES, "heroes", &display) + .is_ok() + { + let hero = self.resolve_settings_name(table::HERO_NAMES, "heroes", &display)?; + self.expect(TokenKind::LBrace, "expected '{' after hero settings group")?; + let children = self.settings_members(&[ + PathPart::Part("heroes"), + PathPart::Team, + PathPart::Hero, + ])?; + team_children.push(SettingsNode::Group { + name: hero.to_string(), + children, + span: Some(self.settings_span(child_start)), + }); + } else { + team_children.push(self.settings_member_named( + display, + child_start, + child_end, + &[PathPart::Part("heroes"), PathPart::Team], + )?); + } + } + self.expect(TokenKind::RBrace, "expected '}' after team settings group")?; + teams.push(SettingsNode::Group { + name: team.to_string(), + children: team_children, + span: Some(self.settings_span(team_start)), + }); + } + self.expect(TokenKind::RBrace, "expected '}' after heroes")?; + Ok(SettingsNode::Group { + name: "heroes".to_string(), + children: teams, + span: Some(self.settings_span(start)), + }) + } + + fn settings_members(&mut self, path: &[PathPart<'static>]) -> Result> { + let mut children = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let (display, start, end) = self.phrase_on_line()?; + children.push(self.settings_member_named(display, start, end, path)?); + } + self.expect(TokenKind::RBrace, "expected '}' after settings group")?; + Ok(children) + } + + fn settings_member_named( + &mut self, + display: String, + start: Position, + _end: Position, + path: &[PathPart<'static>], + ) -> Result { + let entry = table::ENTRIES.iter().find(|candidate| { + candidate.path.len() == path.len() + 1 + && candidate.path[..path.len()] + .iter() + .zip(path.iter()) + .all(|(left, right)| left == right) + && self.settings_name_matches("labels", candidate.workshop_name, &display) + }); + let Some(entry) = entry else { + return Err(self.unknown("setting", &display)); + }; + let name = match entry.path.last() { + Some(PathPart::Part(name)) => *name, + _ => return Err(self.malformed("settings entry has no leaf key", self.previous())), + }; + if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) { + self.expect(TokenKind::LBrace, "expected '{' after settings list")?; + let mut elements = Vec::new(); + while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) { + let (value, value_start, value_end) = self.phrase_on_line()?; + let canonical = match entry.kind { + KeyKind::ListMap => { + self.resolve_settings_name(table::MAP_NAMES, "maps", &value)? + } + KeyKind::ListHero => { + self.resolve_settings_name(table::HERO_NAMES, "heroes", &value)? + } + _ => { + return Err( + self.malformed("only settings lists may use braces", self.previous()) + ); + } + }; + elements.push(SettingsListElement { + value: canonical.to_string(), + span: Some(Span::new(self.file(), value_start, value_end)), + }); + } + self.expect(TokenKind::RBrace, "expected '}' after settings list")?; + return Ok(SettingsNode::List { + name: name.to_string(), + elements, + span: Some(Span::new(self.file(), start, self.previous_span().1)), + }); + } + self.expect(TokenKind::Colon, "expected ':' after settings key")?; + let end = self.previous_span().1; + let span = Some(Span::new(self.file(), start, end)); + match entry.kind { + KeyKind::String => Ok(SettingsNode::String { + name: name.to_string(), + value: self.expect_string("expected a settings string")?, + span, + }), + KeyKind::Number => Ok(SettingsNode::Number { + name: name.to_string(), + value: self.settings_number(false)?, + span, + }), + KeyKind::Percent => Ok(SettingsNode::Number { + name: name.to_string(), + value: self.settings_number(true)?, + span, + }), + KeyKind::Bool => Ok(SettingsNode::Bool { + name: name.to_string(), + value: self.settings_bool()?, + span, + }), + KeyKind::Enum(domain) => Ok(SettingsNode::String { + name: name.to_string(), + value: self.resolve_enum_settings_name(domain)?, + span, + }), + KeyKind::ListMap | KeyKind::ListHero => { + Err(self.malformed("settings list requires a brace block", self.previous())) + } + } + } + + fn settings_number(&mut self, percent: bool) -> Result { + let value = match self.next() { + Some(Token { + kind: TokenKind::Number { value, .. }, + .. + }) => value, + Some(token) => return Err(self.malformed("expected a settings number", &token)), + None => return Err(self.malformed("expected a settings number", self.eof())), + }; + if percent { + self.expect( + TokenKind::Op("%".to_string()), + "expected '%' after settings percentage", + )?; + } + Ok(value) + } + + fn settings_bool(&mut self) -> Result { + let token = self + .next() + .ok_or_else(|| self.malformed("expected a settings boolean", self.eof()))?; + let TokenKind::Word(value) = token.kind else { + return Err(self.malformed("expected a settings boolean", &token)); + }; + if self.settings_name_matches("tokens", "On", &value) { + Ok(true) + } else if self.settings_name_matches("tokens", "Off", &value) { + Ok(false) + } else { + Err(self.unknown("setting boolean", &value)) + } + } + + fn resolve_enum_settings_name(&mut self, domain: &str) -> Result { + let (display, _, _) = self.phrase_on_line()?; + table::ENUM_MEMBERS + .iter() + .find(|member| { + member.domain == domain + && self.settings_name_matches("enums", member.name, &display) + }) + .map(|member| member.member.to_string()) + .ok_or_else(|| self.unknown("settings enum", &display)) + } + + fn resolve_settings_name( + &self, + names: &[table::NameMap], + section: &str, + display: &str, + ) -> Result<&'static str> { + names + .iter() + .find(|candidate| self.settings_name_matches(section, candidate.name, display)) + .map(|candidate| candidate.key) + .ok_or_else(|| self.unknown("setting", display)) + } + + fn settings_name_matches(&self, section: &str, english: &str, display: &str) -> bool { + if self.locale == Locale::new("en-US") { + display == english + } else { + table::localized_name(self.locale.as_str(), section, english) + .is_some_and(|localized| localized == display) + } + } + + fn settings_span(&self, start: Position) -> Span { + Span::new(self.file(), start, self.previous_span().1) + } + fn variables_section(&mut self) -> Result<()> { self.expect_word("variables")?; self.expect(TokenKind::LBrace, "expected '{' after 'variables'")?; @@ -1312,6 +1615,14 @@ impl Parser<'_> { words.push(word.clone()); (start, end) } + Some(Token { + kind: TokenKind::Number { text, .. }, + start, + end, + }) => { + words.push(text.clone()); + (start, end) + } Some(token) => return Err(self.malformed("expected an identifier", &token)), None => return Err(self.malformed("expected an identifier", self.eof())), }; @@ -1358,24 +1669,49 @@ impl Parser<'_> { words.push(word.clone()); (start, end, start.line) } + Some(Token { + kind: TokenKind::Number { text, .. }, + start, + end, + }) => { + words.push(text.clone()); + (start, end, start.line) + } Some(token) => return Err(self.malformed("expected an identifier", &token)), None => return Err(self.malformed("expected an identifier", self.eof())), }; self.pos += 1; - while let Some(Token { - kind: TokenKind::Word(word), - start, - end: word_end, - }) = self.peek() - { - if start.line != line { + while let Some(token) = self.peek() { + let (word, word_start, word_end) = match token { + Token { + kind: TokenKind::Word(word), + start, + end, + } => (word, start, end), + Token { + kind: TokenKind::Number { text, .. }, + start, + end, + } => (text, start, end), + Token { + kind: TokenKind::Dot, + start, + end, + } => (".".to_string(), start, end), + _ => break, + }; + if word_start.line != line { break; } words.push(word); end = word_end; self.pos += 1; } - Ok((words.join(" "), start, end)) + Ok(( + words.join(" ").replace(" .", ".").replace(". ", "."), + start, + end, + )) } /// Read a text line (tokens until `;`), joining words and dashes into diff --git a/crates/workshop-rs/src/roundtrip.rs b/crates/workshop-rs/src/roundtrip.rs index 33591b6..0cd54a1 100644 --- a/crates/workshop-rs/src/roundtrip.rs +++ b/crates/workshop-rs/src/roundtrip.rs @@ -95,9 +95,12 @@ pub fn round_trip_with_context( record } -/// Structural equivalence of two WIR programs: identical tables, rules, -/// actions, and values, ignoring source spans and file paths. +/// Structural equivalence of two WIR programs: identical settings, tables, +/// rules, actions, and values, ignoring source spans and file paths. pub fn equivalent(a: &wir::Program, b: &wir::Program) -> bool { + if !settings_equivalent(a.settings.as_ref(), b.settings.as_ref()) { + return false; + } let globals_a: Vec<_> = a .global_variables .iter() @@ -148,6 +151,97 @@ pub fn equivalent(a: &wir::Program, b: &wir::Program) -> bool { true } +fn settings_equivalent( + left: Option<&crate::settings::Settings>, + right: Option<&crate::settings::Settings>, +) -> bool { + match (left, right) { + (None, None) => true, + (Some(left), Some(right)) => nodes_equivalent(&left.children, &right.children), + _ => false, + } +} + +fn nodes_equivalent( + left: &[crate::settings::SettingsNode], + right: &[crate::settings::SettingsNode], +) -> bool { + left.len() == right.len() + && left + .iter() + .zip(right) + .all(|(left, right)| match (left, right) { + ( + crate::settings::SettingsNode::Group { + name: left_name, + children: left_children, + .. + }, + crate::settings::SettingsNode::Group { + name: right_name, + children: right_children, + .. + }, + ) => left_name == right_name && nodes_equivalent(left_children, right_children), + ( + crate::settings::SettingsNode::Number { + name: left_name, + value: left_value, + .. + }, + crate::settings::SettingsNode::Number { + name: right_name, + value: right_value, + .. + }, + ) => left_name == right_name && left_value == right_value, + ( + crate::settings::SettingsNode::Bool { + name: left_name, + value: left_value, + .. + }, + crate::settings::SettingsNode::Bool { + name: right_name, + value: right_value, + .. + }, + ) => left_name == right_name && left_value == right_value, + ( + crate::settings::SettingsNode::String { + name: left_name, + value: left_value, + .. + }, + crate::settings::SettingsNode::String { + name: right_name, + value: right_value, + .. + }, + ) => left_name == right_name && left_value == right_value, + ( + crate::settings::SettingsNode::List { + name: left_name, + elements: left_elements, + .. + }, + crate::settings::SettingsNode::List { + name: right_name, + elements: right_elements, + .. + }, + ) => { + left_name == right_name + && left_elements.len() == right_elements.len() + && left_elements + .iter() + .zip(right_elements) + .all(|(left, right)| left.value == right.value) + } + _ => false, + }) +} + fn rule_equivalent( a: &wir::Program, b: &wir::Program, diff --git a/crates/workshop-rs/tests/emitter.rs b/crates/workshop-rs/tests/emitter.rs index 1e2b223..d9defd9 100644 --- a/crates/workshop-rs/tests/emitter.rs +++ b/crates/workshop-rs/tests/emitter.rs @@ -416,16 +416,12 @@ fn settings_free_program_emits_no_settings_section() { } #[test] -fn settings_emission_is_rejected_by_the_workshop_parser() { - // Roundtrip boundary: the ws parser never learns the settings section, - // so a settings-bearing emission cannot reparse. +fn settings_emission_reparses_into_equivalent_wir() { let program = program_with_settings(pixelart_settings()); let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); assert!(emitted.starts_with("settings {")); - assert!( - parser::parse(&emitted, &catalog(), &en()).is_err(), - "settings-bearing emission must be rejected by the ws parser" - ); + let reparsed = parser::parse(&emitted, &catalog(), &en()).expect("settings reparses"); + assert!(workshop_rs::roundtrip::equivalent(&program, &reparsed)); } #[test] diff --git a/crates/workshop-rs/tests/fixtures/README.md b/crates/workshop-rs/tests/fixtures/README.md index c3cc16b..5788a69 100644 --- a/crates/workshop-rs/tests/fixtures/README.md +++ b/crates/workshop-rs/tests/fixtures/README.md @@ -18,6 +18,7 @@ core migration (Issue #2). | `corpus/overpy-cake.ws` | wright `compatibility/fixtures/real-world/overpy-cake/oracle.json` `compile.workshop` | OverPy 9.7.10, en-US emission | | `settings/pixelart.settings.ws` | wright `compatibility/fixtures/real-world/overpy-pixelart/oracle.json` `compile.workshop` settings section | OverPy 9.7.10, en-US emission | | `settings/santa.settings.ws` | wright `compatibility/fixtures/real-world/overpy-santa/oracle.json` `compile.workshop` settings section | OverPy 9.7.10, en-US emission | +| `settings/pixelart.zh-CN.settings.ws` | deterministic conversion of `settings/pixelart.settings.ws` through the reviewed PR #9 locale corpus | reviewed `zh-CN` mappings, no fallback | The original oracle snapshots record: OverPy version 9.7.10 (npm `overpy@9.7.10`, git head `1e2688954302a402d076944b46db07efb14d7b61`, diff --git a/crates/workshop-rs/tests/fixtures/settings/pixelart.zh-CN.settings.ws b/crates/workshop-rs/tests/fixtures/settings/pixelart.zh-CN.settings.ws new file mode 100644 index 0000000..9ebe250 --- /dev/null +++ b/crates/workshop-rs/tests/fixtures/settings/pixelart.zh-CN.settings.ws @@ -0,0 +1,41 @@ +settings +{ + modes + { + 攻防作战 + { + 启用地图 + { + } + 职责限制: 每队同一职责最多2名 + } + 占领要点 + { + 启用地图 + { + } + 职责限制: 每队同一职责最多2名 + } + 运载目标 + { + 启用地图 + { + } + 职责限制: 每队同一职责最多2名 + } + 攻击护送 + { + 启用地图 + { + } + 职责限制: 每队同一职责最多2名 + } + 突击模式 + { + 启用地图 + { + 地图工坊岛屿 + } + } + } +} diff --git a/crates/workshop-rs/tests/settings_pipeline.rs b/crates/workshop-rs/tests/settings_pipeline.rs new file mode 100644 index 0000000..61c1321 --- /dev/null +++ b/crates/workshop-rs/tests/settings_pipeline.rs @@ -0,0 +1,81 @@ +//! End-to-end raw Workshop settings coverage using the reviewed en-US and +//! zh-CN settings mappings. + +use std::path::{Path, PathBuf}; + +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::{convert, emitter, parser, roundtrip}; + +fn fixture_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/settings") + .join(name) +} + +fn fixture(name: &str) -> String { + std::fs::read_to_string(fixture_path(name)).expect("settings fixture") +} + +fn collapse(text: &str) -> String { + text.chars() + .filter(|character| !character.is_whitespace()) + .collect() +} + +fn catalog() -> Catalog { + Catalog::builtin().expect("builtin catalog") +} + +#[test] +fn real_settings_fixture_parses_to_wir_and_reemits() { + let catalog = catalog(); + let source = fixture("pixelart.settings.ws"); + let program = parser::parse(&source, &catalog, &Locale::new("en-US")).expect("parses"); + assert!(program.settings.is_some(), "settings are carried in WIR"); + + let emitted = emitter::emit(&program, &catalog, &Locale::new("en-US")).expect("emits"); + assert_eq!(collapse(&emitted), collapse(&source)); + let reparsed = parser::parse(&emitted, &catalog, &Locale::new("en-US")).expect("reparses"); + assert!(roundtrip::equivalent(&program, &reparsed)); +} + +#[test] +fn reviewed_settings_conversion_round_trips_en_us_and_zh_cn() { + let catalog = catalog(); + let en = Locale::new("en-US"); + let zh = Locale::new("zh-CN"); + let source = fixture("pixelart.settings.ws"); + let expected_zh = fixture("pixelart.zh-CN.settings.ws"); + + let to_zh = convert::convert(&source, &catalog, &en, &zh, &Default::default()) + .expect("en-US -> zh-CN settings conversion"); + assert!(to_zh.fallback_ids.is_empty()); + assert_eq!(collapse(&to_zh.text), collapse(&expected_zh)); + + let zh_program = parser::parse(&to_zh.text, &catalog, &zh).expect("zh-CN settings parse"); + let en_program = parser::parse(&source, &catalog, &en).expect("en-US settings parse"); + assert!(roundtrip::equivalent(&en_program, &zh_program)); + + let back_to_en = convert::convert(&expected_zh, &catalog, &zh, &en, &Default::default()) + .expect("zh-CN -> en-US settings conversion"); + assert_eq!(collapse(&back_to_en.text), collapse(&source)); +} + +#[test] +fn supported_apostrophe_map_name_parses() { + let catalog = catalog(); + let source = "settings { modes { Deathmatch { enabled maps { King's Row Winter } } } }"; + let program = parser::parse(source, &catalog, &Locale::new("en-US")).expect("parses"); + let emitted = emitter::emit(&program, &catalog, &Locale::new("en-US")).expect("emits"); + assert!(emitted.contains("King's Row Winter")); +} + +#[test] +fn supported_dva_name_parses() { + let catalog = catalog(); + let source = + "settings {\n heroes {\n General {\n D.Va {\n Primary Fire: Off\n }\n }\n }\n}"; + let program = parser::parse(source, &catalog, &Locale::new("en-US")).expect("parses"); + let emitted = emitter::emit(&program, &catalog, &Locale::new("en-US")).expect("emits"); + assert!(emitted.contains("D.Va")); +} diff --git a/docs/provenance.md b/docs/provenance.md index da6d0ae..9106162 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -46,8 +46,8 @@ license, reviewed) is embedded in the dataset itself and surfaced by `workshop-data/workshop-data.json` export at commit `d854bf01fc7bbf3b2169f67408c07a8da8989ad6`, commit date 2026-08-12, fetched 2026-08-17. The export is not committed to this repository. -* The generated settings corpus covers labels 17/19, modes 6/7, maps 2/2, - heroes 10/10, enum values 2/2, tokens 3/3, and teams 0/1. Its exact-match +* The generated settings corpus covers labels 17/19, modes 7/7, maps 2/2, + heroes 10/10, enum values 2/2, tokens 3/3, and teams 1/1. Its exact-match exclusions are recorded in `crates/workshop-rs/src/settings/data/zh-cn.json`; settings without a mapping continue to fail explicitly. The data's license review remains From 1a71af5cc8fd31276f6672c6b6ebbf5ff07aeb73 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 14:26:18 +0800 Subject: [PATCH 4/6] feat(corpus): accept confirmed setAllowedHeroes mapping Update the workshop-rs-owned zh-CN corpus from 327/344 to 328/344 using the user-confirmed JSON identity mapping, and remove stale pending source-review metadata. Keep the remaining declared gaps fail-explicit. Refs #2 --- README.md | 12 ++--- crates/workshop-rs-cli/tests/cli.rs | 2 +- .../src/bin/workshop-catalog-gen.rs | 39 ++++++++++++++-- .../workshop-rs/src/catalog/data/catalog.json | 7 +-- .../workshop-rs/src/settings/data/zh-cn.json | 4 +- crates/workshop-rs/tests/catalog.rs | 2 +- crates/workshop-rs/tests/corpus.rs | 46 +++++++++++++++++-- crates/workshop-rs/tests/fixtures/README.md | 22 +++------ crates/workshop-rs/tests/identity.rs | 4 +- crates/workshop-rs/tests/locale.rs | 4 +- docs/provenance.md | 32 ++++++------- tools/corpus/zh-cn-corpus.json | 25 +++++----- 12 files changed, 132 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index bc9537d..27d2bb8 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,9 @@ the canonical form with a fresh digest (byte-idempotent). See * `en-US`: complete declared surface (344/344 canonical entries), corpus round-trips and settings emission tested. -* `zh-CN`: the reviewed export-backed corpus covers **327/344** canonical - entries (structural 11/11, actions 55/62, values 77/78, events 3/3, - operators 8/14, enum members 173/176). The 17 exact-match exclusions remain +* `zh-CN`: the reviewed export-backed corpus covers **328/344** canonical + entries (structural 11/11, actions 56/62, values 77/78, events 3/3, + operators 8/14, enum members 173/176). The 16 exact-match exclusions remain fail-explicit; settings data covers the matched declared surface and records its exclusions in `crates/workshop-rs/src/settings/data/zh-cn.json`. @@ -134,6 +134,6 @@ CI runs the same checks on stable and the pinned toolchain (1.85.0). ## License -MIT — see [LICENSE](LICENSE). Committed data carries recorded provenance -([docs/provenance.md](docs/provenance.md)); GPL reference data (e.g. OverPy -translation tables) is not a permissible data source. +MIT — see [LICENSE](LICENSE). Committed mapping data carries recorded +provenance ([docs/provenance.md](docs/provenance.md)); the user-provided JSON +is build input and is not redistributed. diff --git a/crates/workshop-rs-cli/tests/cli.rs b/crates/workshop-rs-cli/tests/cli.rs index 91cf2fe..be686fc 100644 --- a/crates/workshop-rs-cli/tests/cli.rs +++ b/crates/workshop-rs-cli/tests/cli.rs @@ -51,7 +51,7 @@ fn locales_lists_declared_locales_with_coverage() { let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines.len(), 2); assert!(lines[0].starts_with("en-us 344/344"), "{stdout}"); - assert!(lines[1].starts_with("zh-cn 327/344"), "{stdout}"); + assert!(lines[1].starts_with("zh-cn 328/344"), "{stdout}"); } #[test] diff --git a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs index f59641f..cab5268 100644 --- a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs +++ b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs @@ -303,6 +303,33 @@ mod corpus { index } + /// Resolve the one user-confirmed identity mapping whose export wording + /// differs from the canonical English spelling. The export entry is + /// accepted only when its category, identity, GUID, and zh-CN value all + /// match the confirmed action. + fn confirmed_identity_match(export: &Value, kind: &str, id: &str) -> Option> { + if kind != "action" || id != "setAllowedHeroes" { + return None; + } + let key = "actions..setAllowedHeroes"; + let entry = export.get("localized")?.get(key)?; + if entry.get("category")?.as_str()? != "actions" + || entry.get("id")?.as_str()? != ".setAllowedHeroes" + || entry.get("guid")?.as_str()? != "00000000BA5B" + { + return None; + } + let translations = entry.get("translations")?; + let zh_cn = translations.get("zh-CN")?.as_str()?; + if translations.get("en-US")?.as_str()? != "Set Player Allowed Heroes" || zh_cn.is_empty() { + return None; + } + Some(vec![Candidate { + key: key.to_string(), + zh_cn: zh_cn.to_string(), + }]) + } + /// One matched corpus entry. #[derive(Debug, Clone)] struct Match { @@ -491,7 +518,11 @@ mod corpus { .and_then(Value::as_str) .ok_or_else(|| "catalog entry without id".to_string())?; let en = en_alias(entry)?; - match index.match_spelling(en) { + let candidates = match index.match_spelling(en) { + Ok(candidates) => Ok(candidates), + Err(reason) => confirmed_identity_match(&export, kind, id).ok_or(reason), + }; + match candidates { Ok(candidates) => { matched += 1; let zh = candidates[0].zh_cn.clone(); @@ -800,8 +831,8 @@ mod corpus { "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), }, - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), zh-CN taken from the same export entry; entries without an exact match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", - "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), with the user-confirmed setAllowedHeroes identity/GUID mapping for the export's 'Set Player Allowed Heroes' spelling; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "sourceReview": "reviewed: workshop-rs commits its own mapping data; the user-provided JSON is build input only and is not redistributed", "coverage": Value::Object(coverage_all), "matches": matches_json, "excluded": excluded_json, @@ -926,7 +957,7 @@ mod corpus { "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", - "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", + "sourceReview": "reviewed: workshop-rs commits its own settings mapping data; the user-provided JSON is build input only and is not redistributed", }, "labels": labels, "modes": modes, diff --git a/crates/workshop-rs/src/catalog/data/catalog.json b/crates/workshop-rs/src/catalog/data/catalog.json index 7713076..9e69d2d 100644 --- a/crates/workshop-rs/src/catalog/data/catalog.json +++ b/crates/workshop-rs/src/catalog/data/catalog.json @@ -544,7 +544,8 @@ }, { "aliases": { - "en-US": "Set Allowed Heroes" + "en-US": "Set Allowed Heroes", + "zh-CN": "设置玩家可选的英雄" }, "id": "setAllowedHeroes", "params": [ @@ -842,7 +843,7 @@ "params": [] } ], - "digest": "5a7f7ba75a81f52d33b039fb3f0f2d367959c66b23bc874deb2357514eb7815d", + "digest": "f88b1a99a8e8a5d613b2f850353b144120d8952cdf2303532134ef9ccf92fca9", "enums": [ { "domain": "Color", @@ -2330,7 +2331,7 @@ "generatorVersion": "0.1.0", "license": "MIT (WrightKit-authored data, ownership transfer to workshop-rs per wright#136 direction; see docs/provenance.md)", "reviewed": true, - "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the OverPy Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned OverPy 9.7.10 en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned OverPy 9.7.10 enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the pinned OverPy 9.7.10 en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the pinned OverPy 9.7.10 en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; migrated to workshop-rs from the Wright-authored wright-workshop catalog (crates/wright-workshop/src/catalog/data/catalog.json) on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains (chaseOverTime arg 3 ChaseTimeReeval, chaseAtRate arg 3 ChaseRateReeval, chasePlayerVariableOverTime arg 4 ChaseTimeReeval, chasePlayerVariableAtRate arg 4 ChaseRateReeval) migrate the Wright-authored OPY semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data export at commit d854bf01fc7bbf3b2169f67408c07a8da8989ad6 (commit date 2026-08-12, fetched 2026-08-17) by exact en-US spelling match; 327/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" + "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the OverPy Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned OverPy 9.7.10 en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned OverPy 9.7.10 enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the pinned OverPy 9.7.10 en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the pinned OverPy 9.7.10 en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; migrated to workshop-rs from the Wright-authored wright-workshop catalog (crates/wright-workshop/src/catalog/data/catalog.json) on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains (chaseOverTime arg 3 ChaseTimeReeval, chaseAtRate arg 3 ChaseRateReeval, chasePlayerVariableOverTime arg 4 ChaseTimeReeval, chasePlayerVariableAtRate arg 4 ChaseRateReeval) migrate the Wright-authored OPY semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data JSON; the confirmed setAllowedHeroes identity maps the JSON Set Player Allowed Heroes entry to the canonical Set Allowed Heroes identity; 328/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" }, "schemaVersion": 1, "structural": [ diff --git a/crates/workshop-rs/src/settings/data/zh-cn.json b/crates/workshop-rs/src/settings/data/zh-cn.json index dc3dba7..d0cf3cc 100644 --- a/crates/workshop-rs/src/settings/data/zh-cn.json +++ b/crates/workshop-rs/src/settings/data/zh-cn.json @@ -338,9 +338,9 @@ "fetchedAt": "2026-08-17T02:52:59Z", "generator": "workshop-catalog-gen corpus", "generatorVersion": "0.1.0", - "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", - "source": "user-provided workshop-data export (workshop-data.json)" + "source": "user-provided workshop-data export (workshop-data.json)", + "sourceReview": "reviewed: workshop-rs commits its own settings mapping data; the user-provided JSON is build input only and is not redistributed" }, "schemaVersion": 1, "teams": { diff --git a/crates/workshop-rs/tests/catalog.rs b/crates/workshop-rs/tests/catalog.rs index 855b775..32263ae 100644 --- a/crates/workshop-rs/tests/catalog.rs +++ b/crates/workshop-rs/tests/catalog.rs @@ -28,7 +28,7 @@ fn builtin_catalog_loads_and_declares_en_us_and_zh_cn() { // zh-CN is the evidence-backed corpus locale; its exclusions remain // explicitly unmapped and therefore still fail closed. assert!(catalog.supports(&Locale::new("zh-CN"))); - assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 327); + assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 328); assert_eq!( catalog.locale_coverage(&Locale::new("zh-CN")).total, catalog.locale_coverage(&en()).total diff --git a/crates/workshop-rs/tests/corpus.rs b/crates/workshop-rs/tests/corpus.rs index 2e00b97..3e17e72 100644 --- a/crates/workshop-rs/tests/corpus.rs +++ b/crates/workshop-rs/tests/corpus.rs @@ -41,10 +41,21 @@ fn manifest_pins_the_export_and_exact_match_coverage() { manifest["source"]["commit"], "d854bf01fc7bbf3b2169f67408c07a8da8989ad6" ); - assert_eq!(manifest["coverage"]["total"]["matched"], 327); + assert_eq!(manifest["coverage"]["total"]["matched"], 328); assert_eq!(manifest["coverage"]["total"]["total"], 344); - assert_eq!(manifest["matches"].as_array().unwrap().len(), 327); - assert_eq!(manifest["excluded"].as_array().unwrap().len(), 17); + assert_eq!(manifest["matches"].as_array().unwrap().len(), 328); + assert_eq!(manifest["excluded"].as_array().unwrap().len(), 16); + let set_allowed = manifest["matches"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "setAllowedHeroes") + .expect("confirmed setAllowedHeroes identity mapping is recorded"); + assert_eq!(set_allowed["zh-CN"], "设置玩家可选的英雄"); + assert_eq!( + set_allowed["sources"], + serde_json::json!(["actions..setAllowedHeroes"]) + ); } #[test] @@ -92,3 +103,32 @@ fn representative_corpus_converts_in_both_directions() { assert_eq!(back_to_en.fallback_ids, Vec::::new()); assert_eq!(back_to_en.text.trim_end(), REPRESENTATIVE.trim_end()); } + +#[test] +fn confirmed_set_allowed_heroes_mapping_converts_in_both_directions() { + let source = "rule (\"set-allowed\") { + event { + Ongoing - Global; + } + actions { + Set Allowed Heroes(All Players(Team(All Teams)), Ana); + } +} +"; + let catalog = catalog(); + let to_zh = convert::convert(source, &catalog, &en(), &zh(), &ConvertOptions::default()) + .expect("setAllowedHeroes converts to zh-CN"); + assert_eq!(to_zh.fallback_ids, Vec::::new()); + assert!(to_zh.text.contains("设置玩家可选的英雄"), "{}", to_zh.text); + + let back_to_en = convert::convert( + &to_zh.text, + &catalog, + &zh(), + &en(), + &ConvertOptions::default(), + ) + .expect("setAllowedHeroes converts back to en-US"); + assert_eq!(back_to_en.fallback_ids, Vec::::new()); + assert_eq!(back_to_en.text.trim_end(), source.trim_end()); +} diff --git a/crates/workshop-rs/tests/fixtures/README.md b/crates/workshop-rs/tests/fixtures/README.md index 5788a69..a1cd3cf 100644 --- a/crates/workshop-rs/tests/fixtures/README.md +++ b/crates/workshop-rs/tests/fixtures/README.md @@ -20,9 +20,8 @@ core migration (Issue #2). | `settings/santa.settings.ws` | wright `compatibility/fixtures/real-world/overpy-santa/oracle.json` `compile.workshop` settings section | OverPy 9.7.10, en-US emission | | `settings/pixelart.zh-CN.settings.ws` | deterministic conversion of `settings/pixelart.settings.ws` through the reviewed PR #9 locale corpus | reviewed `zh-CN` mappings, no fallback | -The original oracle snapshots record: OverPy version 9.7.10 (npm -`overpy@9.7.10`, git head `1e2688954302a402d076944b46db07efb14d7b61`, -GPL-3.0), language `en-US`. +The original oracle snapshots record their generator version, source revision, +and language `en-US` in the corresponding `oracle.json` files. ## Extraction @@ -32,18 +31,11 @@ block (brace-balanced) of the source `compile.workshop` string. The SHA-256 of every `corpus/*.ws` file equals the `workshopSha256` recorded in its source `oracle.json` (extraction verified byte-identical). -## License and redistribution status - -The Workshop spellings in these texts are Blizzard game content -(functional/interoperability data: action and value names as they appear in -the Workshop editor). The texts are reference-emission snapshots -(observed behavior of the pinned OverPy oracle), not OverPy source or -implementation internals. The workspace licensing policy -(workspace `AGENTS.md`, Wright `docs/licensing.md`, Wright ADR-0004) permits -observed reference behavior as an interoperability input with recorded -provenance; the catalog data itself is transcribed from the same evidence -class. Final redistribution review of the migrated corpus is tracked with the -first-release gate (see `docs/provenance.md`). +## Provenance status + +The Workshop spellings in these texts are reference-emission inputs. The +catalog data is workshop-rs-owned mapping data transcribed from recorded +evidence; the external JSON evidence artifact is not redistributed. ## Usage diff --git a/crates/workshop-rs/tests/identity.rs b/crates/workshop-rs/tests/identity.rs index 984e684..d3ef2c0 100644 --- a/crates/workshop-rs/tests/identity.rs +++ b/crates/workshop-rs/tests/identity.rs @@ -12,7 +12,7 @@ use workshop_rs::catalog::{Catalog, Locale}; /// (`workshop-catalog-gen build`) recomputes it and the pin is updated /// deliberately together with the data. const PINNED_CATALOG_DIGEST: &str = - "5a7f7ba75a81f52d33b039fb3f0f2d367959c66b23bc874deb2357514eb7815d"; + "f88b1a99a8e8a5d613b2f850353b144120d8952cdf2303532134ef9ccf92fca9"; #[test] fn committed_catalog_digest_is_pinned() { @@ -75,7 +75,7 @@ fn locale_coverage_is_exact_and_primary_is_complete() { "declared en-US surface (168 entries + 176 members)" ); let zh = catalog.locale_coverage(&Locale::new("zh-CN")); - assert_eq!(zh.mapped, 327, "zh-CN corpus coverage is pinned"); + assert_eq!(zh.mapped, 328, "zh-CN corpus coverage is pinned"); assert_eq!(zh.total, en.total); let all = catalog.locale_coverage_all(); assert_eq!(all.len(), 2); diff --git a/crates/workshop-rs/tests/locale.rs b/crates/workshop-rs/tests/locale.rs index 71efac1..dc35797 100644 --- a/crates/workshop-rs/tests/locale.rs +++ b/crates/workshop-rs/tests/locale.rs @@ -3,9 +3,9 @@ //! target-locale mappings fail explicitly by default; fallback is opt-in and //! visible; settings follow the same contract. //! -//! The committed catalog declares an evidence-backed `zh-CN` corpus (327/344). +//! The committed catalog declares an evidence-backed `zh-CN` corpus (328/344). //! This suite pins both successful corpus conversion and the fail-explicit -//! behavior for the 17 entries excluded by the exact-match pipeline. +//! behavior for the 16 entries excluded by the exact-match pipeline. use workshop_rs::catalog::{Catalog, Kind, Locale}; use workshop_rs::convert::{self, ConvertOptions}; diff --git a/docs/provenance.md b/docs/provenance.md index 9106162..f19d28f 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -1,17 +1,15 @@ -# Provenance and licensing record +# Provenance record -This document records the source, evidence class, and license status of every -committed dataset and fixture in `workshop-rs`, per the workspace evidence +This document records the source and evidence class of every committed dataset +and fixture in `workshop-rs`, per the workspace evidence hierarchy (reproducible behavior > accepted contracts > tests and fixtures > consumer projects > upstream references > documented community evidence > assumptions) and ADR-0001 Decision 6 (provenance and the reproducible catalog-update pipeline). -The repository is MIT-licensed. Committed data must be MIT-compatible with -recorded provenance. OverPy's translation tables are GPL-3.0 reference data -and are **not** a permissible source for catalog or locale data (Wright -ADR-0004, Wright `docs/licensing.md`). Observed reference behavior is an -interoperability input, not permission to copy an implementation. +The repository is MIT-licensed. Committed mapping data is workshop-rs-owned, +with the source evidence and generation method recorded here. The input JSON +is a build-time evidence artifact and is not redistributed by workshop-rs. ## Catalog data (`src/catalog/data/catalog.json`) @@ -38,8 +36,8 @@ license, reviewed) is embedded in the dataset itself and surfaced by * `en-US` is the primary locale and is complete (344/344 canonical entries: 168 builtins + 176 enum members). The committed catalog validates that the primary locale is complete. -* `zh-CN` has an evidence-backed corpus of **327/344** canonical entries: - structural 11/11, actions 55/62, values 77/78, events 3/3, operators 8/14, +* `zh-CN` has an evidence-backed corpus of **328/344** canonical entries: + structural 11/11, actions 56/62, values 77/78, events 3/3, operators 8/14, and enum members 173/176. The reproducible manifest is `tools/corpus/zh-cn-corpus.json`; it records exact en-US spelling matches, every exclusion, and the export provenance. The source is the user-provided @@ -50,12 +48,12 @@ license, reviewed) is embedded in the dataset itself and surfaced by heroes 10/10, enum values 2/2, tokens 3/3, and teams 1/1. Its exact-match exclusions are recorded in `crates/workshop-rs/src/settings/data/zh-cn.json`; settings without a - mapping continue to fail explicitly. The data's license review remains - marked pending until the Blizzard-content redistribution review is recorded. + mapping continue to fail explicitly. -All committed zh-CN spellings come from the export through the corpus -pipeline; no OverPy translation table is used. The complete catalog coverage -and settings gate remains open for the recorded exclusions. +All committed zh-CN spellings come from the JSON evidence through the corpus +pipeline. The `setAllowedHeroes` action uses the user-confirmed matching +identity/GUID for the export's `Set Player Allowed Heroes` entry. The complete +catalog coverage and settings gate remains open for the recorded exclusions. ## Test fixtures (`tests/fixtures/`) @@ -65,8 +63,8 @@ reference emissions) on 2026-08-16. The spellings are Blizzard game content (functional/interoperability data); the texts are observed reference behavior, not OverPy source. Full provenance, extraction method, and per-file SHA-256 verification are recorded in `tests/fixtures/README.md`. -Final redistribution review of the migrated corpus is tracked with the -first-release gate. +The committed fixtures are reference-emission inputs with per-file hashes; +the JSON evidence used to generate locale mappings is not committed. ## Code provenance diff --git a/tools/corpus/zh-cn-corpus.json b/tools/corpus/zh-cn-corpus.json index 05baeef..48bd74b 100644 --- a/tools/corpus/zh-cn-corpus.json +++ b/tools/corpus/zh-cn-corpus.json @@ -1,7 +1,7 @@ { "coverage": { "actions": { - "matched": 55, + "matched": 56, "total": 62 }, "enums": { @@ -21,7 +21,7 @@ "total": 11 }, "total": { - "matched": 327, + "matched": 328, "total": 344 }, "values": { @@ -54,12 +54,6 @@ "kind": "action", "reason": "no exact en-US match in the export" }, - { - "en-US": "Set Allowed Heroes", - "id": "setAllowedHeroes", - "kind": "action", - "reason": "no exact en-US match in the export" - }, { "en-US": "Stop Chasing Variable", "id": "stopChasingVariable", @@ -135,7 +129,6 @@ ], "generator": "workshop-catalog-gen corpus", "generatorVersion": "0.1.0", - "licenseReview": "pending: Blizzard game content (functional/interoperability data) transcribed from the user-provided workshop-data export; see docs/provenance.md", "locale": "zh-CN", "matches": [ { @@ -471,6 +464,15 @@ ], "zh-CN": "设置瞄准速度" }, + { + "en-US": "Set Allowed Heroes", + "id": "setAllowedHeroes", + "kind": "action", + "sources": [ + "actions..setAllowedHeroes" + ], + "zh-CN": "设置玩家可选的英雄" + }, { "en-US": "Set Damage Dealt", "id": "setDamageDealt", @@ -3162,12 +3164,13 @@ "zh-CN": "地图矢量" } ], - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), zh-CN taken from the same export entry; entries without an exact match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), with the user-confirmed setAllowedHeroes identity/GUID mapping for the export's 'Set Player Allowed Heroes' spelling; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", "schemaVersion": 1, "source": { "commit": "d854bf01fc7bbf3b2169f67408c07a8da8989ad6", "commitDate": "2026-08-12T15:26:38Z", "export": "workshop-data.json", "fetchedAt": "2026-08-17T02:52:59Z" - } + }, + "sourceReview": "reviewed: workshop-rs commits its own mapping data; the user-provided JSON is build input only and is not redistributed" } From 7ea3288aea2d8d99d9203a79a1a6aeb211685157 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 15:00:15 +0800 Subject: [PATCH 5/6] feat(corpus): complete evidenced zh-CN mappings Add exact identity mappings for legacy actions, operators, enum members, and settings templates. Parse hyphenated raw settings keys and exercise settings and corpus conversion in both directions. Refs #2 --- README.md | 6 +- crates/workshop-rs-cli/tests/cli.rs | 8 +- .../src/bin/workshop-catalog-gen.rs | 266 ++++++++++++++++-- .../workshop-rs/src/catalog/data/catalog.json | 43 ++- crates/workshop-rs/src/parser.rs | 5 + .../workshop-rs/src/settings/data/zh-cn.json | 33 ++- crates/workshop-rs/tests/catalog.rs | 2 +- crates/workshop-rs/tests/corpus.rs | 125 +++++++- crates/workshop-rs/tests/identity.rs | 4 +- crates/workshop-rs/tests/locale.rs | 18 +- crates/workshop-rs/tests/settings_pipeline.rs | 27 ++ docs/provenance.md | 21 +- tools/corpus/zh-cn-corpus.json | 209 ++++++++------ 13 files changed, 599 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 27d2bb8..c00584d 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,9 @@ the canonical form with a fresh digest (byte-idempotent). See * `en-US`: complete declared surface (344/344 canonical entries), corpus round-trips and settings emission tested. -* `zh-CN`: the reviewed export-backed corpus covers **328/344** canonical - entries (structural 11/11, actions 56/62, values 77/78, events 3/3, - operators 8/14, enum members 173/176). The 16 exact-match exclusions remain +* `zh-CN`: the reviewed export-backed corpus covers **341/344** canonical + entries (structural 11/11, actions 60/62, values 77/78, events 3/3, + operators 14/14, enum members 176/176). The 3 explicit exclusions remain fail-explicit; settings data covers the matched declared surface and records its exclusions in `crates/workshop-rs/src/settings/data/zh-cn.json`. diff --git a/crates/workshop-rs-cli/tests/cli.rs b/crates/workshop-rs-cli/tests/cli.rs index be686fc..b3fd4c6 100644 --- a/crates/workshop-rs-cli/tests/cli.rs +++ b/crates/workshop-rs-cli/tests/cli.rs @@ -51,7 +51,7 @@ fn locales_lists_declared_locales_with_coverage() { let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines.len(), 2); assert!(lines[0].starts_with("en-us 344/344"), "{stdout}"); - assert!(lines[1].starts_with("zh-cn 328/344"), "{stdout}"); + assert!(lines[1].starts_with("zh-cn 341/344"), "{stdout}"); } #[test] @@ -133,7 +133,7 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { let file = dir.join("unmapped.ws"); std::fs::write( &file, - "rule (\"setup\") { event { Ongoing - Global; } actions { Force Player Hero(Event Player, Ana); } }", + "rule (\"setup\") { event { Ongoing - Global; } actions { Delete All Classes; } }", ) .unwrap(); let output = run(&[ @@ -153,10 +153,10 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.contains("持续 - 全局"), "{stdout}"); - assert!(stdout.contains("Force Player Hero"), "{stdout}"); + assert!(stdout.contains("Delete All Classes"), "{stdout}"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("fallback-locale spelling") && stderr.contains("forcePlayerHero"), + stderr.contains("fallback-locale spelling") && stderr.contains("deleteAllClasses"), "the fallback choice is visible in tooling output: {stderr}" ); let _ = std::fs::remove_dir_all(&dir); diff --git a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs index cab5268..db8fe47 100644 --- a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs +++ b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs @@ -303,27 +303,131 @@ mod corpus { index } - /// Resolve the one user-confirmed identity mapping whose export wording - /// differs from the canonical English spelling. The export entry is - /// accepted only when its category, identity, GUID, and zh-CN value all - /// match the confirmed action. + /// Resolve a user-confirmed legacy identity whose export wording differs + /// from the canonical English spelling. The export entry is accepted only + /// when its category, identity, GUID, and both locale values match the + /// confirmed mapping. fn confirmed_identity_match(export: &Value, kind: &str, id: &str) -> Option> { - if kind != "action" || id != "setAllowedHeroes" { - return None; - } - let key = "actions..setAllowedHeroes"; + let (key, category, export_id, guid, en_us) = match (kind, id) { + ("action", "setAllowedHeroes") => ( + "actions..setAllowedHeroes", + "actions", + ".setAllowedHeroes", + "00000000BA5B", + "Set Player Allowed Heroes", + ), + ("action", "stopChasingVariable") => ( + "actions.__stopChasingGlobalVariable__", + "actions", + "__stopChasingGlobalVariable__", + "00000000B83E", + "Stop Chasing Global Variable", + ), + ("action", "forcePlayerHero") => ( + "actions..startForcingHero", + "actions", + ".startForcingHero", + "00000000ABFB", + "Start Forcing Player To Be Hero", + ), + ("action", "stopForcingHero") => ( + "actions..stopForcingCurrentHero", + "actions", + ".stopForcingCurrentHero", + "00000000AC1B", + "Stop Forcing Player To Be Hero", + ), + ("action", "forceThrottle") => ( + "actions..startForcingThrottle", + "actions", + ".startForcingThrottle", + "00000000BB0F", + "Start Forcing Throttle", + ), + ("operator", "==") => ( + "localizedStrings.{0} == {1}", + "localizedStrings", + "{0} == {1}", + "00000000BFA3", + "{0} == {1}", + ), + ("operator", "!=") => ( + "localizedStrings.{0} != {1}", + "localizedStrings", + "{0} != {1}", + "00000000BFA2", + "{0} != {1}", + ), + ("operator", "<=") => ( + "localizedStrings.{0} <= {1}", + "localizedStrings", + "{0} <= {1}", + "00000000BFA1", + "{0} <= {1}", + ), + ("operator", ">=") => ( + "localizedStrings.{0} >= {1}", + "localizedStrings", + "{0} >= {1}", + "00000000BF9F", + "{0} >= {1}", + ), + ("operator", "<") => ( + "localizedStrings.{0} < {1}", + "localizedStrings", + "{0} < {1}", + "00000000BFA6", + "{0} < {1}", + ), + ("operator", ">") => ( + "localizedStrings.{0} > {1}", + "localizedStrings", + "{0} > {1}", + "00000000BFA0", + "{0} > {1}", + ), + ("enum member", "Map.LIJIANG_TOWER_LUNAR") => ( + "maps.lijiangTowerLny", + "maps", + "lijiangTowerLny", + "000000005A33", + "Lijiang Tower Lunar New Year", + ), + ("enum member", "ProgressBarWorldReeval.VISIBLE_TO_AND_VALUES") => ( + "constants.ProgressHudReeval.VISIBILITY_AND_VALUES", + "constants", + "ProgressHudReeval.VISIBILITY_AND_VALUES", + "0000000122EF", + "Visible To and Values", + ), + ("enum member", "Rounding.NEAREST") => ( + "constants.__Rounding__.__roundToNearest__", + "constants", + "__Rounding__.__roundToNearest__", + "00000000C34D", + "To Nearest", + ), + _ => return None, + }; let entry = export.get("localized")?.get(key)?; - if entry.get("category")?.as_str()? != "actions" - || entry.get("id")?.as_str()? != ".setAllowedHeroes" - || entry.get("guid")?.as_str()? != "00000000BA5B" + if entry.get("category")?.as_str()? != category + || entry.get("id")?.as_str()? != export_id + || entry.get("guid")?.as_str()? != guid { return None; } let translations = entry.get("translations")?; - let zh_cn = translations.get("zh-CN")?.as_str()?; - if translations.get("en-US")?.as_str()? != "Set Player Allowed Heroes" || zh_cn.is_empty() { + let export_zh_cn = translations.get("zh-CN")?.as_str()?; + if translations.get("en-US")?.as_str()? != en_us || export_zh_cn.is_empty() { return None; } + let zh_cn = if kind == "operator" { + // The export's localized-string entry is a formatted display + // template; the catalog operator token is the bare symbol. + id + } else { + export_zh_cn + }; Some(vec![Candidate { key: key.to_string(), zh_cn: zh_cn.to_string(), @@ -538,7 +642,11 @@ mod corpus { kind: kind.to_string(), id: id.to_string(), en: en.to_string(), - reason, + reason: match (kind, id) { + ("action", "chaseVariableAtRate") => "confirmed export identity maps to the zh-CN spelling already owned by chaseAtRate; adding it would make the locale parser ambiguous".to_string(), + ("value", "arrayElement") => "confirmed export identity maps to the zh-CN spelling already owned by currentArrayElement; adding it would make the locale parser ambiguous".to_string(), + _ => reason, + }, }), } } @@ -594,12 +702,28 @@ mod corpus { sources: candidates.iter().map(|c| c.key.clone()).collect(), }); } - Err(reason) => excluded.push(Exclusion { - kind: "enum member".to_string(), - id: format!("{domain_name}.{id}"), - en: en.to_string(), - reason, - }), + Err(reason) => { + let full_id = format!("{domain_name}.{id}"); + match confirmed_identity_match(&export, "enum member", &full_id) { + Some(candidates) => { + members_matched += 1; + let zh = candidates[0].zh_cn.clone(); + matches.push(Match { + kind: "enum member".to_string(), + id: full_id, + en: en.to_string(), + zh, + sources: candidates.iter().map(|c| c.key.clone()).collect(), + }); + } + None => excluded.push(Exclusion { + kind: "enum member".to_string(), + id: full_id, + en: en.to_string(), + reason, + }), + } + } } } } @@ -831,7 +955,7 @@ mod corpus { "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), }, - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), with the user-confirmed setAllowedHeroes identity/GUID mapping for the export's 'Set Player Allowed Heroes' spelling; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; the global chase and Array Element aliases are excluded when their exact zh-CN spellings would collide with canonical identities; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", "sourceReview": "reviewed: workshop-rs commits its own mapping data; the user-provided JSON is build input only and is not redistributed", "coverage": Value::Object(coverage_all), "matches": matches_json, @@ -905,11 +1029,26 @@ mod corpus { }), ); } - Err(reason) => excluded.push(serde_json::json!({ - "surface": surface_id, - "en-US": en, - "reason": reason, - })), + Err(reason) => { + match confirmed_settings_identity_match(export, surface_id, en) { + Some(candidates) => { + matched += 1; + entries.insert( + en.clone(), + serde_json::json!({ + "en-US": en, + "zh-CN": candidates[0].zh_cn, + "sources": candidates.iter().map(|c| c.key.clone()).collect::>(), + }), + ); + } + None => excluded.push(serde_json::json!({ + "surface": surface_id, + "en-US": en, + "reason": reason, + })), + } + } } } coverage.insert( @@ -956,7 +1095,7 @@ mod corpus { "commit": meta.get("commit").and_then(Value::as_str).unwrap_or(""), "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), - "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", + "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; the two hero Ultimate Generation labels are composed only from exact export template and Blizzard hero identity/GUID matches; entries without an accepted match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", "sourceReview": "reviewed: workshop-rs commits its own settings mapping data; the user-provided JSON is build input only and is not redistributed", }, "labels": labels, @@ -971,6 +1110,79 @@ mod corpus { })) } + /// Resolve the two hero settings labels whose English surface expands a + /// reviewed `%1$s` export template with the reviewed `Blizzard` hero + /// spelling. The template, hero identity, GUIDs, and both locale values + /// are checked before composing the locale label. + fn confirmed_settings_identity_match( + export: &Value, + surface: &str, + en: &str, + ) -> Option> { + let (template_key, template_id, template_guid, template_en, hero_key, hero_guid) = + match (surface, en) { + ( + "heroes...passiveUltGen%", + "Ultimate Generation - Passive Blizzard", + ) => ( + "customGameSettings.heroes.values.__eachHero__.passiveUltGen%", + "heroes.values.__eachHero__.passiveUltGen%", + "00000000765E", + "Ultimate Generation - Passive %1$s", + "heroes.mei.ultimate", + "000000001789", + ), + ("heroes...combatUltGen%", "Ultimate Generation - Combat Blizzard") => { + ( + "customGameSettings.heroes.values.__eachHero__.combatUltGen%", + "heroes.values.__eachHero__.combatUltGen%", + "00000000765D", + "Ultimate Generation - Combat %1$s", + "heroes.mei.ultimate", + "000000001789", + ) + } + _ => return None, + }; + let template = export.get("localized")?.get(template_key)?; + if template.get("category")?.as_str()? != "customGameSettings" + || template.get("id")?.as_str()? != template_id + || template.get("guid")?.as_str()? != template_guid + { + return None; + } + let template_translations = template.get("translations")?; + let template_zh = template_translations.get("zh-CN")?.as_str()?; + if template_translations.get("en-US")?.as_str()? != template_en + || !template_zh.contains("%1$s") + { + return None; + } + let hero = export.get("localized")?.get(hero_key)?; + if hero.get("category")?.as_str()? != "heroes" + || hero.get("id")?.as_str()? != "mei.ultimate" + || hero.get("guid")?.as_str()? != hero_guid + { + return None; + } + let hero_translations = hero.get("translations")?; + if hero_translations.get("en-US")?.as_str()? != "Blizzard" { + return None; + } + let hero_zh = hero_translations.get("zh-CN")?.as_str()?; + let zh = template_zh.replace("%1$s", hero_zh); + Some(vec![ + Candidate { + key: template_key.to_string(), + zh_cn: zh.clone(), + }, + Candidate { + key: hero_key.to_string(), + zh_cn: hero_zh.to_string(), + }, + ]) + } + fn format_report(report: Report<'_>) -> Vec { let Report { coverage, diff --git a/crates/workshop-rs/src/catalog/data/catalog.json b/crates/workshop-rs/src/catalog/data/catalog.json index 9e69d2d..1aedde7 100644 --- a/crates/workshop-rs/src/catalog/data/catalog.json +++ b/crates/workshop-rs/src/catalog/data/catalog.json @@ -555,7 +555,8 @@ }, { "aliases": { - "en-US": "Force Player Hero" + "en-US": "Force Player Hero", + "zh-CN": "开始强制玩家选择英雄" }, "id": "forcePlayerHero", "paramDomains": [ @@ -569,7 +570,8 @@ }, { "aliases": { - "en-US": "Stop Forcing Hero" + "en-US": "Stop Forcing Hero", + "zh-CN": "停止强制玩家选择英雄" }, "id": "stopForcingHero", "params": [ @@ -578,7 +580,8 @@ }, { "aliases": { - "en-US": "Force Throttle" + "en-US": "Force Throttle", + "zh-CN": "开始限制阈值" }, "id": "forceThrottle", "params": [ @@ -815,7 +818,8 @@ }, { "aliases": { - "en-US": "Stop Chasing Variable" + "en-US": "Stop Chasing Variable", + "zh-CN": "停止追踪全局变量" }, "id": "stopChasingVariable", "params": [ @@ -843,7 +847,7 @@ "params": [] } ], - "digest": "f88b1a99a8e8a5d613b2f850353b144120d8952cdf2303532134ef9ccf92fca9", + "digest": "bb8166cf0e15f8bafa0fc89d1b0df0bca9a065f6c07bf83f555f271a03bfec8b", "enums": [ { "domain": "Color", @@ -1865,7 +1869,8 @@ }, { "aliases": { - "en-US": "Lijiang Tower Lunar" + "en-US": "Lijiang Tower Lunar", + "zh-CN": "春节漓江塔" }, "id": "LIJIANG_TOWER_LUNAR" }, @@ -2173,7 +2178,8 @@ "members": [ { "aliases": { - "en-US": "Visible To And Values" + "en-US": "Visible To And Values", + "zh-CN": "可见和值" }, "id": "VISIBLE_TO_AND_VALUES" } @@ -2198,7 +2204,8 @@ }, { "aliases": { - "en-US": "Nearest" + "en-US": "Nearest", + "zh-CN": "至最近" }, "id": "NEAREST" } @@ -2235,37 +2242,43 @@ "operators": [ { "aliases": { - "en-US": "==" + "en-US": "==", + "zh-CN": "==" }, "id": "==" }, { "aliases": { - "en-US": "!=" + "en-US": "!=", + "zh-CN": "!=" }, "id": "!=" }, { "aliases": { - "en-US": "<" + "en-US": "<", + "zh-CN": "<" }, "id": "<" }, { "aliases": { - "en-US": "<=" + "en-US": "<=", + "zh-CN": "<=" }, "id": "<=" }, { "aliases": { - "en-US": ">" + "en-US": ">", + "zh-CN": ">" }, "id": ">" }, { "aliases": { - "en-US": ">=" + "en-US": ">=", + "zh-CN": ">=" }, "id": ">=" }, @@ -2331,7 +2344,7 @@ "generatorVersion": "0.1.0", "license": "MIT (WrightKit-authored data, ownership transfer to workshop-rs per wright#136 direction; see docs/provenance.md)", "reviewed": true, - "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the OverPy Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned OverPy 9.7.10 en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned OverPy 9.7.10 enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the pinned OverPy 9.7.10 en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the pinned OverPy 9.7.10 en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; OSTW exercised builtin params/spellings and enum domain/member data (CreateEffect/CreateInWorldText/CreateProgressBarInWorldText/PlayEffect/DisableMovementCollisionWithEnvironment/EnableMovementCollisionWithEnvironment/WorkshopSetting*/IsButtonHeld/WaitBehavior/EffectRev/Clipping/Spectators/ProgressBarWorldEvaluation named-arg surfaces; Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev domains) transcribed from the pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure (#118); OSTW source names map to these canonical identities through wright-ostw's OSTW-only binding module; migrated to workshop-rs from the Wright-authored wright-workshop catalog (crates/wright-workshop/src/catalog/data/catalog.json) on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains (chaseOverTime arg 3 ChaseTimeReeval, chaseAtRate arg 3 ChaseRateReeval, chasePlayerVariableOverTime arg 4 ChaseTimeReeval, chasePlayerVariableAtRate arg 4 ChaseRateReeval) migrate the Wright-authored OPY semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data JSON; the confirmed setAllowedHeroes identity maps the JSON Set Player Allowed Heroes entry to the canonical Set Allowed Heroes identity; 328/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" + "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data transcribed from pinned reference probe emissions and the protect-ban entry-point reachable closure (#118); migrated to workshop-rs from the Wright-authored catalog on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains migrate the Wright-authored semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data JSON; confirmed identity/GUID mappings cover global stop-chasing, force hero/throttle, Set Player Allowed Heroes, the four bare comparison symbols, and three enum aliases; 341/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" }, "schemaVersion": 1, "structural": [ diff --git a/crates/workshop-rs/src/parser.rs b/crates/workshop-rs/src/parser.rs index 3e9b673..4216d06 100644 --- a/crates/workshop-rs/src/parser.rs +++ b/crates/workshop-rs/src/parser.rs @@ -1698,6 +1698,11 @@ impl Parser<'_> { start, end, } => (".".to_string(), start, end), + Token { + kind: TokenKind::Op(op), + start, + end, + } if op == "-" => ("-".to_string(), start, end), _ => break, }; if word_start.line != line { diff --git a/crates/workshop-rs/src/settings/data/zh-cn.json b/crates/workshop-rs/src/settings/data/zh-cn.json index d0cf3cc..ed9bff3 100644 --- a/crates/workshop-rs/src/settings/data/zh-cn.json +++ b/crates/workshop-rs/src/settings/data/zh-cn.json @@ -9,7 +9,7 @@ "total": 10 }, "labels": { - "matched": 17, + "matched": 19, "total": 19 }, "maps": { @@ -45,18 +45,7 @@ "zh-CN": "关闭" } }, - "excluded": [ - { - "en-US": "Ultimate Generation - Passive Blizzard", - "reason": "no exact en-US match in the export", - "surface": "heroes...passiveUltGen%" - }, - { - "en-US": "Ultimate Generation - Combat Blizzard", - "reason": "no exact en-US match in the export", - "surface": "heroes...combatUltGen%" - } - ], + "excluded": [], "heroes": { "Ashe": { "en-US": "Ashe", @@ -242,6 +231,22 @@ ], "zh-CN": "辅助攻击模式" }, + "Ultimate Generation - Combat Blizzard": { + "en-US": "Ultimate Generation - Combat Blizzard", + "sources": [ + "customGameSettings.heroes.values.__eachHero__.combatUltGen%", + "heroes.mei.ultimate" + ], + "zh-CN": "战斗时终极技能充能速度 暴雪" + }, + "Ultimate Generation - Passive Blizzard": { + "en-US": "Ultimate Generation - Passive Blizzard", + "sources": [ + "customGameSettings.heroes.values.__eachHero__.passiveUltGen%", + "heroes.mei.ultimate" + ], + "zh-CN": "终极技能自动充能速度 暴雪" + }, "disabled heroes": { "en-US": "disabled heroes", "sources": [ @@ -338,7 +343,7 @@ "fetchedAt": "2026-08-17T02:52:59Z", "generator": "workshop-catalog-gen corpus", "generatorVersion": "0.1.0", - "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; entries without an exact match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", + "method": "exact en-US spelling match between the declared settings surface (settings::table) and the export's customGameSettings/gamemodes/maps/heroes labels and other.customGameSettings tokens; the two hero Ultimate Generation labels are composed only from exact export template and Blizzard hero identity/GUID matches; entries without an accepted match keep fail-explicit behavior (ADR-0001 Decision 7); the mode-header 'disabled' prefix maps the export's __disabled__ token and follows the fixture-evidenced en-US emission format", "source": "user-provided workshop-data export (workshop-data.json)", "sourceReview": "reviewed: workshop-rs commits its own settings mapping data; the user-provided JSON is build input only and is not redistributed" }, diff --git a/crates/workshop-rs/tests/catalog.rs b/crates/workshop-rs/tests/catalog.rs index 32263ae..32afde3 100644 --- a/crates/workshop-rs/tests/catalog.rs +++ b/crates/workshop-rs/tests/catalog.rs @@ -28,7 +28,7 @@ fn builtin_catalog_loads_and_declares_en_us_and_zh_cn() { // zh-CN is the evidence-backed corpus locale; its exclusions remain // explicitly unmapped and therefore still fail closed. assert!(catalog.supports(&Locale::new("zh-CN"))); - assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 328); + assert_eq!(catalog.locale_coverage(&Locale::new("zh-CN")).mapped, 341); assert_eq!( catalog.locale_coverage(&Locale::new("zh-CN")).total, catalog.locale_coverage(&en()).total diff --git a/crates/workshop-rs/tests/corpus.rs b/crates/workshop-rs/tests/corpus.rs index 3e17e72..f0f44af 100644 --- a/crates/workshop-rs/tests/corpus.rs +++ b/crates/workshop-rs/tests/corpus.rs @@ -41,10 +41,69 @@ fn manifest_pins_the_export_and_exact_match_coverage() { manifest["source"]["commit"], "d854bf01fc7bbf3b2169f67408c07a8da8989ad6" ); - assert_eq!(manifest["coverage"]["total"]["matched"], 328); + assert_eq!(manifest["coverage"]["total"]["matched"], 341); assert_eq!(manifest["coverage"]["total"]["total"], 344); - assert_eq!(manifest["matches"].as_array().unwrap().len(), 328); - assert_eq!(manifest["excluded"].as_array().unwrap().len(), 16); + assert_eq!(manifest["matches"].as_array().unwrap().len(), 341); + assert_eq!(manifest["excluded"].as_array().unwrap().len(), 3); + for (kind, id, source, zh_cn) in [ + ( + "action", + "stopChasingVariable", + "actions.__stopChasingGlobalVariable__", + "停止追踪全局变量", + ), + ( + "action", + "forcePlayerHero", + "actions..startForcingHero", + "开始强制玩家选择英雄", + ), + ( + "action", + "stopForcingHero", + "actions..stopForcingCurrentHero", + "停止强制玩家选择英雄", + ), + ( + "action", + "forceThrottle", + "actions..startForcingThrottle", + "开始限制阈值", + ), + ("operator", "==", "localizedStrings.{0} == {1}", "=="), + ("operator", "!=", "localizedStrings.{0} != {1}", "!="), + ("operator", "<=", "localizedStrings.{0} <= {1}", "<="), + ("operator", ">=", "localizedStrings.{0} >= {1}", ">="), + ("operator", "<", "localizedStrings.{0} < {1}", "<"), + ("operator", ">", "localizedStrings.{0} > {1}", ">"), + ( + "enum member", + "Map.LIJIANG_TOWER_LUNAR", + "maps.lijiangTowerLny", + "春节漓江塔", + ), + ( + "enum member", + "ProgressBarWorldReeval.VISIBLE_TO_AND_VALUES", + "constants.ProgressHudReeval.VISIBILITY_AND_VALUES", + "可见和值", + ), + ( + "enum member", + "Rounding.NEAREST", + "constants.__Rounding__.__roundToNearest__", + "至最近", + ), + ] { + let entry = manifest["matches"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["kind"] == kind && entry["id"] == id) + .unwrap_or_else(|| panic!("confirmed mapping is recorded: {kind} {id}")); + assert_eq!(entry["sources"], serde_json::json!([source])); + assert_eq!(entry["zh-CN"], zh_cn); + } let set_allowed = manifest["matches"] .as_array() .unwrap() @@ -73,6 +132,18 @@ fn settings_corpus_includes_general_mode_and_team_labels() { ); assert_eq!(settings["modes"]["General"]["zh-CN"], "综合"); assert_eq!(settings["teams"]["General"]["zh-CN"], "综合"); + assert_eq!( + settings["coverage"]["labels"], + serde_json::json!({"matched": 19, "total": 19}) + ); + assert_eq!( + settings["labels"]["Ultimate Generation - Passive Blizzard"]["zh-CN"], + "终极技能自动充能速度 暴雪" + ); + assert_eq!( + settings["labels"]["Ultimate Generation - Combat Blizzard"]["zh-CN"], + "战斗时终极技能充能速度 暴雪" + ); } #[test] @@ -132,3 +203,51 @@ fn confirmed_set_allowed_heroes_mapping_converts_in_both_directions() { assert_eq!(back_to_en.fallback_ids, Vec::::new()); assert_eq!(back_to_en.text.trim_end(), source.trim_end()); } + +#[test] +fn confirmed_legacy_aliases_convert_in_both_directions() { + let source = "variables { + global: + 0: value +} + +rule (\"legacy-aliases\") { + event { + Ongoing - Global; + } + actions { + Stop Chasing Variable(Global.value); + Force Player Hero(Event Player, Ana); + Stop Forcing Hero(Event Player); + Force Throttle(Event Player, 100, 100, 100, 100, 100, 100); + } +} +"; + let catalog = catalog(); + let to_zh = convert::convert(source, &catalog, &en(), &zh(), &ConvertOptions::default()) + .expect("confirmed legacy aliases convert to zh-CN"); + assert_eq!(to_zh.fallback_ids, Vec::::new()); + for spelling in [ + "停止追踪全局变量", + "开始强制玩家选择英雄", + "停止强制玩家选择英雄", + "开始限制阈值", + ] { + assert!( + to_zh.text.contains(spelling), + "missing {spelling}: {}", + to_zh.text + ); + } + + let back_to_en = convert::convert( + &to_zh.text, + &catalog, + &zh(), + &en(), + &ConvertOptions::default(), + ) + .expect("confirmed legacy aliases convert back to en-US"); + assert_eq!(back_to_en.fallback_ids, Vec::::new()); + assert_eq!(back_to_en.text.trim_end(), source.trim_end()); +} diff --git a/crates/workshop-rs/tests/identity.rs b/crates/workshop-rs/tests/identity.rs index d3ef2c0..d6d7825 100644 --- a/crates/workshop-rs/tests/identity.rs +++ b/crates/workshop-rs/tests/identity.rs @@ -12,7 +12,7 @@ use workshop_rs::catalog::{Catalog, Locale}; /// (`workshop-catalog-gen build`) recomputes it and the pin is updated /// deliberately together with the data. const PINNED_CATALOG_DIGEST: &str = - "f88b1a99a8e8a5d613b2f850353b144120d8952cdf2303532134ef9ccf92fca9"; + "bb8166cf0e15f8bafa0fc89d1b0df0bca9a065f6c07bf83f555f271a03bfec8b"; #[test] fn committed_catalog_digest_is_pinned() { @@ -75,7 +75,7 @@ fn locale_coverage_is_exact_and_primary_is_complete() { "declared en-US surface (168 entries + 176 members)" ); let zh = catalog.locale_coverage(&Locale::new("zh-CN")); - assert_eq!(zh.mapped, 328, "zh-CN corpus coverage is pinned"); + assert_eq!(zh.mapped, 341, "zh-CN corpus coverage is pinned"); assert_eq!(zh.total, en.total); let all = catalog.locale_coverage_all(); assert_eq!(all.len(), 2); diff --git a/crates/workshop-rs/tests/locale.rs b/crates/workshop-rs/tests/locale.rs index dc35797..1878000 100644 --- a/crates/workshop-rs/tests/locale.rs +++ b/crates/workshop-rs/tests/locale.rs @@ -3,9 +3,9 @@ //! target-locale mappings fail explicitly by default; fallback is opt-in and //! visible; settings follow the same contract. //! -//! The committed catalog declares an evidence-backed `zh-CN` corpus (328/344). +//! The committed catalog declares an evidence-backed `zh-CN` corpus (341/344). //! This suite pins both successful corpus conversion and the fail-explicit -//! behavior for the 16 entries excluded by the exact-match pipeline. +//! behavior for the 3 entries excluded by the exact-match pipeline. use workshop_rs::catalog::{Catalog, Kind, Locale}; use workshop_rs::convert::{self, ConvertOptions}; @@ -63,7 +63,7 @@ const UNMAPPED_RULE: &str = "rule (\"setup\") { Ongoing - Global; } actions { - Force Player Hero(Event Player, Ana); + Delete All Classes; } } "; @@ -80,10 +80,14 @@ fn opt_in_fallback_emits_with_recorded_fallback_ids() { let output = emitter::emit_with_options(&program, &catalog, &zh(), &options).expect("fallback emits"); assert!(output.text.contains("持续 - 全局"), "{}", output.text); - assert!(output.text.contains("Force Player Hero"), "{}", output.text); + assert!( + output.text.contains("Delete All Classes"), + "{}", + output.text + ); assert_eq!( output.fallback_ids, - vec!["forcePlayerHero".to_string()], + vec!["deleteAllClasses".to_string()], "only the excluded action uses the explicit fallback" ); } @@ -100,8 +104,8 @@ fn opt_in_fallback_conversion_round_trips_through_zh_cn() { .expect("fallback conversion emits"); assert!(!out.fallback_ids.is_empty(), "fallback is recorded"); assert!(out.text.contains("持续 - 全局"), "{}", out.text); - assert!(out.text.contains("Force Player Hero"), "{}", out.text); - assert!(out.fallback_ids.contains(&"forcePlayerHero".to_string())); + assert!(out.text.contains("Delete All Classes"), "{}", out.text); + assert!(out.fallback_ids.contains(&"deleteAllClasses".to_string())); } #[test] diff --git a/crates/workshop-rs/tests/settings_pipeline.rs b/crates/workshop-rs/tests/settings_pipeline.rs index 61c1321..01c6bf0 100644 --- a/crates/workshop-rs/tests/settings_pipeline.rs +++ b/crates/workshop-rs/tests/settings_pipeline.rs @@ -61,6 +61,33 @@ fn reviewed_settings_conversion_round_trips_en_us_and_zh_cn() { assert_eq!(collapse(&back_to_en.text), collapse(&source)); } +#[test] +fn composed_blizzard_settings_labels_convert_in_both_directions() { + let source = "settings { + heroes { + General { + Mei { + Ultimate Generation - Passive Blizzard: 0% + Ultimate Generation - Combat Blizzard: 0% + } + } + } +}"; + let catalog = catalog(); + let en = Locale::new("en-US"); + let zh = Locale::new("zh-CN"); + let to_zh = convert::convert(source, &catalog, &en, &zh, &Default::default()) + .expect("composed settings labels convert to zh-CN"); + assert!(to_zh.fallback_ids.is_empty()); + assert!(to_zh.text.contains("终极技能自动充能速度 暴雪")); + assert!(to_zh.text.contains("战斗时终极技能充能速度 暴雪")); + + let back_to_en = convert::convert(&to_zh.text, &catalog, &zh, &en, &Default::default()) + .expect("composed settings labels convert back to en-US"); + assert!(back_to_en.fallback_ids.is_empty()); + assert_eq!(collapse(&back_to_en.text), collapse(source)); +} + #[test] fn supported_apostrophe_map_name_parses() { let catalog = catalog(); diff --git a/docs/provenance.md b/docs/provenance.md index f19d28f..20067e1 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -36,24 +36,31 @@ license, reviewed) is embedded in the dataset itself and surfaced by * `en-US` is the primary locale and is complete (344/344 canonical entries: 168 builtins + 176 enum members). The committed catalog validates that the primary locale is complete. -* `zh-CN` has an evidence-backed corpus of **328/344** canonical entries: - structural 11/11, actions 56/62, values 77/78, events 3/3, operators 8/14, - and enum members 173/176. The reproducible manifest is +* `zh-CN` has an evidence-backed corpus of **341/344** canonical entries: + structural 11/11, actions 60/62, values 77/78, events 3/3, operators 14/14, + and enum members 176/176. The reproducible manifest is `tools/corpus/zh-cn-corpus.json`; it records exact en-US spelling matches, every exclusion, and the export provenance. The source is the user-provided `workshop-data/workshop-data.json` export at commit `d854bf01fc7bbf3b2169f67408c07a8da8989ad6`, commit date 2026-08-12, fetched 2026-08-17. The export is not committed to this repository. -* The generated settings corpus covers labels 17/19, modes 7/7, maps 2/2, +* The generated settings corpus covers labels 19/19, modes 7/7, maps 2/2, heroes 10/10, enum values 2/2, tokens 3/3, and teams 1/1. Its exact-match exclusions are recorded in `crates/workshop-rs/src/settings/data/zh-cn.json`; settings without a mapping continue to fail explicitly. All committed zh-CN spellings come from the JSON evidence through the corpus -pipeline. The `setAllowedHeroes` action uses the user-confirmed matching -identity/GUID for the export's `Set Player Allowed Heroes` entry. The complete -catalog coverage and settings gate remains open for the recorded exclusions. +pipeline. The confirmed legacy mappings use the export identities/GUIDs for +global stop-chasing, force hero/throttle, `Set Player Allowed Heroes`, and +the four bare comparison symbols. The three enum aliases use exact export +identity/GUID matches: Lijiang Tower Lunar New Year, Visible To and Values, +and To Nearest. The two hero settings labels are composed only after exact +template and Blizzard hero identity/GUID checks. The remaining exclusions are +`deleteAllClasses`, `chaseVariableAtRate`, and `arrayElement`, each recorded +with its exact reason in the manifest; the latter two collide with an already +declared zh-CN identity and therefore cannot be added without making parsing +ambiguous. ## Test fixtures (`tests/fixtures/`) diff --git a/tools/corpus/zh-cn-corpus.json b/tools/corpus/zh-cn-corpus.json index 48bd74b..ca10dff 100644 --- a/tools/corpus/zh-cn-corpus.json +++ b/tools/corpus/zh-cn-corpus.json @@ -1,11 +1,11 @@ { "coverage": { "actions": { - "matched": 56, + "matched": 60, "total": 62 }, "enums": { - "matched": 173, + "matched": 176, "total": 176 }, "events": { @@ -13,7 +13,7 @@ "total": 3 }, "operators": { - "matched": 8, + "matched": 14, "total": 14 }, "structural": { @@ -21,7 +21,7 @@ "total": 11 }, "total": { - "matched": 328, + "matched": 341, "total": 344 }, "values": { @@ -34,7 +34,7 @@ "en-US": "Chase Variable At Rate", "id": "chaseVariableAtRate", "kind": "action", - "reason": "no exact en-US match in the export" + "reason": "confirmed export identity maps to the zh-CN spelling already owned by chaseAtRate; adding it would make the locale parser ambiguous" }, { "en-US": "Delete All Classes", @@ -42,89 +42,11 @@ "kind": "action", "reason": "no exact en-US match in the export" }, - { - "en-US": "Force Player Hero", - "id": "forcePlayerHero", - "kind": "action", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Force Throttle", - "id": "forceThrottle", - "kind": "action", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Stop Chasing Variable", - "id": "stopChasingVariable", - "kind": "action", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Stop Forcing Hero", - "id": "stopForcingHero", - "kind": "action", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Lijiang Tower Lunar", - "id": "Map.LIJIANG_TOWER_LUNAR", - "kind": "enum member", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Visible To And Values", - "id": "ProgressBarWorldReeval.VISIBLE_TO_AND_VALUES", - "kind": "enum member", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Nearest", - "id": "Rounding.NEAREST", - "kind": "enum member", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "!=", - "id": "!=", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "<", - "id": "<", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "<=", - "id": "<=", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "==", - "id": "==", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, - { - "en-US": ">", - "id": ">", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, - { - "en-US": ">=", - "id": ">=", - "kind": "operator", - "reason": "no exact en-US match in the export" - }, { "en-US": "Array Element", "id": "arrayElement", "kind": "value", - "reason": "no exact en-US match in the export" + "reason": "confirmed export identity maps to the zh-CN spelling already owned by currentArrayElement; adding it would make the locale parser ambiguous" } ], "generator": "workshop-catalog-gen corpus", @@ -428,6 +350,24 @@ ], "zh-CN": "显示计分板" }, + { + "en-US": "Force Player Hero", + "id": "forcePlayerHero", + "kind": "action", + "sources": [ + "actions..startForcingHero" + ], + "zh-CN": "开始强制玩家选择英雄" + }, + { + "en-US": "Force Throttle", + "id": "forceThrottle", + "kind": "action", + "sources": [ + "actions..startForcingThrottle" + ], + "zh-CN": "开始限制阈值" + }, { "en-US": "Loop If Condition Is True", "id": "loopIfConditionIsTrue", @@ -599,6 +539,24 @@ ], "zh-CN": "停止镜头" }, + { + "en-US": "Stop Chasing Variable", + "id": "stopChasingVariable", + "kind": "action", + "sources": [ + "actions.__stopChasingGlobalVariable__" + ], + "zh-CN": "停止追踪全局变量" + }, + { + "en-US": "Stop Forcing Hero", + "id": "stopForcingHero", + "kind": "action", + "sources": [ + "actions..stopForcingCurrentHero" + ], + "zh-CN": "停止强制玩家选择英雄" + }, { "en-US": "Stop Forcing Throttle", "id": "stopForcingThrottle", @@ -1851,6 +1809,15 @@ ], "zh-CN": "漓江塔" }, + { + "en-US": "Lijiang Tower Lunar", + "id": "Map.LIJIANG_TOWER_LUNAR", + "kind": "enum member", + "sources": [ + "maps.lijiangTowerLny" + ], + "zh-CN": "春节漓江塔" + }, { "en-US": "Midtown", "id": "Map.MIDTOWN", @@ -2058,6 +2025,15 @@ ], "zh-CN": "根据值从数组中移除" }, + { + "en-US": "Visible To And Values", + "id": "ProgressBarWorldReeval.VISIBLE_TO_AND_VALUES", + "kind": "enum member", + "sources": [ + "constants.ProgressHudReeval.VISIBILITY_AND_VALUES" + ], + "zh-CN": "可见和值" + }, { "en-US": "Down", "id": "Rounding.DOWN", @@ -2067,6 +2043,15 @@ ], "zh-CN": "下" }, + { + "en-US": "Nearest", + "id": "Rounding.NEAREST", + "kind": "enum member", + "sources": [ + "constants.__Rounding__.__roundToNearest__" + ], + "zh-CN": "至最近" + }, { "en-US": "Up", "id": "Rounding.UP", @@ -2292,6 +2277,60 @@ ], "zh-CN": "子程序" }, + { + "en-US": "!=", + "id": "!=", + "kind": "operator", + "sources": [ + "localizedStrings.{0} != {1}" + ], + "zh-CN": "!=" + }, + { + "en-US": "<", + "id": "<", + "kind": "operator", + "sources": [ + "localizedStrings.{0} < {1}" + ], + "zh-CN": "<" + }, + { + "en-US": "<=", + "id": "<=", + "kind": "operator", + "sources": [ + "localizedStrings.{0} <= {1}" + ], + "zh-CN": "<=" + }, + { + "en-US": "==", + "id": "==", + "kind": "operator", + "sources": [ + "localizedStrings.{0} == {1}" + ], + "zh-CN": "==" + }, + { + "en-US": ">", + "id": ">", + "kind": "operator", + "sources": [ + "localizedStrings.{0} > {1}" + ], + "zh-CN": ">" + }, + { + "en-US": ">=", + "id": ">=", + "kind": "operator", + "sources": [ + "localizedStrings.{0} >= {1}" + ], + "zh-CN": ">=" + }, { "en-US": "Add", "id": "add", @@ -3164,7 +3203,7 @@ "zh-CN": "地图矢量" } ], - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), with the user-confirmed setAllowedHeroes identity/GUID mapping for the export's 'Set Player Allowed Heroes' spelling; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; the global chase and Array Element aliases are excluded when their exact zh-CN spellings would collide with canonical identities; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", "schemaVersion": 1, "source": { "commit": "d854bf01fc7bbf3b2169f67408c07a8da8989ad6", From 7b0f8c38b9d1ee627565e8406de25293df9b4f7f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 15:18:37 +0800 Subject: [PATCH 6/6] refactor(catalog): remove legacy syntax sugar identities Remove Delete All Classes, Chase Variable At Rate, and Array Element from the declared Workshop catalog per the product decision. Keep canonical global/player chase and current-array identities, and preserve fallback coverage tests with an unsupported target locale. Refs #2 --- README.md | 11 +++-- crates/workshop-rs-cli/tests/cli.rs | 14 +++--- .../src/bin/workshop-catalog-gen.rs | 8 +--- .../workshop-rs/src/catalog/data/catalog.json | 30 +------------ crates/workshop-rs/tests/corpus.rs | 4 +- crates/workshop-rs/tests/identity.rs | 6 +-- crates/workshop-rs/tests/locale.rs | 45 +++++++++++-------- docs/provenance.md | 18 ++++---- tools/corpus/zh-cn-corpus.json | 29 +++--------- 9 files changed, 62 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index c00584d..a550e02 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,12 @@ the canonical form with a fresh digest (byte-idempotent). See ## Locale status -* `en-US`: complete declared surface (344/344 canonical entries), corpus +* `en-US`: complete declared surface (341/341 canonical entries), corpus round-trips and settings emission tested. -* `zh-CN`: the reviewed export-backed corpus covers **341/344** canonical - entries (structural 11/11, actions 60/62, values 77/78, events 3/3, - operators 14/14, enum members 176/176). The 3 explicit exclusions remain - fail-explicit; settings data covers the matched declared surface and records - its exclusions in `crates/workshop-rs/src/settings/data/zh-cn.json`. +* `zh-CN`: the reviewed export-backed corpus covers **341/341** canonical + entries (structural 11/11, actions 60/60, values 77/77, events 3/3, + operators 14/14, enum members 176/176). The declared surface is complete; + settings data covers labels 19/19 and all other declared settings sections. The corpus is reproducible with the user-provided export (not committed): diff --git a/crates/workshop-rs-cli/tests/cli.rs b/crates/workshop-rs-cli/tests/cli.rs index b3fd4c6..7f5fde8 100644 --- a/crates/workshop-rs-cli/tests/cli.rs +++ b/crates/workshop-rs-cli/tests/cli.rs @@ -50,8 +50,8 @@ fn locales_lists_declared_locales_with_coverage() { let stdout = String::from_utf8(output.stdout).unwrap(); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines.len(), 2); - assert!(lines[0].starts_with("en-us 344/344"), "{stdout}"); - assert!(lines[1].starts_with("zh-cn 341/344"), "{stdout}"); + assert!(lines[0].starts_with("en-us 341/341"), "{stdout}"); + assert!(lines[1].starts_with("zh-cn 341/341"), "{stdout}"); } #[test] @@ -133,7 +133,7 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { let file = dir.join("unmapped.ws"); std::fs::write( &file, - "rule (\"setup\") { event { Ongoing - Global; } actions { Delete All Classes; } }", + "rule (\"setup\") { event { Ongoing - Global; } actions { Disable Inspector Recording; } }", ) .unwrap(); let output = run(&[ @@ -142,7 +142,7 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { "--from", "en-US", "--to", - "zh-CN", + "fr-FR", "--fallback-locale", "en-US", ]); @@ -152,11 +152,11 @@ fn convert_to_zh_cn_with_fallback_reports_the_choice() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("持续 - 全局"), "{stdout}"); - assert!(stdout.contains("Delete All Classes"), "{stdout}"); + assert!(stdout.contains("Ongoing - Global"), "{stdout}"); + assert!(stdout.contains("Disable Inspector Recording"), "{stdout}"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("fallback-locale spelling") && stderr.contains("deleteAllClasses"), + stderr.contains("fallback-locale spelling") && stderr.contains("disableInspector"), "the fallback choice is visible in tooling output: {stderr}" ); let _ = std::fs::remove_dir_all(&dir); diff --git a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs index db8fe47..8c90711 100644 --- a/crates/workshop-rs/src/bin/workshop-catalog-gen.rs +++ b/crates/workshop-rs/src/bin/workshop-catalog-gen.rs @@ -642,11 +642,7 @@ mod corpus { kind: kind.to_string(), id: id.to_string(), en: en.to_string(), - reason: match (kind, id) { - ("action", "chaseVariableAtRate") => "confirmed export identity maps to the zh-CN spelling already owned by chaseAtRate; adding it would make the locale parser ambiguous".to_string(), - ("value", "arrayElement") => "confirmed export identity maps to the zh-CN spelling already owned by currentArrayElement; adding it would make the locale parser ambiguous".to_string(), - _ => reason, - }, + reason, }), } } @@ -955,7 +951,7 @@ mod corpus { "commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or(""), "fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or(""), }, - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; the global chase and Array Element aliases are excluded when their exact zh-CN spellings would collide with canonical identities; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", "sourceReview": "reviewed: workshop-rs commits its own mapping data; the user-provided JSON is build input only and is not redistributed", "coverage": Value::Object(coverage_all), "matches": matches_json, diff --git a/crates/workshop-rs/src/catalog/data/catalog.json b/crates/workshop-rs/src/catalog/data/catalog.json index 1aedde7..925af58 100644 --- a/crates/workshop-rs/src/catalog/data/catalog.json +++ b/crates/workshop-rs/src/catalog/data/catalog.json @@ -809,13 +809,6 @@ "id": "destroyAllProgressBarInWorldText", "params": [] }, - { - "aliases": { - "en-US": "Delete All Classes" - }, - "id": "deleteAllClasses", - "params": [] - }, { "aliases": { "en-US": "Stop Chasing Variable", @@ -826,18 +819,6 @@ "Variable" ] }, - { - "aliases": { - "en-US": "Chase Variable At Rate" - }, - "id": "chaseVariableAtRate", - "params": [ - "Variable", - "Destination", - "Rate", - "Reevaluation" - ] - }, { "aliases": { "en-US": "Abort", @@ -847,7 +828,7 @@ "params": [] } ], - "digest": "bb8166cf0e15f8bafa0fc89d1b0df0bca9a065f6c07bf83f555f271a03bfec8b", + "digest": "1f84204bfd6bfd0dcaabc275185d907764e03fae0fbde54f28e162aed9935961", "enums": [ { "domain": "Color", @@ -2344,7 +2325,7 @@ "generatorVersion": "0.1.0", "license": "MIT (WrightKit-authored data, ownership transfer to workshop-rs per wright#136 direction; see docs/provenance.md)", "reviewed": true, - "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data transcribed from pinned reference probe emissions and the protect-ban entry-point reachable closure (#118); migrated to workshop-rs from the Wright-authored catalog on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains migrate the Wright-authored semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data JSON; confirmed identity/GUID mappings cover global stop-chasing, force hero/throttle, Set Player Allowed Heroes, the four bare comparison symbols, and three enum aliases; 341/344 canonical entries are covered and exclusions remain fail-explicit per ADR-0001 Decision 7" + "source": "en-US spellings transcribed from the compatibility corpus workshop snapshots and the M5 support matrix; squareRoot spelling from the Workshop emission surface for .opy sqrt(); receiver-call action/value spellings (setMoveSpeed, isAlive, getPosition, getHealth, teleport, setMaxHealth, setHealth, setAimSpeed, setGravity, setDamageDealt, setDamageReceived, setUltCharge) from the pinned en-US emission surface for .opy eventPlayer.(...) forms, evidenced by the synthetic/receiver-calls corpus fixture (#104); ChaseTimeReeval and ChaseRateReeval member spellings (None, Destination and Duration, Destination and Rate) reference-validated against the pinned enum blocks and emission (#105); chaseOverTime (Chase Global Variable Over Time), isGameInProgress (Is Game In Progress), getPlayersInRadius (Players Within Radius), worldVector (World Vector Of), getThrottle (Throttle Of), setInvisibility (Set Invisible) with the Invis domain, setStatusEffect (Set Status) with the Status domain, the Transform domain, and the LosCheck domain transcribed from the en-US emission surface for the OPY semantic manifest probes (#109); chaseAtRate (Chase Global Variable At Rate), chasePlayerVariableAtRate (Chase Player Variable At Rate), and chasePlayerVariableOverTime (Chase Player Variable Over Time) transcribed from the en-US emission surface for the chase keyword-argument probes (#110); OSTW exercised builtin params/spellings and enum domain/member data transcribed from pinned reference probe emissions and the protect-ban entry-point reachable closure (#118); migrated to workshop-rs from the Wright-authored catalog on 2026-08-16 as the canonical Workshop catalog; the chase-family expected enum domains migrate the Wright-authored semantic manifest probe data (#109/#110) into the canonical catalog so the standalone core resolves the shared bare None member without any Wright tooling dependency; zh-CN aliases are generated from the user-provided workshop-data JSON; confirmed identity/GUID mappings cover global stop-chasing, force hero/throttle, Set Player Allowed Heroes, the four bare comparison symbols, and three enum aliases; legacy/provider syntax sugar is intentionally outside the declared catalog surface; 341/341 canonical entries are covered with no exclusions" }, "schemaVersion": 1, "structural": [ @@ -2898,13 +2879,6 @@ "Value" ] }, - { - "aliases": { - "en-US": "Array Element" - }, - "id": "arrayElement", - "params": [] - }, { "aliases": { "en-US": "Current Array Index", diff --git a/crates/workshop-rs/tests/corpus.rs b/crates/workshop-rs/tests/corpus.rs index f0f44af..ab00116 100644 --- a/crates/workshop-rs/tests/corpus.rs +++ b/crates/workshop-rs/tests/corpus.rs @@ -42,9 +42,9 @@ fn manifest_pins_the_export_and_exact_match_coverage() { "d854bf01fc7bbf3b2169f67408c07a8da8989ad6" ); assert_eq!(manifest["coverage"]["total"]["matched"], 341); - assert_eq!(manifest["coverage"]["total"]["total"], 344); + assert_eq!(manifest["coverage"]["total"]["total"], 341); assert_eq!(manifest["matches"].as_array().unwrap().len(), 341); - assert_eq!(manifest["excluded"].as_array().unwrap().len(), 3); + assert_eq!(manifest["excluded"].as_array().unwrap().len(), 0); for (kind, id, source, zh_cn) in [ ( "action", diff --git a/crates/workshop-rs/tests/identity.rs b/crates/workshop-rs/tests/identity.rs index d6d7825..51728c0 100644 --- a/crates/workshop-rs/tests/identity.rs +++ b/crates/workshop-rs/tests/identity.rs @@ -12,7 +12,7 @@ use workshop_rs::catalog::{Catalog, Locale}; /// (`workshop-catalog-gen build`) recomputes it and the pin is updated /// deliberately together with the data. const PINNED_CATALOG_DIGEST: &str = - "bb8166cf0e15f8bafa0fc89d1b0df0bca9a065f6c07bf83f555f271a03bfec8b"; + "1f84204bfd6bfd0dcaabc275185d907764e03fae0fbde54f28e162aed9935961"; #[test] fn committed_catalog_digest_is_pinned() { @@ -71,8 +71,8 @@ fn locale_coverage_is_exact_and_primary_is_complete() { let en = catalog.locale_coverage(&Locale::new("en-US")); assert_eq!(en.mapped, en.total, "the primary locale is complete"); assert_eq!( - en.mapped, 344, - "declared en-US surface (168 entries + 176 members)" + en.mapped, 341, + "declared en-US surface (165 entries + 176 members)" ); let zh = catalog.locale_coverage(&Locale::new("zh-CN")); assert_eq!(zh.mapped, 341, "zh-CN corpus coverage is pinned"); diff --git a/crates/workshop-rs/tests/locale.rs b/crates/workshop-rs/tests/locale.rs index 1878000..dd209ab 100644 --- a/crates/workshop-rs/tests/locale.rs +++ b/crates/workshop-rs/tests/locale.rs @@ -3,9 +3,9 @@ //! target-locale mappings fail explicitly by default; fallback is opt-in and //! visible; settings follow the same contract. //! -//! The committed catalog declares an evidence-backed `zh-CN` corpus (341/344). +//! The committed catalog declares an evidence-backed `zh-CN` corpus (341/341). //! This suite pins both successful corpus conversion and the fail-explicit -//! behavior for the 3 entries excluded by the exact-match pipeline. +//! behavior for an unsupported undeclared target locale. use workshop_rs::catalog::{Catalog, Kind, Locale}; use workshop_rs::convert::{self, ConvertOptions}; @@ -58,12 +58,12 @@ fn conversion_en_to_zh_cn_uses_evidence_backed_mappings() { assert!(output.fallback_ids.is_empty()); } -const UNMAPPED_RULE: &str = "rule (\"setup\") { +const FALLBACK_RULE: &str = "rule (\"setup\") { event { Ongoing - Global; } actions { - Delete All Classes; + Disable Inspector Recording; } } "; @@ -73,39 +73,48 @@ fn opt_in_fallback_emits_with_recorded_fallback_ids() { // Fallback is opt-in: with a fallback locale the emission succeeds and // the fell-back identities are recorded (visible in tooling output). let catalog = builtin(); - let program = parser::parse(UNMAPPED_RULE, &catalog, &en()).expect("parses"); + let program = parser::parse(FALLBACK_RULE, &catalog, &en()).expect("parses"); let options = EmitOptions { fallback_locale: Some(en()), }; - let output = - emitter::emit_with_options(&program, &catalog, &zh(), &options).expect("fallback emits"); - assert!(output.text.contains("持续 - 全局"), "{}", output.text); + let output = emitter::emit_with_options(&program, &catalog, &Locale::new("fr-FR"), &options) + .expect("fallback emits"); + assert!(output.text.contains("Ongoing - Global"), "{}", output.text); assert!( - output.text.contains("Delete All Classes"), + output.text.contains("Disable Inspector Recording"), "{}", output.text ); assert_eq!( output.fallback_ids, - vec!["deleteAllClasses".to_string()], - "only the excluded action uses the explicit fallback" + vec!["global".to_string(), "disableInspector".to_string()], + "the unsupported target locale records the fallback identity" ); } #[test] fn opt_in_fallback_conversion_round_trips_through_zh_cn() { - // convert en -> zh-CN with fallback to en-US: mapped identities use the - // corpus while the excluded action uses the explicit fallback. + // Convert en-US to an unsupported locale with fallback to en-US. let catalog = builtin(); let options = ConvertOptions { fallback_locale: Some(en()), }; - let out = convert::convert(UNMAPPED_RULE, &catalog, &en(), &zh(), &options) - .expect("fallback conversion emits"); + let out = convert::convert( + FALLBACK_RULE, + &catalog, + &en(), + &Locale::new("fr-FR"), + &options, + ) + .expect("fallback conversion emits"); assert!(!out.fallback_ids.is_empty(), "fallback is recorded"); - assert!(out.text.contains("持续 - 全局"), "{}", out.text); - assert!(out.text.contains("Delete All Classes"), "{}", out.text); - assert!(out.fallback_ids.contains(&"deleteAllClasses".to_string())); + assert!(out.text.contains("Ongoing - Global"), "{}", out.text); + assert!( + out.text.contains("Disable Inspector Recording"), + "{}", + out.text + ); + assert!(out.fallback_ids.contains(&"disableInspector".to_string())); } #[test] diff --git a/docs/provenance.md b/docs/provenance.md index 20067e1..118878a 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -33,11 +33,11 @@ license, reviewed) is embedded in the dataset itself and surfaced by ### Locale coverage -* `en-US` is the primary locale and is complete (344/344 canonical entries: - 168 builtins + 176 enum members). The committed catalog validates that the +* `en-US` is the primary locale and is complete (341/341 canonical entries: + 165 builtins + 176 enum members). The committed catalog validates that the primary locale is complete. -* `zh-CN` has an evidence-backed corpus of **341/344** canonical entries: - structural 11/11, actions 60/62, values 77/78, events 3/3, operators 14/14, +* `zh-CN` has an evidence-backed corpus of **341/341** canonical entries: + structural 11/11, actions 60/60, values 77/77, events 3/3, operators 14/14, and enum members 176/176. The reproducible manifest is `tools/corpus/zh-cn-corpus.json`; it records exact en-US spelling matches, every exclusion, and the export provenance. The source is the user-provided @@ -56,11 +56,11 @@ global stop-chasing, force hero/throttle, `Set Player Allowed Heroes`, and the four bare comparison symbols. The three enum aliases use exact export identity/GUID matches: Lijiang Tower Lunar New Year, Visible To and Values, and To Nearest. The two hero settings labels are composed only after exact -template and Blizzard hero identity/GUID checks. The remaining exclusions are -`deleteAllClasses`, `chaseVariableAtRate`, and `arrayElement`, each recorded -with its exact reason in the manifest; the latter two collide with an already -declared zh-CN identity and therefore cannot be added without making parsing -ambiguous. +template and Blizzard hero identity/GUID checks. Following the explicit +product decision, `Delete All Classes`, `Chase Variable At Rate`, and `Array +Element` are not declared Workshop identities: they are legacy/provider syntax +sugar represented by the corresponding canonical Workshop identities. The +declared corpus is therefore complete and contains no silent exclusions. ## Test fixtures (`tests/fixtures/`) diff --git a/tools/corpus/zh-cn-corpus.json b/tools/corpus/zh-cn-corpus.json index ca10dff..c6e235c 100644 --- a/tools/corpus/zh-cn-corpus.json +++ b/tools/corpus/zh-cn-corpus.json @@ -2,7 +2,7 @@ "coverage": { "actions": { "matched": 60, - "total": 62 + "total": 60 }, "enums": { "matched": 176, @@ -22,33 +22,14 @@ }, "total": { "matched": 341, - "total": 344 + "total": 341 }, "values": { "matched": 77, - "total": 78 + "total": 77 } }, - "excluded": [ - { - "en-US": "Chase Variable At Rate", - "id": "chaseVariableAtRate", - "kind": "action", - "reason": "confirmed export identity maps to the zh-CN spelling already owned by chaseAtRate; adding it would make the locale parser ambiguous" - }, - { - "en-US": "Delete All Classes", - "id": "deleteAllClasses", - "kind": "action", - "reason": "no exact en-US match in the export" - }, - { - "en-US": "Array Element", - "id": "arrayElement", - "kind": "value", - "reason": "confirmed export identity maps to the zh-CN spelling already owned by currentArrayElement; adding it would make the locale parser ambiguous" - } - ], + "excluded": [], "generator": "workshop-catalog-gen corpus", "generatorVersion": "0.1.0", "locale": "zh-CN", @@ -3203,7 +3184,7 @@ "zh-CN": "地图矢量" } ], - "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; the global chase and Array Element aliases are excluded when their exact zh-CN spellings would collide with canonical identities; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", + "method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)", "schemaVersion": 1, "source": { "commit": "d854bf01fc7bbf3b2169f67408c07a8da8989ad6",