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
101 (lang, LangFilePath::new(pkg_name, pkg_version, file))
102 }
103 _ => {
104 continue;
105 }
106 };
107
108 let lang = match Lang::from_str(lang) {
109 Ok(l) => l,
110 Err(e) => {
111 errors.push(Arc::new(e));
112 continue;
113 }
114 };
115
116 set.get_exact_or_insert(lang, Default::default)
117 .insert(file, entry.path().to_owned());
118 }
119
120 if errors.is_empty() {
121 } else {
123 let s = LangResourceStatus::Errors(errors);
124 tracing::error!("'loading available' {s}");
125 return Err(s);
126 }
127
128 Ok(Some(Arc::new(set)))
129 }),
130 );
131
132 Self {
133 dir_watch,
134 dir_watch_status: status.read_only(),
135 res: HashMap::new(),
136 }
137 }
138}
139impl L10nSource for L10nDir {
140 fn available_langs(&mut self) -> Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>> {
141 self.dir_watch.clone()
142 }
143 fn available_langs_status(&mut self) -> Var<LangResourceStatus> {
144 self.dir_watch_status.clone()
145 }
146
147 fn lang_resource(&mut self, lang: Lang, file: LangFilePath) -> Var<Option<ArcEq<fluent::FluentResource>>> {
148 match self.res.entry((lang, file)) {
149 std::collections::hash_map::Entry::Occupied(mut e) => {
150 if let Some(out) = e.get().res.upgrade() {
151 out
152 } else {
153 let (lang, file) = e.key();
154 let out = resource_var(&self.dir_watch, e.get().status.clone(), lang.clone(), file.clone());
155 e.get_mut().res = out.downgrade();
156 out
157 }
158 }
159 std::collections::hash_map::Entry::Vacant(e) => {
160 let mut f = L10nFile::new();
161 let (lang, file) = e.key();
162 let out = resource_var(&self.dir_watch, f.status.clone(), lang.clone(), file.clone());
163 f.res = out.downgrade();
164 e.insert(f);
165 out
166 }
167 }
168 }
169
170 fn lang_resource_status(&mut self, lang: Lang, file: LangFilePath) -> Var<LangResourceStatus> {
171 self.res.entry((lang, file)).or_insert_with(L10nFile::new).status.read_only()
172 }
173}
174struct L10nFile {
175 res: WeakVar<Option<ArcEq<fluent::FluentResource>>>,
176 status: Var<LangResourceStatus>,
177}
178impl L10nFile {
179 fn new() -> Self {
180 Self {
181 res: weak_var(),
182 status: var(LangResourceStatus::Loading),
183 }
184 }
185}
186
187fn resource_var(
188 dir_watch: &Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
189 status: Var<LangResourceStatus>,
190 lang: Lang,
191 file: LangFilePath,
192) -> Var<Option<ArcEq<fluent::FluentResource>>> {
193 dir_watch
194 .map(move |w| w.get_file(&lang, &file).cloned())
195 .flat_map(move |p| match p {
196 Some(p) => {
197 status.set(LangResourceStatus::Loading);
198
199 let r = WATCHER.read(
200 p.clone(),
201 None,
202 clmv!(status, |file| {
203 status.set(LangResourceStatus::Loading);
204
205 match file.and_then(|mut f| f.string()) {
206 Ok(flt) => match fluent::FluentResource::try_new(flt) {
207 Ok(flt) => {
208 return Some(Some(ArcEq::new(flt)));
211 }
212 Err(e) => {
213 let e = FluentParserErrors(e.1);
214 tracing::error!("error parsing fluent resource, {e}");
215 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
216 }
217 },
218 Err(e) => {
219 if matches!(e.kind(), io::ErrorKind::NotFound) {
220 status.set(LangResourceStatus::NotAvailable);
221 } else {
222 tracing::error!("error loading fluent resource, {e}");
223 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
224 }
225 }
226 }
227 Some(None)
229 }),
230 );
231 r.bind_filter_map(&status, |v| v.as_ref().map(|_| LangResourceStatus::Loaded))
233 .perm();
234 r
235 }
236 None => const_var(None),
237 })
238}