1use std::{
2 collections::BTreeSet,
3 env, fs,
4 io::Write as _,
5 path::{Path, PathBuf},
6};
7
8use fluent_syntax::parser;
9use unic_langid::LanguageIdentifier;
10
11pub struct FluentZeroBuilder {
13 locales_dir: PathBuf,
14 charset_dest: Option<PathBuf>,
15}
16
17impl FluentZeroBuilder {
18 #[must_use]
20 pub fn new<P: AsRef<Path>>(locales_dir_path: P) -> Self {
21 Self {
22 locales_dir: locales_dir_path.as_ref().to_path_buf(),
23 charset_dest: None,
24 }
25 }
26
27 #[must_use]
31 pub fn export_charset<P: AsRef<Path>>(mut self, dest: P) -> Self {
32 self.charset_dest = Some(dest.as_ref().to_path_buf());
33 self
34 }
35
36 #[allow(clippy::too_many_lines)]
38 pub fn generate(self) {
39 println!("cargo:rerun-if-changed={}", self.locales_dir.display());
40
41 let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
42 let dest_path = Path::new(&out_dir).join("static_cache.rs");
43 let mut file = fs::File::create(&dest_path).expect("Failed to create static_cache.rs");
44
45 writeln!(&mut file, "// @generated by fluent-zero-build").unwrap();
46
47 if !self.locales_dir.exists() {
48 return;
49 }
50
51 let mut bundle_entries: Vec<(String, String)> = Vec::new();
52 let mut cache_root_entries: Vec<(String, String)> = Vec::new();
53
54 let mut unique_chars: BTreeSet<char> = BTreeSet::new();
56
57 for entry in fs::read_dir(&self.locales_dir).unwrap() {
58 let entry = entry.unwrap();
59 let path = entry.path();
60
61 if path.is_dir() {
62 let dir_name = path.file_name().unwrap().to_str().unwrap();
63 let lang_id: LanguageIdentifier = match dir_name.parse() {
64 Ok(id) => id,
65 Err(_) => continue,
66 };
67 let lang_key = lang_id.to_string();
68 let sanitized_lang = lang_key.replace('-', "_").to_uppercase();
69
70 let cache_name = format!("CACHE_{sanitized_lang}");
72 let bundle_name = format!("BUNDLE_{sanitized_lang}");
73
74 let mut combined_ftl_source = String::new();
75 let mut cache_entries: Vec<(String, String)> = Vec::new();
77
78 for file_entry in fs::read_dir(&path).unwrap() {
79 let file_entry = file_entry.unwrap();
80 let file_path = file_entry.path();
81
82 if file_path.extension().is_some_and(|ext| ext == "ftl") {
83 println!("cargo:rerun-if-changed={}", file_path.display());
84 let source = fs::read_to_string(&file_path).unwrap();
85 combined_ftl_source.push_str(&source);
86 combined_ftl_source.push('\n');
87
88 let ast = parser::parse(source).expect("Failed to parse FTL");
89 for entry in ast.body {
90 if let fluent_syntax::ast::Entry::Message(msg) = entry {
91 if let Some(pattern) = &msg.value {
93 for element in &pattern.elements {
94 if let fluent_syntax::ast::PatternElement::TextElement {
95 value,
96 } = element
97 {
98 for ch in value.chars() {
99 if !ch.is_control() {
101 unique_chars.insert(ch);
102 }
103 }
104 }
105 }
106 }
107
108 if let Some(pattern) = &msg.value
116 && pattern.elements.len() == 1
117 && let fluent_syntax::ast::PatternElement::TextElement { value } =
118 &pattern.elements[0]
119 && !value.contains('\\')
120 {
121 cache_entries.push((
124 msg.id.name.clone(),
125 format!("::fluent_zero::CacheEntry::Static(\"{value}\")"),
126 ));
127 } else {
128 cache_entries.push((
131 msg.id.name.clone(),
132 "::fluent_zero::CacheEntry::Dynamic".to_string(),
133 ));
134 }
135 }
136 }
137 }
138 }
139
140 let escaped_ftl = format!("{combined_ftl_source:?}");
144 let bundle_init_code = format!(
145 "std::sync::LazyLock::new(|| {{
146 let lang: ::fluent_zero::LanguageIdentifier = \"{lang_key}\".parse().unwrap();
147 let mut bundle = ::fluent_zero::ConcurrentFluentBundle::new_concurrent(vec![lang]);
148 let res = ::fluent_zero::FluentResource::try_new({escaped_ftl}.to_string()).expect(\"FTL Error\");
149 bundle.add_resource(res).expect(\"Resource Error\");
150 bundle
151 }})"
152 );
153
154 writeln!(&mut file,
155 "static {bundle_name}: std::sync::LazyLock<::fluent_zero::ConcurrentFluentBundle<::fluent_zero::FluentResource>> = {bundle_init_code};"
156 ).unwrap();
157
158 bundle_entries.push((lang_key.clone(), format!("&{bundle_name}")));
159
160 let mut map = phf_codegen::Map::new();
162 map.phf_path("::fluent_zero::phf");
163 for (k, v) in &cache_entries {
164 map.entry(k.as_str(), v.as_str());
165 }
166
167 writeln!(
168 &mut file,
169 "static {}: ::fluent_zero::phf::Map<&'static str, ::fluent_zero::CacheEntry> = {};",
170 cache_name,
171 map.build()
172 )
173 .unwrap();
174 cache_root_entries.push((lang_key.clone(), cache_name));
175 }
176 }
177
178 let mut root_map = phf_codegen::Map::new();
182 root_map.phf_path("::fluent_zero::phf");
183 for (l, v) in &cache_root_entries {
184 root_map.entry(l.as_str(), format!("&{v}"));
185 }
186 writeln!(&mut file,
187 "pub static CACHE: ::fluent_zero::phf::Map<&'static str, &'static ::fluent_zero::phf::Map<&'static str, ::fluent_zero::CacheEntry>> = {};",
188 root_map.build()
189 ).unwrap();
190
191 let mut bundle_map = phf_codegen::Map::new();
193 bundle_map.phf_path("::fluent_zero::phf");
194 for (l, c) in &bundle_entries {
195 bundle_map.entry(l.as_str(), c.as_str());
196 }
197 writeln!(&mut file,
198 "pub static LOCALES: ::fluent_zero::phf::Map<&'static str, &'static std::sync::LazyLock<::fluent_zero::ConcurrentFluentBundle<::fluent_zero::FluentResource>>> = {};",
199 bundle_map.build()
200 ).unwrap();
201
202 if let Some(charset_dest) = self.charset_dest {
204 let charset_string: String = unique_chars.into_iter().collect();
205 fs::write(&charset_dest, charset_string).expect("Failed to write fluent charset file");
206
207 println!("cargo:rerun-if-changed={}", charset_dest.display());
210 }
211 }
212}
213
214pub fn generate_static_cache(locales_dir_path: &str) {
223 FluentZeroBuilder::new(locales_dir_path).generate();
224}