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