i18n-config-0.4.6/.cargo_vcs_info.json0000644000000001510000000000100130620ustar { "git": { "sha1": "29cb389f19949204e67b1eeff9a5fc27f7ffc2b8" }, "path_in_vcs": "i18n-config" }i18n-config-0.4.6/.gitignore000064400000000000000000000000451046102023000136440ustar 00000000000000/target **/*.rs.bk .vscode Cargo.locki18n-config-0.4.6/CHANGELOG.md000064400000000000000000000041671046102023000134760ustar 00000000000000# `i18n-config` Changelog ## v0.4.6 ### Internal + Bump `toml` to version `0.8`. ## v0.4.5 ### Internal + Bump dependencies and refactor to use workspace dependencies. ## v0.4.4 ### New Features + Add option to override the default domain name for fluent assets. ### Internal + Fix clippy warnings. + Bump `toml` to version `0.7`. ## v0.4.3 + Prevent a panic when the crate root is a workspace. [#93](https://github.com/kellpossible/cargo-i18n/pull/93) thanks to [@ctrlcctrlv](https://github.com/ctrlcctrlv). ## v0.4.2 ### New Features + Add the `use_fuzzy` option for the `gettext` system. [#68](https://github.com/kellpossible/cargo-i18n/pull/68) thanks to [@vkill](https://github.com/vkill). ## v0.4.1 ### New Features + New `locate_crate_paths()` function for use in procedural macros. ## v0.4.0 ### New Features + Introduced new `assets_dir` member of `[fluent]` subsection. ### Breaking Changes + Changed type of `fallback_language` from `String` to `unic_langid::LanguageIdentifier`. ### Internal Changes + Improved error messages. ## v0.3.0 Changes for the support of the `fluent` localization system. ### New Features + New `FluentConfig` (along with associated `[fluent]` subsection in the configuration file format) for using the `fluent` localization system. ### Breaking Changes + Renamed `src_locale` to `fallback_language`. + Moved `target_locales` to within the `[gettext]` subsection, and renamed it to `target_languages`. ### Internal Changes + Now using `parking_lot::RwLock` for the language loaders, instead of the `RwLock` in the standard library. ## v0.2.2 + Add support for `xtr` `add-location` option. ## v0.2.1 + Updated link to this changelog in the crate README. ## v0.2.0 + A bunch of changes to help with solving [issue 13](https://github.com/kellpossible/cargo-i18n/issues/13). + Add some debug logging using the [log crate](https://crates.io/crates/log). + Migrate away from `anyhow` and provide a new `I18nConfigError` type. + Change `I18nConfig#subcrates` type from `Option>` to `Vec` and use `serde` default of empty vector. + Add a `find_parent` method which searches. i18n-config-0.4.6/Cargo.toml0000644000000024450000000000100110700ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "i18n-config" version = "0.4.6" authors = ["Luke Frisken "] description = "This library contains the configuration stucts (along with their parsing functions) for the cargo-i18n tool/system." readme = "README.md" keywords = [ "cargo", "build", "i18n", "gettext", "locale", ] categories = [ "localization", "internationalization", ] license = "MIT" repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-config" [dependencies.log] version = "0.4" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_derive] version = "1.0" [dependencies.thiserror] version = "1.0" [dependencies.toml] version = "0.8" [dependencies.unic-langid] version = "0.9" features = ["serde"] [badges.maintenance] status = "actively-developed" i18n-config-0.4.6/Cargo.toml.orig000064400000000000000000000015451046102023000145510ustar 00000000000000[package] authors = ["Luke Frisken "] categories = ["localization", "internationalization"] description = "This library contains the configuration stucts (along with their parsing functions) for the cargo-i18n tool/system." edition = "2018" keywords = ["cargo", "build", "i18n", "gettext", "locale"] license = "MIT" name = "i18n-config" readme = "README.md" repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-config" version = "0.4.6" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [badges] maintenance = { status = "actively-developed" } [dependencies] log = { workspace = true } toml = "0.8" serde = { workspace = true, features = ["derive"] } serde_derive = { workspace = true } thiserror = { workspace = true } unic-langid = { workspace = true, features = ["serde"] } i18n-config-0.4.6/LICENSE.txt000064400000000000000000000020601046102023000134760ustar 00000000000000Copyright 2020 Luke Frisken Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. © 2020 Luke Friskeni18n-config-0.4.6/README.md000064400000000000000000000014341046102023000131360ustar 00000000000000# i18n-config [![crates.io badge](https://img.shields.io/crates/v/i18n-config.svg)](https://crates.io/crates/i18n-config) [![docs.rs badge](https://docs.rs/i18n-config/badge.svg)](https://docs.rs/i18n-config/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-config/LICENSE.txt) [![github actions badge](https://github.com/kellpossible/cargo-i18n/workflows/Rust/badge.svg)](https://github.com/kellpossible/cargo-i18n/actions?query=workflow%3ARust) This library contains the configuration structs (along with their parsing functions) for the [cargo-i18n](https://crates.io/crates/cargo_i18n) tool/system. **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-config/CHANGELOG.md)** i18n-config-0.4.6/src/fluent.rs000064400000000000000000000012631046102023000143110ustar 00000000000000use serde::Deserialize; use std::path::PathBuf; /// The data structure representing what is stored (and possible to /// store) within the `fluent` subsection of a `i18n.toml` file. #[derive(Deserialize, Debug, Clone)] pub struct FluentConfig { /// (Required) The path to the assets directory. /// /// The paths inside the assets directory should be structured /// like so: `assets_dir/{language}/{domain}.ftl` pub assets_dir: PathBuf, /// (Optional) Domain name to override default value (i.e. package name) /// The paths inside the assets directory should be structured /// like so: `assets_dir/{language}/{domain}.ftl` pub domain: Option, } i18n-config-0.4.6/src/gettext.rs000064400000000000000000000104211046102023000144740ustar 00000000000000use serde::Deserialize; use std::path::PathBuf; /// The data structure representing what is stored (and possible to /// store) within the `gettext` subsection of a `i18n.toml` file. #[derive(Deserialize, Debug, Clone)] pub struct GettextConfig { /// The languages that the software will be translated into. pub target_languages: Vec, /// Path to the output directory, relative to `i18n.toml` of the /// crate being localized. pub output_dir: PathBuf, // If this crate is being localized as a subcrate, store the // localization artifacts with the parent crate's output. // Currently crates which contain subcrates with duplicate names // are not supported. // // By default this is **false**. #[serde(default)] pub extract_to_parent: bool, // If a subcrate has extract_to_parent set to true, // then merge the output pot file of that subcrate into this // crate's pot file. // // By default this is **false**. #[serde(default)] pub collate_extracted_subcrates: bool, /// Set the copyright holder for the generated files. pub copyright_holder: Option, /// The reporting address for msgid bugs. This is the email /// address or URL to which the translators shall report bugs in /// the untranslated strings. pub msgid_bugs_address: Option, /// Whether or not to perform string extraction using the `xtr` command. pub xtr: Option, /// Generate ‘#: filename:line’ lines (default) in the pot files when /// running the `xtr` command. If the type is ‘full’ (the default), /// it generates the lines with both file name and line number. /// If it is ‘file’, the line number part is omitted. If it is ‘never’, /// nothing is generated. [possible values: full, file, never]. #[serde(default)] pub add_location: GettextAddLocation, /// Path to where the pot files will be written to by the `xtr` /// command, and were they will be read from by `msginit` and /// `msgmerge`. pot_dir: Option, /// Path to where the po files will be stored/edited with the /// `msgmerge` and `msginit` commands, and where they will be read /// from with the `msgfmt` command. po_dir: Option, /// Path to where the mo files will be written to by the /// `msgfmt` command. mo_dir: Option, /// Enable the `--use-fuzzy` option for the `msgfmt` command. /// /// By default this is **false**. #[serde(default)] pub use_fuzzy: bool, } impl GettextConfig { /// Path to where the pot files will be written to by the `xtr` /// command, and were they will be read from by `msginit` and /// `msgmerge`. /// /// By default this is /// **[output_dir](GettextConfig::output_dir)/pot**. pub fn pot_dir(&self) -> PathBuf { // match self.pot_dir { // Some(pot_dir) => pot_dir, // None => { // panic!("panic"); // }, // } self.pot_dir .clone() .unwrap_or_else(|| self.output_dir.join("pot")) } /// Path to where the po files will be stored/edited with the /// `msgmerge` and `msginit` commands, and where they will /// be read from with the `msgfmt` command. /// /// By default this is **[output_dir](GettextConfig::output_dir)/po**. pub fn po_dir(&self) -> PathBuf { self.po_dir .clone() .unwrap_or_else(|| self.output_dir.join("po")) } /// Path to where the mo files will be written to by the `msgfmt` command. /// /// By default this is /// **[output_dir](GettextConfig::output_dir)/mo**. pub fn mo_dir(&self) -> PathBuf { self.mo_dir .clone() .unwrap_or_else(|| self.output_dir.join("mo")) } } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "lowercase")] pub enum GettextAddLocation { Full, File, Never, } impl GettextAddLocation { pub fn to_str(&self) -> &str { match self { GettextAddLocation::Full => "full", GettextAddLocation::File => "file", GettextAddLocation::Never => "never", } } } impl Default for GettextAddLocation { fn default() -> Self { GettextAddLocation::Full } } i18n-config-0.4.6/src/lib.rs000064400000000000000000000401241046102023000135610ustar 00000000000000//! This library contains the configuration stucts (along with their //! parsing functions) for the //! [cargo-i18n](https://crates.io/crates/cargo_i18n) tool/system. mod fluent; mod gettext; pub use fluent::FluentConfig; pub use gettext::GettextConfig; use std::fs::read_to_string; use std::io; use std::{ fmt::Display, path::{Path, PathBuf}, }; use log::{debug, error}; use serde_derive::Deserialize; use thiserror::Error; use unic_langid::LanguageIdentifier; /// An error type explaining why a crate failed to verify. #[derive(Debug, Error)] pub enum WhyNotCrate { #[error("there is no Cargo.toml present")] NoCargoToml, #[error("it is a workspace")] Workspace, } /// An error type for use with the `i18n-config` crate. #[derive(Debug, Error)] pub enum I18nConfigError { #[error("The specified path is not a crate because {1}.")] NotACrate(PathBuf, WhyNotCrate), #[error("Cannot read file {0:?} in the current working directory {1:?} because {2}.")] CannotReadFile(PathBuf, io::Result, #[source] io::Error), #[error("Cannot parse Cargo configuration file {0:?} because {1}.")] CannotParseCargoToml(PathBuf, String), #[error("Cannot deserialize toml file {0:?} because {1}.")] CannotDeserializeToml(PathBuf, toml::de::Error), #[error("Cannot parse i18n configuration file {0:?} because {1}.")] CannotPaseI18nToml(PathBuf, String), #[error("There is no i18n configuration file present for the crate {0}.")] NoI18nConfig(String), #[error("The \"{0}\" is required to be present in the i18n configuration file \"{1}\"")] OptionMissingInI18nConfig(String, PathBuf), #[error("There is no parent crate for {0}. Required because {1}.")] NoParentCrate(String, String), #[error( "There is no i18n config file present for the parent crate of {0}. Required because {1}." )] NoParentI18nConfig(String, String), #[error("Cannot read `CARGO_MANIFEST_DIR` environment variable.")] CannotReadCargoManifestDir, } /// Represents a rust crate. #[derive(Debug, Clone)] pub struct Crate<'a> { /// The name of the crate. pub name: String, /// The version of the crate. pub version: String, /// The path to the crate. pub path: PathBuf, /// Path to the parent crate which is triggering the localization /// for this crate. pub parent: Option<&'a Crate<'a>>, /// The file path expected to be used for `i18n_config` relative to this crate's root. pub config_file_path: PathBuf, /// The localization config for this crate (if it exists). pub i18n_config: Option, } impl<'a> Crate<'a> { /// Read crate from `Cargo.toml` i18n config using the /// `config_file_path` (if there is one). pub fn from, P2: Into>( path: P1, parent: Option<&'a Crate>, config_file_path: P2, ) -> Result, I18nConfigError> { let path_into = path.into(); let config_file_path_into = config_file_path.into(); let cargo_path = path_into.join("Cargo.toml"); if !cargo_path.exists() { return Err(I18nConfigError::NotACrate( path_into, WhyNotCrate::NoCargoToml, )); } let toml_str = read_to_string(cargo_path.clone()).map_err(|err| { I18nConfigError::CannotReadFile(cargo_path.clone(), std::env::current_dir(), err) })?; let cargo_toml: toml::Value = toml::from_str(toml_str.as_ref()) .map_err(|err| I18nConfigError::CannotDeserializeToml(cargo_path.clone(), err))?; let package = cargo_toml .as_table() .ok_or_else(|| I18nConfigError::CannotParseCargoToml(cargo_path.clone(), "Cargo.toml needs have sections (such as the \"gettext\" section when using gettext.".to_string()))? .get("package") .ok_or_else(|| { match cargo_toml.get("workspace") { Some(_) => I18nConfigError::NotACrate(cargo_path.clone(), WhyNotCrate::Workspace), None => I18nConfigError::CannotParseCargoToml(cargo_path.clone(), "Cargo.toml needs to have a \"package\" section.".to_string()) } })? .as_table() .ok_or_else(|| I18nConfigError::CannotParseCargoToml(cargo_path.clone(), "Cargo.toml's \"package\" section needs to contain values.".to_string() ))?; let name = package .get("name") .ok_or_else(|| { I18nConfigError::CannotParseCargoToml( cargo_path.clone(), "Cargo.toml needs to specify a package name.".to_string(), ) })? .as_str() .ok_or_else(|| { I18nConfigError::CannotParseCargoToml( cargo_path.clone(), "Cargo.toml's package name needs to be a string.".to_string(), ) })?; let version = package .get("version") .ok_or_else(|| { I18nConfigError::CannotParseCargoToml( cargo_path.clone(), "Cargo.toml needs to specify a package version.".to_string(), ) })? .as_str() .ok_or_else(|| { I18nConfigError::CannotParseCargoToml( cargo_path, "Cargo.toml's package version needs to be a string.".to_string(), ) })?; let full_config_file_path = path_into.join(&config_file_path_into); let i18n_config = if full_config_file_path.exists() { Some(I18nConfig::from_file(&full_config_file_path)?) } else { None }; Ok(Crate { name: String::from(name), version: String::from(version), path: path_into, parent, config_file_path: config_file_path_into, i18n_config, }) } /// The name of the module/library used for this crate. Replaces /// `-` characters with `_` in the crate name. pub fn module_name(&self) -> String { self.name.replace('-', "_") } /// If there is a parent, get it's /// [I18nConfig#active_config()](I18nConfig#active_config()), /// otherwise return None. pub fn parent_active_config( &'a self, ) -> Result, I18nConfigError> { match self.parent { Some(parent) => parent.active_config(), None => Ok(None), } } /// Identify the config which should be used for this crate, and /// the crate (either this crate or one of it's parents) /// associated with that config. pub fn active_config(&'a self) -> Result, I18nConfigError> { debug!("Resolving active config for {0}", self); match &self.i18n_config { Some(config) => { if let Some(gettext_config) = &config.gettext { if gettext_config.extract_to_parent { debug!("Resolving active config for {0}, extract_to_parent is true, so attempting to obtain parent config.", self); if self.parent.is_none() { return Err(I18nConfigError::NoParentCrate( self.to_string(), "the gettext extract_to_parent option is active".to_string(), )); } return Ok(Some(self.parent_active_config()?.ok_or_else(|| { I18nConfigError::NoParentI18nConfig( self.to_string(), "the gettext extract_to_parent option is active".to_string(), ) })?)); } } Ok(Some((self, config))) } None => { debug!( "{0} has no i18n config, attempting to obtain parent config instead.", self ); self.parent_active_config() } } } /// Get the [I18nConfig](I18nConfig) in this crate, or return an /// error if there is none present. pub fn config_or_err(&self) -> Result<&I18nConfig, I18nConfigError> { match &self.i18n_config { Some(config) => Ok(config), None => Err(I18nConfigError::NoI18nConfig(self.to_string())), } } /// Get the [GettextConfig](GettextConfig) in this crate, or /// return an error if there is none present. pub fn gettext_config_or_err(&self) -> Result<&GettextConfig, I18nConfigError> { match &self.config_or_err()?.gettext { Some(gettext_config) => Ok(gettext_config), None => Err(I18nConfigError::OptionMissingInI18nConfig( "gettext section".to_string(), self.config_file_path.clone(), )), } } /// If this crate has a parent, check whether the parent wants to /// collate subcrates string extraction, as per the parent's /// [GettextConfig#collate_extracted_subcrates](GettextConfig#collate_extracted_subcrates). /// This also requires that the current crate's [GettextConfig#extract_to_parent](GettextConfig#extract_to_parent) /// is **true**. /// /// Returns **false** if there is no parent or the parent has no gettext config. pub fn collated_subcrate(&self) -> bool { let parent_extract_to_subcrate = self .parent .map(|parent_crate| { parent_crate .gettext_config_or_err() .map(|parent_gettext_config| parent_gettext_config.collate_extracted_subcrates) .unwrap_or(false) }) .unwrap_or(false); let extract_to_parent = self .gettext_config_or_err() .map(|gettext_config| gettext_config.extract_to_parent) .unwrap_or(false); parent_extract_to_subcrate && extract_to_parent } /// Attempt to resolve the parents of this crate which have this /// crate listed as a subcrate in their i18n config. pub fn find_parent(&self) -> Option> { let parent_crt = match self .path .canonicalize() .map(|op| op.parent().map(|p| p.to_path_buf())) .ok() .unwrap_or(None) { Some(parent_path) => match Crate::from(parent_path, None, "i18n.toml") { Ok(parent_crate) => { debug!("Found parent ({0}) of {1}.", parent_crate, self); Some(parent_crate) } Err(err) => { match err { I18nConfigError::NotACrate(path, WhyNotCrate::Workspace) => { debug!("The parent of {0} at path {1:?} is a workspace", self, path); } I18nConfigError::NotACrate(path, WhyNotCrate::NoCargoToml) => { debug!("The parent of {0} at path {1:?} is not a valid crate with a Cargo.toml", self, path); } _ => { error!( "Error occurred while attempting to resolve parent of {0}: {1}", self, err ); } } None } }, None => None, }; match parent_crt { Some(crt) => match &crt.i18n_config { Some(config) => { let this_is_subcrate = config .subcrates .iter() .any(|subcrate_path| { let subcrate_path_canon = match crt.path.join(subcrate_path).canonicalize() { Ok(canon) => canon, Err(err) => { error!("Error: unable to canonicalize the subcrate path: {0:?} because {1}", subcrate_path, err); return false; } }; let self_path_canon = match self.path.canonicalize() { Ok(canon) => canon, Err(err) => { error!("Error: unable to canonicalize the crate path: {0:?} because {1}", self.path, err); return false; } }; subcrate_path_canon == self_path_canon }); if this_is_subcrate { Some(crt) } else { debug!("Parent {0} does not have {1} correctly listed as one of its subcrates (curently: {2:?}) in its i18n config.", crt, self, config.subcrates); None } } None => { debug!("Parent {0} of {1} does not have an i18n config", crt, self); None } }, None => { debug!("Could not find a valid parent of {0}.", self); None } } } } impl<'a> Display for Crate<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Crate \"{0}\" at \"{1}\"", self.name, self.path.to_string_lossy() ) } } /// The data structure representing what is stored (and possible to /// store) within a `i18n.toml` file. #[derive(Deserialize, Debug, Clone)] pub struct I18nConfig { /// The locale identifier of the language used in the source code /// for `gettext` system, and the primary fallback language (for /// which all strings must be present) when using the `fluent` /// system. pub fallback_language: LanguageIdentifier, /// Specify which subcrates to perform localization within. The /// subcrate needs to have its own `i18n.toml`. #[serde(default)] pub subcrates: Vec, /// The subcomponent of this config relating to gettext, only /// present if the gettext localization system will be used. pub gettext: Option, /// The subcomponent of this config relating to gettext, only /// present if the fluent localization system will be used. pub fluent: Option, } impl I18nConfig { /// Load the config from the specified toml file path. pub fn from_file>(toml_path: P) -> Result { let toml_path_final: &Path = toml_path.as_ref(); let toml_str = read_to_string(toml_path_final).map_err(|err| { I18nConfigError::CannotReadFile( toml_path_final.to_path_buf(), std::env::current_dir(), err, ) })?; let config: I18nConfig = toml::from_str(toml_str.as_ref()).map_err(|err| { I18nConfigError::CannotDeserializeToml(toml_path_final.to_path_buf(), err) })?; Ok(config) } } /// Important i18n-config paths related to the current crate. pub struct CratePaths { /// The current crate directory path (where the `Cargo.toml` is /// located). pub crate_dir: PathBuf, /// The current i18n config file path pub i18n_config_file: PathBuf, } /// Locate the current crate's directory and `i18n.toml` config file. /// This is intended to be called by a procedural macro during crate /// compilation. pub fn locate_crate_paths() -> Result { let crate_dir = Path::new( &std::env::var_os("CARGO_MANIFEST_DIR") .ok_or(I18nConfigError::CannotReadCargoManifestDir)?, ) .to_path_buf(); let i18n_config_file = crate_dir.join("i18n.toml"); Ok(CratePaths { crate_dir, i18n_config_file, }) }