Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
140 lines (127 loc) · 5.2 KB
/
Copy pathbuild.rs
File metadata and controls
140 lines (127 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:rerun-if-env-changed=CODMIN_I18N_STRICT");
println!("cargo:rerun-if-env-changed=CI");
println!(
"cargo:rerun-if-changed={}",
manifest_dir.join("Cargo.toml").display()
);
check_dioxus_sources(&manifest_dir);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
generate_i18n_tables(&manifest_dir, &out_dir);
}
fn check_dioxus_sources(manifest_dir: &Path) {
const DIOXUS_VERSION: &str = "=0.7.10";
let cargo_toml_path = manifest_dir.join("Cargo.toml");
let raw = fs::read_to_string(&cargo_toml_path)
.unwrap_or_else(|err| panic!("read {}: {}", cargo_toml_path.display(), err));
for crate_name in ["dioxus", "dioxus-logger"] {
let line = raw
.lines()
.find(|line| line.trim_start().starts_with(crate_name))
.unwrap_or_else(|| panic!("Cargo.toml Dioxus version drift: missing {crate_name}"));
assert!(
line.contains(DIOXUS_VERSION),
"Cargo.toml Dioxus version drift: {crate_name} must use {DIOXUS_VERSION}"
);
}
}
/// Reads the externalized flat-JSON translation files (`i18n/en.json`,
/// `i18n/zh-CN.json`) and emits a sorted, compile-time static lookup table
/// (`i18n_tables.rs`) into `OUT_DIR`. The generated tables are plain
/// `&'static [(&'static str, &'static str)]` slices so the runtime does zero
/// heap allocation and keeps no `HashMap`; `i18n.rs` resolves keys with a
/// `binary_search_by`.
fn generate_i18n_tables(manifest_dir: &Path, out_dir: &Path) {
let i18n_dir = manifest_dir.join("i18n");
let en_path = i18n_dir.join("en.json");
let zh_path = i18n_dir.join("zh-CN.json");
println!("cargo:rerun-if-changed={}", en_path.display());
println!("cargo:rerun-if-changed={}", zh_path.display());
let en = load_i18n_table(&en_path);
let zh = load_i18n_table(&zh_path);
// Consistency check: key-set drift is a hard error in strict/CI mode.
let en_keys: BTreeSet<&str> = en.iter().map(|(k, _)| k.as_str()).collect();
let zh_keys: BTreeSet<&str> = zh.iter().map(|(k, _)| k.as_str()).collect();
let missing_in_zh: Vec<&str> = en_keys.difference(&zh_keys).copied().collect();
let missing_in_en: Vec<&str> = zh_keys.difference(&en_keys).copied().collect();
let strict_i18n = i18n_strict();
if !missing_in_zh.is_empty() {
let message = format!(
"{} key(s) present in en.json but missing in zh-CN.json: {}",
missing_in_zh.len(),
missing_in_zh.join(", ")
);
if strict_i18n {
panic!("i18n: {message}");
}
println!("cargo:warning=i18n: {message}");
}
if !missing_in_en.is_empty() {
let message = format!(
"{} key(s) present in zh-CN.json but missing in en.json: {}",
missing_in_en.len(),
missing_in_en.join(", ")
);
if strict_i18n {
panic!("i18n: {message}");
}
println!("cargo:warning=i18n: {message}");
}
let mut out = String::new();
out.push_str("// @generated by codmin build.rs; do not edit by hand.\n");
render_i18n_table(&mut out, "I18N_EN", &en);
render_i18n_table(&mut out, "I18N_ZH_CN", &zh);
fs::write(out_dir.join("i18n_tables.rs"), out).expect("write i18n tables");
}
fn i18n_strict() -> bool {
env::var("CODMIN_I18N_STRICT").ok().as_deref() == Some("1")
|| env::var("CI").ok().as_deref() == Some("true")
|| env::var("CI").ok().as_deref() == Some("1")
}
/// Parses a flat `{ "key": "value", ... }` JSON object into a key-sorted
/// vector, ready for `binary_search_by` at runtime.
fn load_i18n_table(path: &Path) -> Vec<(String, String)> {
let raw = fs::read_to_string(path)
.unwrap_or_else(|err| panic!("read i18n file {}: {}", path.display(), err));
let value: serde_json::Value = serde_json::from_str(&raw)
.unwrap_or_else(|err| panic!("parse i18n JSON {}: {}", path.display(), err));
let object = value
.as_object()
.unwrap_or_else(|| panic!("i18n file {} is not a JSON object", path.display()));
let mut entries: Vec<(String, String)> = object
.iter()
.map(|(key, val)| {
let text = val.as_str().unwrap_or_else(|| {
panic!(
"i18n file {} key `{}` is not a string value",
path.display(),
key
)
});
(key.clone(), text.to_owned())
})
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries.dedup_by(|a, b| a.0 == b.0);
entries
}
fn render_i18n_table(out: &mut String, name: &str, entries: &[(String, String)]) {
out.push_str("pub static ");
out.push_str(name);
out.push_str(": &[(&str, &str)] = &[\n");
for (key, value) in entries {
out.push_str(" (");
out.push_str("e(key));
out.push_str(", ");
out.push_str("e(value));
out.push_str("),\n");
}
out.push_str("];\n");
}
fn quote(value: &str) -> String {
serde_json::to_string(value).expect("quote string")
}