zng_ext_l10n/sources/
dir.rs
1use 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, ArcVar, BoxedVar, BoxedWeakVar, LocalVar, Var as _, WeakVar as _, types::WeakArcVar, var};
8
9use crate::{FluentParserErrors, L10nSource, Lang, LangFilePath, LangMap, LangResourceStatus};
10
11pub struct L10nDir {
16 dir_watch: BoxedVar<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
17 dir_watch_status: BoxedVar<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, mut file) = match entry.depth() {
77 2 => {
79 let lang = utf8_path[0];
80 let file = Txt::from_str(utf8_path[1].rsplit_once('.').unwrap().0);
81 (lang, LangFilePath::current_app(file))
82 }
83 5 => {
85 if utf8_path[1] != "deps" {
86 continue;
87 }
88 let lang = utf8_path[0];
89 let pkg_name = Txt::from_str(utf8_path[2]);
90 let pkg_version: Version = match utf8_path[3].parse() {
91 Ok(v) => v,
92 Err(e) => {
93 errors.push(Arc::new(e));
94 continue;
95 }
96 };
97 let file = Txt::from_str(utf8_path[4]);
98
99 (lang, LangFilePath::new(pkg_name, pkg_version, file))
100 }
101 _ => {
102 continue;
103 }
104 };
105
106 let lang = match Lang::from_str(lang) {
107 Ok(l) => l,
108 Err(e) => {
109 errors.push(Arc::new(e));
110 continue;
111 }
112 };
113
114 if file.file == "_" {
115 file.file = "".into();
116 }
117
118 set.get_exact_or_insert(lang, Default::default)
119 .insert(file, entry.path().to_owned());
120 }
121
122 if errors.is_empty() {
123 } else {
125 let s = LangResourceStatus::Errors(errors);
126 tracing::error!("'loading available' {s}");
127 return Err(s);
128 }
129
130 Ok(Some(Arc::new(set)))
131 }),
132 );
133
134 Self {
135 dir_watch: dir_watch.boxed(),
136 dir_watch_status: status.read_only().boxed(),
137 res: HashMap::new(),
138 }
139 }
140}
141impl L10nSource for L10nDir {
142 fn available_langs(&mut self) -> BoxedVar<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>> {
143 self.dir_watch.clone()
144 }
145 fn available_langs_status(&mut self) -> BoxedVar<LangResourceStatus> {
146 self.dir_watch_status.clone()
147 }
148
149 fn lang_resource(&mut self, lang: Lang, file: LangFilePath) -> BoxedVar<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) -> BoxedVar<LangResourceStatus> {
173 self.res
174 .entry((lang, file))
175 .or_insert_with(L10nFile::new)
176 .status
177 .read_only()
178 .boxed()
179 }
180}
181struct L10nFile {
182 res: BoxedWeakVar<Option<ArcEq<fluent::FluentResource>>>,
183 status: ArcVar<LangResourceStatus>,
184}
185impl L10nFile {
186 fn new() -> Self {
187 Self {
188 res: WeakArcVar::default().boxed(),
189 status: var(LangResourceStatus::Loading),
190 }
191 }
192}
193
194fn resource_var(
195 dir_watch: &BoxedVar<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
196 status: ArcVar<LangResourceStatus>,
197 lang: Lang,
198 file: LangFilePath,
199) -> BoxedVar<Option<ArcEq<fluent::FluentResource>>> {
200 dir_watch
201 .map(move |w| w.get_file(&lang, &file).cloned())
202 .flat_map(move |p| match p {
203 Some(p) => {
204 status.set(LangResourceStatus::Loading);
205
206 let r = WATCHER.read(
207 p.clone(),
208 None,
209 clmv!(status, |file| {
210 status.set(LangResourceStatus::Loading);
211
212 match file.and_then(|mut f| f.string()) {
213 Ok(flt) => match fluent::FluentResource::try_new(flt) {
214 Ok(flt) => {
215 return Some(Some(ArcEq::new(flt)));
218 }
219 Err(e) => {
220 let e = FluentParserErrors(e.1);
221 tracing::error!("error parsing fluent resource, {e}");
222 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
223 }
224 },
225 Err(e) => {
226 if matches!(e.kind(), io::ErrorKind::NotFound) {
227 status.set(LangResourceStatus::NotAvailable);
228 } else {
229 tracing::error!("error loading fluent resource, {e}");
230 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
231 }
232 }
233 }
234 Some(None)
236 }),
237 );
238 r.bind_map(&status, |_| LangResourceStatus::Loaded).perm();
239 r.boxed()
240 }
241 None => LocalVar(None).boxed(),
242 })
243}