es_fluent_cli/utils/
ftl.rs1use crate::ftl::{extract_message_keys, parse_ftl_file};
4use anyhow::Result;
5use fluent_syntax::ast;
6use std::collections::HashSet;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub fn main_ftl_path(assets_dir: &Path, locale: &str, crate_name: &str) -> PathBuf {
12 assets_dir.join(locale).join(format!("{}.ftl", crate_name))
13}
14
15pub fn locale_output_dir(assets_dir: &Path, locale: &str) -> PathBuf {
17 assets_dir.join(locale)
18}
19
20pub fn discover_ftl_files(
22 assets_dir: &Path,
23 locale: &str,
24 crate_name: &str,
25) -> Result<Vec<FtlFileInfo>> {
26 let mut files = Vec::new();
27 let locale_dir = assets_dir.join(locale);
28
29 let main_file = locale_dir.join(format!("{}.ftl", crate_name));
31 if main_file.exists() {
32 files.push(FtlFileInfo::new(
33 main_file.clone(),
34 PathBuf::from(format!("{}.ftl", crate_name)),
35 ));
36 }
37
38 let crate_subdir = locale_dir.join(crate_name);
40 if crate_subdir.exists() && crate_subdir.is_dir() {
41 discover_ftl_files_recursive(&crate_subdir, &locale_dir, &mut files)?;
42 }
43
44 Ok(files)
45}
46
47fn discover_ftl_files_recursive(
49 dir: &Path,
50 base_dir: &Path,
51 files: &mut Vec<FtlFileInfo>,
52) -> Result<()> {
53 for entry in fs::read_dir(dir)? {
54 let entry = entry?;
55 let path = entry.path();
56
57 if path.is_dir() {
58 discover_ftl_files_recursive(&path, base_dir, files)?;
60 } else if path.extension().is_some_and(|e| e == "ftl") {
61 let relative_path = path.strip_prefix(base_dir).map_err(|_| {
63 anyhow::anyhow!("Failed to calculate relative path for {}", path.display())
64 })?;
65
66 files.push(FtlFileInfo::new(path.clone(), relative_path.to_path_buf()));
67 }
68 }
69
70 Ok(())
71}
72
73#[derive(Clone, Debug)]
75pub struct FtlFileInfo {
76 pub abs_path: PathBuf,
78 pub relative_path: PathBuf,
80}
81
82impl FtlFileInfo {
83 pub fn new(abs_path: PathBuf, relative_path: PathBuf) -> Self {
85 Self {
86 abs_path,
87 relative_path,
88 }
89 }
90}
91
92#[derive(Clone, Debug)]
94pub struct LoadedFtlFile {
95 pub abs_path: PathBuf,
97 pub relative_path: PathBuf,
99 pub resource: ast::Resource<String>,
101 pub keys: HashSet<String>,
103}
104
105pub fn load_ftl_files(files: Vec<FtlFileInfo>) -> Result<Vec<LoadedFtlFile>> {
107 let mut loaded_files = Vec::new();
108
109 for file_info in files {
110 if file_info.abs_path.exists() {
111 let resource = parse_ftl_file(&file_info.abs_path)?;
112 let keys = extract_message_keys(&resource);
113
114 loaded_files.push(LoadedFtlFile {
115 abs_path: file_info.abs_path.clone(),
116 relative_path: file_info.relative_path.clone(),
117 resource,
118 keys,
119 });
120 }
121 }
122
123 Ok(loaded_files)
124}
125
126pub fn discover_and_load_ftl_files(
128 assets_dir: &Path,
129 locale: &str,
130 crate_name: &str,
131) -> Result<Vec<LoadedFtlFile>> {
132 let files = discover_ftl_files(assets_dir, locale, crate_name)?;
133 load_ftl_files(files)
134}
135
136pub fn parse_ftl_file_with_errors(
138 ftl_path: &Path,
139) -> Result<(
140 fluent_syntax::ast::Resource<String>,
141 Vec<fluent_syntax::parser::ParserError>,
142)> {
143 if !ftl_path.exists() {
144 return Ok((
145 fluent_syntax::ast::Resource { body: Vec::new() },
146 Vec::new(),
147 ));
148 }
149
150 let content = fs::read_to_string(ftl_path)?;
151
152 if content.trim().is_empty() {
153 return Ok((
154 fluent_syntax::ast::Resource { body: Vec::new() },
155 Vec::new(),
156 ));
157 }
158
159 match fluent_syntax::parser::parse(content) {
160 Ok(res) => Ok((res, Vec::new())),
161 Err((res, errors)) => Ok((res, errors)),
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use assert_fs::TempDir;
168
169 use super::*;
170 use std::fs;
171
172 #[test]
173 fn test_ftl_file_info_new() {
174 let abs_path = PathBuf::from("/test/example.ftl");
175 let relative_path = PathBuf::from("example.ftl");
176 let info = FtlFileInfo::new(abs_path.clone(), relative_path.clone());
177
178 assert_eq!(info.abs_path, abs_path);
179 assert_eq!(info.relative_path, relative_path);
180 }
181
182 #[test]
183 fn test_discover_ftl_files_main_only() {
184 let temp_dir = TempDir::new().unwrap();
185 let locale_dir = temp_dir.path().join("en");
186 fs::create_dir_all(&locale_dir).unwrap();
187
188 let main_ftl = locale_dir.join("test-crate.ftl");
190 fs::write(&main_ftl, "hello = Hello\nworld = World").unwrap();
191
192 let files = discover_ftl_files(temp_dir.path(), "en", "test-crate").unwrap();
193
194 assert_eq!(files.len(), 1);
195 assert_eq!(files[0].relative_path, PathBuf::from("test-crate.ftl"));
196 assert_eq!(files[0].abs_path, main_ftl);
197 }
198
199 #[test]
200 fn test_discover_ftl_files_with_namespace() {
201 let temp_dir = TempDir::new().unwrap();
202 let locale_dir = temp_dir.path().join("en");
203 let crate_dir = locale_dir.join("test-crate");
204 fs::create_dir_all(&crate_dir).unwrap();
205
206 let main_ftl = locale_dir.join("test-crate.ftl");
208 fs::write(&main_ftl, "hello = Hello").unwrap();
209
210 let namespace_ftl = crate_dir.join("ui.ftl");
212 fs::write(&namespace_ftl, "button = Click").unwrap();
213
214 let files = discover_ftl_files(temp_dir.path(), "en", "test-crate").unwrap();
215
216 assert_eq!(files.len(), 2);
217
218 let main_file = files
220 .iter()
221 .find(|f| f.relative_path == PathBuf::from("test-crate.ftl"))
222 .unwrap();
223 assert_eq!(main_file.abs_path, main_ftl);
224
225 let ns_file = files
227 .iter()
228 .find(|f| f.relative_path == PathBuf::from("test-crate/ui.ftl"))
229 .unwrap();
230 assert_eq!(ns_file.abs_path, namespace_ftl);
231 }
232
233 #[test]
234 fn test_discover_and_load_ftl_files() {
235 let temp_dir = TempDir::new().unwrap();
236 let locale_dir = temp_dir.path().join("en");
237 fs::create_dir_all(&locale_dir).unwrap();
238
239 let ftl_path = locale_dir.join("test-crate.ftl");
241 fs::write(&ftl_path, "hello = Hello { $name }").unwrap();
242
243 let loaded_files =
244 discover_and_load_ftl_files(temp_dir.path(), "en", "test-crate").unwrap();
245
246 assert_eq!(loaded_files.len(), 1);
247 assert_eq!(
248 loaded_files[0].relative_path,
249 PathBuf::from("test-crate.ftl")
250 );
251 assert_eq!(loaded_files[0].abs_path, ftl_path);
252 assert!(loaded_files[0].resource.body.len() > 0);
253 assert!(loaded_files[0].keys.contains("hello"));
254 }
255}