1mod fluent;
6mod gettext;
7
8pub use fluent::FluentConfig;
9pub use gettext::GettextConfig;
10
11use std::fs::read_to_string;
12use std::io;
13use std::{
14 fmt::Display,
15 path::{Path, PathBuf},
16};
17
18use log::{debug, error};
19use serde_derive::Deserialize;
20use thiserror::Error;
21use unic_langid::LanguageIdentifier;
22
23#[derive(Debug, Error)]
25pub enum WhyNotCrate {
26 #[error("there is no Cargo.toml present")]
27 NoCargoToml,
28 #[error("it is a workspace")]
29 Workspace,
30}
31
32#[derive(Debug, Error)]
34pub enum I18nConfigError {
35 #[error("The specified path is not a crate because {1}.")]
36 NotACrate(PathBuf, WhyNotCrate),
37 #[error("Cannot read file {0:?} in the current working directory {1:?} because {2}.")]
38 CannotReadFile(PathBuf, io::Result<PathBuf>, #[source] io::Error),
39 #[error("Cannot parse Cargo configuration file {0:?} because {1}.")]
40 CannotParseCargoToml(PathBuf, String),
41 #[error("Cannot deserialize toml file {0:?} because {1}.")]
42 CannotDeserializeToml(PathBuf, basic_toml::Error),
43 #[error("Cannot parse i18n configuration file {0:?} because {1}.")]
44 CannotPaseI18nToml(PathBuf, String),
45 #[error("There is no i18n configuration file present for the crate {0}.")]
46 NoI18nConfig(String),
47 #[error("The \"{0}\" is required to be present in the i18n configuration file \"{1}\"")]
48 OptionMissingInI18nConfig(String, PathBuf),
49 #[error("There is no parent crate for {0}. Required because {1}.")]
50 NoParentCrate(String, String),
51 #[error(
52 "There is no i18n config file present for the parent crate of {0}. Required because {1}."
53 )]
54 NoParentI18nConfig(String, String),
55 #[error("Cannot read `CARGO_MANIFEST_DIR` environment variable.")]
56 CannotReadCargoManifestDir,
57}
58
59#[derive(Deserialize)]
60struct RawCrate {
61 #[serde(alias = "workspace")]
62 package: RawPackage,
63}
64
65#[derive(Deserialize)]
66struct RawPackage {
67 name: String,
68 version: String,
69}
70
71#[derive(Debug, Clone)]
73pub struct Crate<'a> {
74 pub name: String,
76 pub version: String,
78 pub path: PathBuf,
80 pub parent: Option<&'a Crate<'a>>,
83 pub config_file_path: PathBuf,
85 pub i18n_config: Option<I18nConfig>,
87}
88
89impl<'a> Crate<'a> {
90 pub fn from<P1: Into<PathBuf>, P2: Into<PathBuf>>(
93 path: P1,
94 parent: Option<&'a Crate>,
95 config_file_path: P2,
96 ) -> Result<Crate<'a>, I18nConfigError> {
97 let path_into = path.into();
98
99 let config_file_path_into = config_file_path.into();
100
101 let cargo_path = path_into.join("Cargo.toml");
102
103 if !cargo_path.exists() {
104 return Err(I18nConfigError::NotACrate(
105 path_into,
106 WhyNotCrate::NoCargoToml,
107 ));
108 }
109
110 let toml_str = read_to_string(cargo_path.clone()).map_err(|err| {
111 I18nConfigError::CannotReadFile(cargo_path.clone(), std::env::current_dir(), err)
112 })?;
113
114 let cargo_toml: RawCrate = basic_toml::from_str(&toml_str)
115 .map_err(|err| I18nConfigError::CannotDeserializeToml(cargo_path.clone(), err))?;
116
117 let full_config_file_path = path_into.join(&config_file_path_into);
118 let i18n_config = if full_config_file_path.exists() {
119 Some(I18nConfig::from_file(&full_config_file_path)?)
120 } else {
121 None
122 };
123
124 Ok(Crate {
125 name: cargo_toml.package.name,
126 version: cargo_toml.package.version,
127 path: path_into,
128 parent,
129 config_file_path: config_file_path_into,
130 i18n_config,
131 })
132 }
133
134 pub fn module_name(&self) -> String {
137 self.name.replace('-', "_")
138 }
139
140 pub fn parent_active_config(
144 &self,
145 ) -> Result<Option<(&'_ Crate, &'_ I18nConfig)>, I18nConfigError> {
146 match self.parent {
147 Some(parent) => parent.active_config(),
148 None => Ok(None),
149 }
150 }
151
152 pub fn active_config(&self) -> Result<Option<(&'_ Crate, &'_ I18nConfig)>, I18nConfigError> {
156 debug!("Resolving active config for {0}", self);
157 match &self.i18n_config {
158 Some(config) => {
159 if let Some(gettext_config) = &config.gettext {
160 if gettext_config.extract_to_parent {
161 debug!("Resolving active config for {0}, extract_to_parent is true, so attempting to obtain parent config.", self);
162
163 if self.parent.is_none() {
164 return Err(I18nConfigError::NoParentCrate(
165 self.to_string(),
166 "the gettext extract_to_parent option is active".to_string(),
167 ));
168 }
169
170 return Ok(Some(self.parent_active_config()?.ok_or_else(|| {
171 I18nConfigError::NoParentI18nConfig(
172 self.to_string(),
173 "the gettext extract_to_parent option is active".to_string(),
174 )
175 })?));
176 }
177 }
178
179 Ok(Some((self, config)))
180 }
181 None => {
182 debug!(
183 "{0} has no i18n config, attempting to obtain parent config instead.",
184 self
185 );
186 self.parent_active_config()
187 }
188 }
189 }
190
191 pub fn config_or_err(&self) -> Result<&I18nConfig, I18nConfigError> {
194 match &self.i18n_config {
195 Some(config) => Ok(config),
196 None => Err(I18nConfigError::NoI18nConfig(self.to_string())),
197 }
198 }
199
200 pub fn gettext_config_or_err(&self) -> Result<&GettextConfig, I18nConfigError> {
203 match &self.config_or_err()?.gettext {
204 Some(gettext_config) => Ok(gettext_config),
205 None => Err(I18nConfigError::OptionMissingInI18nConfig(
206 "gettext section".to_string(),
207 self.config_file_path.clone(),
208 )),
209 }
210 }
211
212 pub fn collated_subcrate(&self) -> bool {
220 let parent_extract_to_subcrate = self
221 .parent
222 .map(|parent_crate| {
223 parent_crate
224 .gettext_config_or_err()
225 .map(|parent_gettext_config| parent_gettext_config.collate_extracted_subcrates)
226 .unwrap_or(false)
227 })
228 .unwrap_or(false);
229
230 let extract_to_parent = self
231 .gettext_config_or_err()
232 .map(|gettext_config| gettext_config.extract_to_parent)
233 .unwrap_or(false);
234
235 parent_extract_to_subcrate && extract_to_parent
236 }
237
238 pub fn find_parent(&self) -> Option<Crate<'a>> {
241 let parent_crt = match self
242 .path
243 .canonicalize()
244 .map(|op| op.parent().map(|p| p.to_path_buf()))
245 .ok()
246 .unwrap_or(None)
247 {
248 Some(parent_path) => match Crate::from(parent_path, None, "i18n.toml") {
249 Ok(parent_crate) => {
250 debug!("Found parent ({0}) of {1}.", parent_crate, self);
251 Some(parent_crate)
252 }
253 Err(err) => {
254 match err {
255 I18nConfigError::NotACrate(path, WhyNotCrate::Workspace) => {
256 debug!("The parent of {0} at path {1:?} is a workspace", self, path);
257 }
258 I18nConfigError::NotACrate(path, WhyNotCrate::NoCargoToml) => {
259 debug!("The parent of {0} at path {1:?} is not a valid crate with a Cargo.toml", self, path);
260 }
261 _ => {
262 error!(
263 "Error occurred while attempting to resolve parent of {0}: {1}",
264 self, err
265 );
266 }
267 }
268
269 None
270 }
271 },
272 None => None,
273 };
274
275 match parent_crt {
276 Some(crt) => match &crt.i18n_config {
277 Some(config) => {
278 let this_is_subcrate = config
279 .subcrates
280 .iter()
281 .any(|subcrate_path| {
282 let subcrate_path_canon = match crt.path.join(subcrate_path).canonicalize() {
283 Ok(canon) => canon,
284 Err(err) => {
285 error!("Error: unable to canonicalize the subcrate path: {0:?} because {1}", subcrate_path, err);
286 return false;
287 }
288 };
289
290 let self_path_canon = match self.path.canonicalize() {
291 Ok(canon) => canon,
292 Err(err) => {
293 error!("Error: unable to canonicalize the crate path: {0:?} because {1}", self.path, err);
294 return false;
295 }
296 };
297
298 subcrate_path_canon == self_path_canon
299 });
300
301 if this_is_subcrate {
302 Some(crt)
303 } else {
304 debug!("Parent {0} does not have {1} correctly listed as one of its subcrates (currently: {2:?}) in its i18n config.", crt, self, config.subcrates);
305 None
306 }
307 }
308 None => {
309 debug!("Parent {0} of {1} does not have an i18n config", crt, self);
310 None
311 }
312 },
313 None => {
314 debug!("Could not find a valid parent of {0}.", self);
315 None
316 }
317 }
318 }
319}
320
321impl<'a> Display for Crate<'a> {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(
324 f,
325 "Crate \"{0}\" at \"{1}\"",
326 self.name,
327 self.path.to_string_lossy()
328 )
329 }
330}
331
332#[derive(Deserialize, Debug, Clone)]
335pub struct I18nConfig {
336 pub fallback_language: LanguageIdentifier,
341 #[serde(default)]
344 pub subcrates: Vec<PathBuf>,
345 pub gettext: Option<GettextConfig>,
348 pub fluent: Option<FluentConfig>,
351}
352
353impl I18nConfig {
354 pub fn from_file<P: AsRef<Path>>(toml_path: P) -> Result<I18nConfig, I18nConfigError> {
356 let toml_path_final: &Path = toml_path.as_ref();
357 let toml_str = read_to_string(toml_path_final).map_err(|err| {
358 I18nConfigError::CannotReadFile(
359 toml_path_final.to_path_buf(),
360 std::env::current_dir(),
361 err,
362 )
363 })?;
364 let config: I18nConfig = basic_toml::from_str(toml_str.as_ref()).map_err(|err| {
365 I18nConfigError::CannotDeserializeToml(toml_path_final.to_path_buf(), err)
366 })?;
367
368 Ok(config)
369 }
370}
371
372pub struct CratePaths {
374 pub crate_dir: PathBuf,
377 pub i18n_config_file: PathBuf,
379}
380
381pub fn locate_crate_paths() -> Result<CratePaths, I18nConfigError> {
385 let crate_dir = Path::new(
386 &std::env::var_os("CARGO_MANIFEST_DIR")
387 .ok_or(I18nConfigError::CannotReadCargoManifestDir)?,
388 )
389 .to_path_buf();
390 let i18n_config_file = crate_dir.join("i18n.toml");
391
392 Ok(CratePaths {
393 crate_dir,
394 i18n_config_file,
395 })
396}