zng_ext_l10n/sources/
dir.rs1use std::{collections::HashMap, io, path::PathBuf, str::FromStr as _, sync::Arc};
2
3use semver::Version;
4use zng_clone_move::clmv;
5use zng_ext_fs_watcher::WATCHER;
6use zng_txt::Txt;
7use zng_var::{ArcEq, Var, WeakVar, const_var, var, weak_var};
8
9use crate::{FluentParserErrors, L10nSource, Lang, LangFilePath, LangMap, LangResourceStatus};
10
11pub struct L10nDir {
16 dir_watch: Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
17 dir_watch_status: Var<LangResourceStatus>,
18 res: HashMap<(Lang, LangFilePath), L10nFile>,
19}
20impl L10nDir {
21 pub fn open(dir: impl Into<PathBuf>) -> Self {
23 Self::new(dir.into())
24 }
25 fn new(dir: PathBuf) -> Self {
26 let (dir_watch, status) = WATCHER.read_dir_status(
27 dir.clone(),
28 true,
29 Arc::default(),
30 clmv!(|d| {
31 let mut set: LangMap<HashMap<LangFilePath, PathBuf>> = LangMap::new();
32 let mut errors: Vec<Arc<dyn std::error::Error + Send + Sync>> = vec![];
33 let mut dir = None;
34 for entry in d.min_depth(0).max_depth(5) {
35 let entry = match entry {
36 Ok(e) => e,
37 Err(e) => {
38 errors.push(Arc::new(e));
39 continue;
40 }
41 };
42 let ty = entry.file_type();
43
44 if dir.is_none() {
45 if !ty.is_dir() {
47 tracing::error!("L10N path not a directory");
48 return Err(LangResourceStatus::NotAvailable);
49 }
50 dir = Some(entry.path().to_owned());
51 continue;
52 }
53
54 const EXT: unicase::Ascii<&'static str> = unicase::Ascii::new("ftl");
55
56 let is_ftl = ty.is_file()
57 && entry
58 .file_name()
59 .to_str()
60 .and_then(|n| n.rsplit_once('.'))
61 .map(|(_, ext)| ext.is_ascii() && unicase::Ascii::new(ext) == EXT)
62 .unwrap_or(false);
63
64 if !is_ftl {
65 continue;
66 }
67
68 let mut utf8_path = [""; 5];
69 for (i, part) in entry.path().iter().rev().take(entry.depth()).enumerate() {
70 match part.to_str() {
71 Some(p) => utf8_path[entry.depth() - i - 1] = p,
72 None => continue,
73 }
74 }
75
76 let (lang, file) = match entry.depth() {
77 2 => {
79 let lang = utf8_path[0];
80 let file_str = utf8_path[1].rsplit_once('.').unwrap().0;
81 let file = Txt::from_str(if file_str == "_" { "" } else { file_str });
82 (lang, LangFilePath::current_app(file))
83 }
84 5 => {
86 if utf8_path[1] != "deps" {
87 continue;
88 }
89 let lang = utf8_path[0];
90 let pkg_name = Txt::from_str(utf8_path[2]);
91 let pkg_version: Version = match utf8_path[3].parse() {
92 Ok(v) => v,
93 Err(e) => {
94 errors.push(Arc::new(e));
95 continue;
96 }
97 };
98 let file_str = utf8_path[4].rsplit_once('.').unwrap().0;
99 let file = Txt::from_str(if file_str == "_" { "" } else { file_str });
100 (lang, LangFilePath::new(pkg_name, pkg_version, file))
101 }
102 _ => {
103 continue;
104 }
105 };
106
107 let lang = match Lang::from_str(lang) {
108 Ok(l) => l,
109 Err(e) => {
110 errors.push(Arc::new(e));
111 continue;
112 }
113 };
114
115 set.get_exact_or_insert(lang, Default::default)
116 .insert(file, entry.path().to_owned());
117 }
118
119 if errors.is_empty() {
120 } else {
122 let s = LangResourceStatus::Errors(errors);
123 tracing::error!("'loading available' {s}");
124 return Err(s);
125 }
126 for m in set.values_mut() {
127 m.shrink_to_fit();
128 }
129 set.shrink_to_fit();
130 Ok(Some(Arc::new(set)))
131 }),
132 );
133
134 Self {
135 dir_watch,
136 dir_watch_status: status.read_only(),
137 res: HashMap::new(),
138 }
139 }
140}
141impl L10nSource for L10nDir {
142 fn available_langs(&mut self) -> Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>> {
143 self.dir_watch.clone()
144 }
145 fn available_langs_status(&mut self) -> Var<LangResourceStatus> {
146 self.dir_watch_status.clone()
147 }
148
149 fn lang_resource(&mut self, lang: Lang, file: LangFilePath) -> Var<Option<ArcEq<fluent::FluentResource>>> {
150 match self.res.entry((lang, file)) {
151 std::collections::hash_map::Entry::Occupied(mut e) => {
152 if let Some(out) = e.get().res.upgrade() {
153 out
154 } else {
155 let (lang, file) = e.key();
156 let out = resource_var(&self.dir_watch, e.get().status.clone(), lang.clone(), file.clone());
157 e.get_mut().res = out.downgrade();
158 out
159 }
160 }
161 std::collections::hash_map::Entry::Vacant(e) => {
162 let mut f = L10nFile::new();
163 let (lang, file) = e.key();
164 let out = resource_var(&self.dir_watch, f.status.clone(), lang.clone(), file.clone());
165 f.res = out.downgrade();
166 e.insert(f);
167 out
168 }
169 }
170 }
171
172 fn lang_resource_status(&mut self, lang: Lang, file: LangFilePath) -> Var<LangResourceStatus> {
173 self.res.entry((lang, file)).or_insert_with(L10nFile::new).status.read_only()
174 }
175}
176struct L10nFile {
177 res: WeakVar<Option<ArcEq<fluent::FluentResource>>>,
178 status: Var<LangResourceStatus>,
179}
180impl L10nFile {
181 fn new() -> Self {
182 Self {
183 res: weak_var(),
184 status: var(LangResourceStatus::Loading),
185 }
186 }
187}
188
189fn resource_var(
190 dir_watch: &Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
191 status: Var<LangResourceStatus>,
192 lang: Lang,
193 file: LangFilePath,
194) -> Var<Option<ArcEq<fluent::FluentResource>>> {
195 dir_watch
196 .map(move |w| w.get_file(&lang, &file).cloned())
197 .flat_map(move |p| match p {
198 Some(p) => {
199 status.set(LangResourceStatus::Loading);
200
201 let r = WATCHER.read(
202 p.clone(),
203 None,
204 clmv!(status, |file| {
205 status.set(LangResourceStatus::Loading);
206
207 match file.and_then(|mut f| f.string()) {
208 Ok(flt) => match fluent::FluentResource::try_new(flt) {
209 Ok(flt) => {
210 return Some(Some(ArcEq::new(flt)));
213 }
214 Err(e) => {
215 let e = FluentParserErrors(e.1);
216 tracing::error!("error parsing fluent resource, {e}");
217 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
218 }
219 },
220 Err(e) => {
221 if matches!(e.kind(), io::ErrorKind::NotFound) {
222 status.set(LangResourceStatus::NotAvailable);
223 } else {
224 tracing::error!("error loading fluent resource, {e}");
225 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
226 }
227 }
228 }
229 Some(None)
231 }),
232 );
233 r.bind_filter_map(&status, |v| v.as_ref().map(|_| LangResourceStatus::Loaded))
235 .perm();
236 r
237 }
238 None => const_var(None),
239 })
240}