Skip to main content

fluent_zero_build/
lib.rs

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