Skip to main content

fluent_zero_build/
lib.rs

1use std::{env, fs, io::Write as _, path::Path};
2
3use fluent_syntax::parser;
4use unic_langid::LanguageIdentifier;
5
6/// Generates the static cache code for `fluent-zero`.
7///
8/// This function reads Fluent (`.ftl`) files from the specified directory, parses them,
9/// and generates a Rust file (`static_cache.rs`) in the `OUT_DIR`.
10///
11/// # Process
12///
13/// 1. Scans `locales_dir_path` for language subdirectories (e.g., `en-US`).
14/// 2. Parses every `.ftl` file found.
15/// 3. Identifies **Static** messages (no variables, standard text) vs **Dynamic** messages.
16/// 4. Generates:
17///    - `CACHE`: A Perfect Hash Map (PHF) mapping keys to `CacheEntry::Static(&str)` or `CacheEntry::Dynamic`.
18///    - `LOCALES`: A Map of Lazy-loaded `ConcurrentFluentBundle`s for fallback/dynamic resolution.
19///
20/// # Arguments
21///
22/// * `locales_dir_path` - Relative path to the folder containing locale subdirectories.
23#[allow(clippy::too_many_lines)]
24pub fn generate_static_cache(locales_dir_path: &str) {
25    println!("cargo:rerun-if-changed={locales_dir_path}");
26
27    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
28    let dest_path = Path::new(&out_dir).join("static_cache.rs");
29    let mut file = fs::File::create(&dest_path).expect("Failed to create static_cache.rs");
30
31    writeln!(&mut file, "// @generated by fluent-zero-build").unwrap();
32
33    let locales_path = Path::new(locales_dir_path);
34    if !locales_path.exists() {
35        return;
36    }
37
38    let mut bundle_entries: Vec<(String, String)> = Vec::new();
39    let mut cache_root_entries: Vec<(String, String)> = Vec::new();
40
41    for entry in fs::read_dir(locales_path).unwrap() {
42        let entry = entry.unwrap();
43        let path = entry.path();
44
45        if path.is_dir() {
46            let dir_name = path.file_name().unwrap().to_str().unwrap();
47            let lang_id: LanguageIdentifier = match dir_name.parse() {
48                Ok(id) => id,
49                Err(_) => continue,
50            };
51            let lang_key = lang_id.to_string();
52            let sanitized_lang = lang_key.replace('-', "_").to_uppercase();
53
54            // Define variable names
55            let cache_name = format!("CACHE_{sanitized_lang}");
56            let bundle_name = format!("BUNDLE_{sanitized_lang}");
57
58            let mut combined_ftl_source = String::new();
59            // Vector of (Key, ValueCode)
60            let mut cache_entries: Vec<(String, String)> = Vec::new();
61
62            for file_entry in fs::read_dir(&path).unwrap() {
63                let file_entry = file_entry.unwrap();
64                let file_path = file_entry.path();
65
66                if file_path.extension().is_some_and(|ext| ext == "ftl") {
67                    println!("cargo:rerun-if-changed={}", file_path.display());
68                    let source = fs::read_to_string(&file_path).unwrap();
69                    combined_ftl_source.push_str(&source);
70                    combined_ftl_source.push('\n');
71
72                    let ast = parser::parse(source).expect("Failed to parse FTL");
73                    for entry in ast.body {
74                        if let fluent_syntax::ast::Entry::Message(msg) = entry {
75                            // Check if Simple Static
76                            // Criteria:
77                            // 1. Has a value.
78                            // 2. Only one element in the pattern.
79                            // 3. That element is Text (not a Variable).
80                            // 4. No escape characters (simplifies generation).
81                            if let Some(pattern) = &msg.value
82                                && pattern.elements.len() == 1
83                                && let fluent_syntax::ast::PatternElement::TextElement { value } =
84                                    &pattern.elements[0]
85                                && !value.contains('\\')
86                            {
87                                // Entry::Static("value")
88                                // Stored directly in the binary .rodata
89                                cache_entries.push((
90                                    msg.id.name.clone(),
91                                    format!("::fluent_zero::CacheEntry::Static(\"{value}\")"),
92                                ));
93                            } else {
94                                // Entry::Dynamic
95                                // Requires parsing by FluentBundle at runtime
96                                cache_entries.push((
97                                    msg.id.name.clone(),
98                                    "::fluent_zero::CacheEntry::Dynamic".to_string(),
99                                ));
100                            }
101                        }
102                    }
103                }
104            }
105
106            // 1. Write the Bundle Static Item
107            // We use LazyLock to ensure we only parse the FTL for the bundle if we actually
108            // hit a dynamic message for this specific locale.
109            let escaped_ftl = format!("{combined_ftl_source:?}");
110            let bundle_init_code = format!(
111                "std::sync::LazyLock::new(|| {{
112                    let lang: ::fluent_zero::LanguageIdentifier = \"{lang_key}\".parse().unwrap();
113                    let mut bundle = ::fluent_zero::ConcurrentFluentBundle::new_concurrent(vec![lang]); 
114                    let res = ::fluent_zero::FluentResource::try_new({escaped_ftl}.to_string()).expect(\"FTL Error\");
115                    bundle.add_resource(res).expect(\"Resource Error\");
116                    bundle
117                }})"
118            );
119
120            writeln!(&mut file,
121                "static {bundle_name}: std::sync::LazyLock<::fluent_zero::ConcurrentFluentBundle<::fluent_zero::FluentResource>> = {bundle_init_code};"
122            ).unwrap();
123
124            bundle_entries.push((lang_key.clone(), format!("&{bundle_name}")));
125
126            // 2. Write Unified Cache Map
127            let mut map = phf_codegen::Map::new();
128            map.phf_path("::fluent_zero::phf");
129            for (k, v) in &cache_entries {
130                map.entry(k.as_str(), v.as_str());
131            }
132
133            writeln!(
134                &mut file,
135                "static {}: ::fluent_zero::phf::Map<&'static str, ::fluent_zero::CacheEntry> = {};",
136                cache_name,
137                map.build()
138            )
139            .unwrap();
140            cache_root_entries.push((lang_key.clone(), cache_name));
141        }
142    }
143
144    // 3. Generate Root Maps
145
146    // Unified Cache Root
147    let mut root_map = phf_codegen::Map::new();
148    root_map.phf_path("::fluent_zero::phf");
149    for (l, v) in &cache_root_entries {
150        root_map.entry(l.as_str(), format!("&{v}"));
151    }
152    writeln!(&mut file,
153        "pub static CACHE: ::fluent_zero::phf::Map<&'static str, &'static ::fluent_zero::phf::Map<&'static str, ::fluent_zero::CacheEntry>> = {};",
154        root_map.build()
155    ).unwrap();
156
157    // Locales Root
158    let mut bundle_map = phf_codegen::Map::new();
159    bundle_map.phf_path("::fluent_zero::phf");
160    for (l, c) in &bundle_entries {
161        bundle_map.entry(l.as_str(), c.as_str());
162    }
163    writeln!(&mut file,
164        "pub static LOCALES: ::fluent_zero::phf::Map<&'static str, &'static std::sync::LazyLock<::fluent_zero::ConcurrentFluentBundle<::fluent_zero::FluentResource>>> = {};",
165        bundle_map.build()
166    ).unwrap();
167}