es_fluent_cli/ftl/
locale.rs1use crate::core::CrateInfo;
7use crate::utils::get_all_locales;
8use anyhow::{Context as _, Result};
9use es_fluent_toml::I18nConfig;
10use std::collections::HashSet;
11use std::path::PathBuf;
12
13#[derive(Clone, Debug)]
18pub struct LocaleContext {
19 pub assets_dir: PathBuf,
21 pub fallback: String,
23 pub locales: Vec<String>,
25 pub crate_name: String,
27}
28
29impl LocaleContext {
30 pub fn from_crate(krate: &CrateInfo, all: bool) -> Result<Self> {
35 let config = I18nConfig::read_from_path(&krate.i18n_config_path)
36 .with_context(|| format!("Failed to read {}", krate.i18n_config_path.display()))?;
37
38 let assets_dir = krate.manifest_dir.join(&config.assets_dir);
39
40 let locales = if all {
41 get_all_locales(&assets_dir)?
42 } else {
43 vec![config.fallback_language.clone()]
44 };
45
46 Ok(Self {
47 assets_dir,
48 fallback: config.fallback_language,
49 locales,
50 crate_name: krate.name.clone(),
51 })
52 }
53
54 pub fn ftl_path(&self, locale: &str) -> PathBuf {
56 self.assets_dir
57 .join(locale)
58 .join(format!("{}.ftl", self.crate_name))
59 }
60
61 pub fn locale_dir(&self, locale: &str) -> PathBuf {
63 self.assets_dir.join(locale)
64 }
65
66 pub fn iter(&self) -> impl Iterator<Item = (&str, PathBuf)> {
70 self.locales.iter().filter_map(|locale| {
71 let locale_dir = self.locale_dir(locale);
72 if locale_dir.exists() {
73 Some((locale.as_str(), self.ftl_path(locale)))
74 } else {
75 None
76 }
77 })
78 }
79
80 pub fn iter_non_fallback(&self) -> impl Iterator<Item = (&str, PathBuf)> {
84 self.locales.iter().filter_map(|locale| {
85 if locale == &self.fallback {
86 return None;
87 }
88 let locale_dir = self.locale_dir(locale);
89 if locale_dir.exists() {
90 Some((locale.as_str(), self.ftl_path(locale)))
91 } else {
92 None
93 }
94 })
95 }
96
97 pub fn is_fallback(&self, locale: &str) -> bool {
99 locale == self.fallback
100 }
101}
102
103pub fn collect_all_available_locales(crates: &[CrateInfo]) -> Result<HashSet<String>> {
105 let mut all_locales = HashSet::new();
106
107 for krate in crates {
108 let ctx = LocaleContext::from_crate(krate, true)?;
109 for locale in ctx.locales {
110 all_locales.insert(locale);
111 }
112 }
113
114 Ok(all_locales)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::fs;
121 use std::path::PathBuf;
122 use tempfile::tempdir;
123
124 fn create_test_crate() -> (tempfile::TempDir, CrateInfo) {
125 let temp_dir = tempdir().unwrap();
126 let assets = temp_dir.path().join("i18n");
127 fs::create_dir(&assets).unwrap();
128 fs::create_dir(assets.join("en")).unwrap();
129 fs::create_dir(assets.join("fr")).unwrap();
130 fs::create_dir(assets.join("de")).unwrap();
131
132 let config_path = temp_dir.path().join("i18n.toml");
133 fs::write(
134 &config_path,
135 "fallback_language = \"en\"\nassets_dir = \"i18n\"\n",
136 )
137 .unwrap();
138
139 let krate = CrateInfo {
140 name: "test-crate".to_string(),
141 manifest_dir: temp_dir.path().to_path_buf(),
142 src_dir: temp_dir.path().join("src"),
143 i18n_config_path: config_path,
144 ftl_output_dir: assets.join("en"),
145 has_lib_rs: true,
146 fluent_features: Vec::new(),
147 };
148
149 (temp_dir, krate)
150 }
151
152 #[test]
153 fn test_collect_all_available_locales() {
154 let temp_dir = tempdir().unwrap();
155 let assets = temp_dir.path().join("i18n");
156 fs::create_dir(&assets).unwrap();
157 fs::create_dir(assets.join("en")).unwrap();
158 fs::create_dir(assets.join("fr")).unwrap();
159 fs::create_dir(assets.join("de")).unwrap();
160
161 let config_path = temp_dir.path().join("i18n.toml");
163 fs::write(
164 &config_path,
165 "fallback_language = \"en\"\nassets_dir = \"i18n\"\n",
166 )
167 .unwrap();
168
169 let crates = vec![CrateInfo {
170 name: "test-crate".to_string(),
171 manifest_dir: temp_dir.path().to_path_buf(),
172 src_dir: PathBuf::new(),
173 i18n_config_path: config_path,
174 ftl_output_dir: PathBuf::new(),
175 has_lib_rs: true,
176 fluent_features: Vec::new(),
177 }];
178
179 let locales = collect_all_available_locales(&crates).unwrap();
180
181 assert!(locales.contains("en"));
182 assert!(locales.contains("fr"));
183 assert!(locales.contains("de"));
184 assert_eq!(locales.len(), 3);
185 assert!(!locales.contains("awd"));
186 }
187
188 #[test]
189 fn test_locale_context_fallback_only() {
190 let (_temp, krate) = create_test_crate();
191 let ctx = LocaleContext::from_crate(&krate, false).unwrap();
192
193 assert_eq!(ctx.locales.len(), 1);
194 assert_eq!(ctx.locales[0], "en");
195 assert_eq!(ctx.fallback, "en");
196 }
197
198 #[test]
199 fn test_locale_context_all_locales() {
200 let (_temp, krate) = create_test_crate();
201 let ctx = LocaleContext::from_crate(&krate, true).unwrap();
202
203 assert_eq!(ctx.locales.len(), 3);
204 assert!(ctx.locales.contains(&"en".to_string()));
205 assert!(ctx.locales.contains(&"fr".to_string()));
206 assert!(ctx.locales.contains(&"de".to_string()));
207 }
208
209 #[test]
210 fn test_ftl_path() {
211 let (_temp, krate) = create_test_crate();
212 let ctx = LocaleContext::from_crate(&krate, false).unwrap();
213
214 let path = ctx.ftl_path("en");
215 assert!(path.ends_with("i18n/en/test-crate.ftl"));
216 }
217
218 #[test]
219 fn test_iter_non_fallback() {
220 let (_temp, krate) = create_test_crate();
221 let ctx = LocaleContext::from_crate(&krate, true).unwrap();
222
223 let non_fallback: Vec<_> = ctx.iter_non_fallback().map(|(l, _)| l).collect();
224 assert!(!non_fallback.contains(&"en"));
225 assert!(non_fallback.contains(&"fr"));
226 assert!(non_fallback.contains(&"de"));
227 }
228
229 #[test]
230 fn test_iter_and_is_fallback_cover_directory_presence() {
231 let (temp, krate) = create_test_crate();
232 std::fs::remove_dir_all(temp.path().join("i18n/fr")).unwrap();
234
235 let ctx = LocaleContext::from_crate(&krate, true).unwrap();
236 let locales_from_iter: Vec<_> = ctx.iter().map(|(l, _)| l.to_string()).collect();
237
238 assert!(ctx.is_fallback("en"));
239 assert!(!ctx.is_fallback("de"));
240 assert!(locales_from_iter.contains(&"en".to_string()));
241 assert!(!locales_from_iter.contains(&"fr".to_string()));
242 }
243}