i18n-embed-fl-0.7.0/.cargo_vcs_info.json0000644000000001530000000000100132670ustar { "git": { "sha1": "bbe62a0e2b6549a80ee716ce057f38d9fba76cdd" }, "path_in_vcs": "i18n-embed-fl" }i18n-embed-fl-0.7.0/CHANGELOG.md000064400000000000000000000060711046102023000136750ustar 00000000000000# Changelog for `i18n-embed-fl` ## v0.7.0 ### Internal + Bump dependencies and use workspace dependencies. ## v0.6.7 + Update to syn version `2.0`. ## v0.6.6 + Fix for [#104](https://github.com/kellpossible/cargo-i18n/issues/104), include files necessary for running tests in crate. ## v0.6.5 ### New Features + Support fluent attributes [#98](https://github.com/kellpossible/cargo-i18n/pull/98) thanks to [@Almost-Senseless-Coder](https://github.com/Almost-Senseless-Coder)! + Tweaked the `fl!()` macro definition such that it optionally accepts an attribute ID in addition to a message ID and arguments. + Implemented compile-time verification of attributes. ### Internal + Bump `i18n-embed` dependency to version `0.13.5`. + Bump `env_logger` dev dependency to version `0.10`. + Fix clippy warnings. ## v0.6.4 + Update `dashmap` to version `5.1`. + Update `rust-embed` to `6.3` to address [RUSTSEC-2021-0126](https://rustsec.org/advisories/RUSTSEC-2021-0126.html). ## v0.6.3 + Revert `dashmap` back to `4.0` due to [security warning](https://rustsec.org/advisories/RUSTSEC-2022-0002.html) ## v0.6.2 + Update `dashmap` to version `5.1`. ## v0.6.1 + Fix for #76, add missing `syn` dependency with `full` feature flag specified. ## v0.6.0 ### Documentation + Don't reference specific `i18n-embed` version number. ### Breaking Changes + Update `i18n-embed` to version `0.13`. + Update `rust-embed` to version `6`. + Update `fluent` to version `0.16`. ## v0.5.0 ### Breaking Changes + Updated `fluent` to version `0.15`. ## v0.4.0 ### Breaking Changes + Update `i18n-embed` to version `0.11`. ### Internal Changes + Refactoring during the fix for [#60](https://github.com/kellpossible/cargo-i18n/issues/60). ## v0.3.1 ### Internal Changes + Safer use of DashMap's new `4.0` API thanks to [#56](https://github.com/kellpossible/cargo-i18n/pull/56). ## v0.3.0 + Update `fluent` dependency to version `0.14`. + Update to `dashmap` version `4.0`, and fix breaking change. ## v0.2.0 + Bumped version to reflect potential breaking changes present in the new version of `fluent`, `0.13` which is exposed in this crate's public API. And yanked previous version of `i18n-embed-fl`: `0.1.6`. ## v0.1.6 ### Internal Changes + Update to `fluent` version `0.13`. + Fixes to address breaking changes in `fluent-syntax` version `0.10`. ## v0.1.5 ### New Features + Updated readme with example convenience wrapper macro. + Added suggestions for message ids (ranked by levenshtein distance) to the error message when the current one fails to match. ## v0.1.4 + Enable the args hashmap option `fl!(loader, "message_id", args())` to be parsed as an expression, instead of just an ident. ## v0.1.3 + Fix bug where message check wasn't occurring with no arguments or with hashmap arguments. ## v0.1.2 + Change the `loader` argument to be an expression, instead of an ident, so it allows more use cases. ## v0.1.1 + Remove `proc_macro_diagnostic` feature causing problems compiling on stable, and use `proc-macro-error` crate instead. ## v0.1.0 + Initial version, introduces the `fl!()` macro. i18n-embed-fl-0.7.0/Cargo.toml0000644000000033700000000000100112710ustar # 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-embed-fl" version = "0.7.0" authors = ["Luke Frisken "] description = "Macro to perform compile time checks when using the i18n-embed crate and the fluent localization system" readme = "README.md" categories = [ "localization", "internationalization", "development-tools", ] license = "MIT" [lib] proc-macro = true [dependencies.dashmap] version = "^5.1" [dependencies.find-crate] version = "0.6" [dependencies.fluent] version = "0.16" [dependencies.fluent-syntax] version = "0.11" [dependencies.i18n-config] version = "0.4.5" [dependencies.i18n-embed] version = "0.14.0" features = [ "fluent-system", "filesystem-assets", ] [dependencies.lazy_static] version = "1.4.0" [dependencies.proc-macro-error] version = "1.0" [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.strsim] version = "0.10" [dependencies.syn] version = "2.0" features = [ "derive", "proc-macro", "parsing", "printing", "extra-traits", "full", ] [dependencies.unic-langid] version = "0.9" [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.10" [dev-dependencies.pretty_assertions] version = "1.4" [dev-dependencies.rust-embed] version = "8.0" i18n-embed-fl-0.7.0/Cargo.toml.orig000064400000000000000000000021021046102023000147420ustar 00000000000000[package] name = "i18n-embed-fl" description = "Macro to perform compile time checks when using the i18n-embed crate and the fluent localization system" categories = ["localization", "internationalization", "development-tools"] version = "0.7.0" authors = ["Luke Frisken "] edition = "2018" license = "MIT" [lib] proc-macro = true [dependencies] dashmap = "^5.1" find-crate = { workspace = true } fluent = { workspace = true } fluent-syntax = { workspace = true } i18n-config = { workspace = true } i18n-embed = { workspace = true, features = ["fluent-system", "filesystem-assets"]} lazy_static = { workspace = true } proc-macro2 = { workspace = true } proc-macro-error = "1.0" quote = { workspace = true } strsim = "0.10" unic-langid = { workspace = true } [dependencies.syn] workspace = true default-features = false features = ["derive", "proc-macro", "parsing", "printing", "extra-traits", "full"] [dev-dependencies] doc-comment = { workspace = true } env_logger = { workspace = true } pretty_assertions = { workspace = true } rust-embed = { workspace = true } i18n-embed-fl-0.7.0/LICENSE.txt000064400000000000000000000020601046102023000137010ustar 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-embed-fl-0.7.0/README.md000064400000000000000000000063151046102023000133440ustar 00000000000000# i18n-embed-fl [![crates.io badge](https://img.shields.io/crates/v/i18n-embed-fl.svg)](https://crates.io/crates/i18n-embed-fl) [![docs.rs badge](https://docs.rs/i18n-embed-fl/badge.svg)](https://docs.rs/i18n-embed-fl/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed-fl/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 crate provides a macro to perform compile time checks when using the [i18n-embed](https://crates.io/crates/i18n-embed) crate and the [fluent](https://www.projectfluent.org/) localization system. See [docs](https://docs.rs/i18n-embed-fl/), and [i18n-embed](https://crates.io/crates/i18n-embed) for more information. **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed-fl/CHANGELOG.md)** ## Example Set up a minimal `i18n.toml` in your crate root to use with `cargo-i18n` (see [cargo i18n](../README.md#configuration) for more information on the configuration file format): ```toml # (Required) The language 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. fallback_language = "en-GB" # Use the fluent localization system. [fluent] # (Required) The path to the assets directory. # The paths inside the assets directory should be structured like so: # `assets_dir/{language}/{domain}.ftl` assets_dir = "i18n" ``` Create a fluent localization file for the `en-GB` language in `i18n/en-GB/{domain}.ftl`, where `domain` is the rust path of your crate (`_` instead of `-`): ```fluent hello-arg = Hello {$name}! ``` Simple set up of the `FluentLanguageLoader`, and obtaining a message formatted with an argument: ```rust use i18n_embed::{ fluent::{fluent_language_loader, FluentLanguageLoader}, LanguageLoader, }; use i18n_embed_fl::fl; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); assert_eq!( "Hello \u{2068}Bob 23\u{2069}!", // Compile time check for message id, and the `name` argument, // to ensure it matches what is specified in the `fallback_language`'s // fluent resource file. fl!(loader, "hello-arg", name = format!("Bob {}", 23)) ) ``` ## Convenience Macro You will notice that this macro requires `loader` to be specified in every call. For you project you may have access to a statically defined loader, and you can create a convenience macro wrapper so this doesn't need to be imported and specified every time. ```rust macro_rules! fl { ($message_id:literal) => {{ i18n_embed_fl::fl!($crate::YOUR_STATIC_LOADER, $message_id) }}; ($message_id:literal, $($args:expr),*) => {{ i18n_embed_fl::fl!($crate::YOUR_STATIC_LOADER, $message_id, $($args), *) }}; } ``` This can now be invoked like so: `fl!("message-id")`, `fl!("message-id", args)` and `fl!("message-id", arg = "value")`. i18n-embed-fl-0.7.0/i18n/en-US/i18n_embed_fl.ftl000064400000000000000000000002771046102023000166770ustar 00000000000000hello-world = Hello World! hello-arg = Hello {$name}! .attr = Hello {$name}'s attribute! hello-arg-2 = Hello {$name1} and {$name2}! hello-attr = Uninspiring. .text = Hello, attribute!i18n-embed-fl-0.7.0/i18n.toml000064400000000000000000000007131046102023000135350ustar 00000000000000# (Required) The language 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. fallback_language = "en-US" # Use the fluent localization system. [fluent] # (Required) The path to the assets directory. # The paths inside the assets directory should be structured like so: # `assets_dir/{language}/{domain}.ftl` assets_dir = "i18n"i18n-embed-fl-0.7.0/src/lib.rs000064400000000000000000000677511046102023000140030ustar 00000000000000use fluent::{FluentAttribute, FluentMessage}; use fluent_syntax::ast::{CallArguments, Expression, InlineExpression, Pattern, PatternElement}; use i18n_embed::{fluent::FluentLanguageLoader, FileSystemAssets, LanguageLoader}; use proc_macro::TokenStream; use proc_macro_error::{abort, emit_error, proc_macro_error}; use quote::quote; use std::{ collections::{HashMap, HashSet}, path::Path, }; use syn::{parse::Parse, parse_macro_input, spanned::Spanned}; use unic_langid::LanguageIdentifier; #[cfg(doctest)] #[macro_use] extern crate doc_comment; #[cfg(doctest)] doctest!("../README.md"); #[derive(Debug)] enum FlAttr { /// An attribute ID got provided. Attr(syn::Lit), /// No attribute ID got provided. None, } impl Parse for FlAttr { fn parse(input: syn::parse::ParseStream) -> syn::Result { if !input.is_empty() { let fork = input.fork(); fork.parse::()?; if fork.parse::().is_ok() && (fork.parse::().is_ok() || fork.is_empty()) { input.parse::()?; let literal = input.parse::()?; Ok(Self::Attr(literal)) } else { Ok(Self::None) } } else { Ok(Self::None) } } } #[derive(Debug)] enum FlArgs { /// `fl!(LOADER, "message", "optional-attribute", args)` where `args` is a /// `HashMap<&'a str, FluentValue<'a>>`. HashMap(syn::Expr), /// ```ignore /// fl!(LOADER, "message", "optional-attribute", /// arg1 = "value", /// arg2 = value2, /// arg3 = calc_value()); /// ``` KeyValuePairs { specified_args: HashMap>, }, /// `fl!(LOADER, "message", "optional-attribute")` no arguments after the message id and optional attribute id. None, } impl Parse for FlArgs { fn parse(input: syn::parse::ParseStream) -> syn::Result { if !input.is_empty() { input.parse::()?; let lookahead = input.fork(); if lookahead.parse::().is_err() { let hash_map = input.parse()?; return Ok(FlArgs::HashMap(hash_map)); } let mut args_map: HashMap> = HashMap::new(); while let Ok(expr) = input.parse::() { let argument_name_ident_opt = match &*expr.left { syn::Expr::Path(path) => path.path.get_ident(), _ => None, }; let argument_name_ident = match argument_name_ident_opt { Some(ident) => ident, None => { return Err(syn::Error::new( expr.left.span(), "fl!() unable to parse argument identifier", )) } } .clone(); let argument_name_string = argument_name_ident.to_string(); let argument_name_lit_str = syn::LitStr::new(&argument_name_string, argument_name_ident.span()); let argument_value = expr.right; if let Some(_duplicate) = args_map.insert(argument_name_lit_str, argument_value) { // There's no Clone implementation by default. let argument_name_lit_str = syn::LitStr::new(&argument_name_string, argument_name_ident.span()); return Err(syn::Error::new( argument_name_lit_str.span(), format!( "fl!() macro contains a duplicate argument `{}`", argument_name_lit_str.value() ), )); } // parse the next comma if there is one let _result = input.parse::(); } if args_map.is_empty() { let span = match input.fork().parse::() { Ok(expr) => expr.span(), Err(_) => input.span(), }; Err(syn::Error::new(span, "fl!() unable to parse args input")) } else { Ok(FlArgs::KeyValuePairs { specified_args: args_map, }) } } else { Ok(FlArgs::None) } } } /// Input for the [fl()] macro. struct FlMacroInput { fluent_loader: syn::Expr, message_id: syn::Lit, attr: FlAttr, args: FlArgs, } impl Parse for FlMacroInput { fn parse(input: syn::parse::ParseStream) -> syn::Result { let fluent_loader = input.parse()?; input.parse::()?; let message_id = input.parse()?; let attr = input.parse()?; let args = input.parse()?; Ok(Self { fluent_loader, message_id, attr, args, }) } } struct DomainSpecificData { loader: FluentLanguageLoader, _assets: FileSystemAssets, } lazy_static::lazy_static! { /// Cached data specific to each localization domain, to improve /// performance of subsequent macro invocations. static ref DOMAINS: dashmap::DashMap = dashmap::DashMap::new(); } /// A macro to obtain localized messages and optionally their attributes, and check the `message_id`, `attribute_id` /// and arguments at compile time. /// /// Compile time checks are performed using the `fallback_language` /// specified in the current crate's `i18n.toml` confiration file. /// /// This macro supports three different calling syntaxes which are /// explained in the following sections. /// /// ## No Arguments /// /// ```ignore /// fl!(loader: FluentLanguageLoader, "message_id") /// ``` /// /// This is the simplest form of the `fl!()` macro, just obtaining a /// message with no arguments. The `message_id` should be specified as /// a literal string, and is checked at compile time. /// /// ### Example /// /// ``` /// use i18n_embed::{ /// fluent::{fluent_language_loader, FluentLanguageLoader}, /// LanguageLoader, /// }; /// use i18n_embed_fl::fl; /// use rust_embed::RustEmbed; /// /// #[derive(RustEmbed)] /// #[folder = "i18n/"] /// struct Localizations; /// /// let loader: FluentLanguageLoader = fluent_language_loader!(); /// loader /// .load_languages(&Localizations, &[loader.fallback_language()]) /// .unwrap(); /// /// // Invoke the fl!() macro to obtain the translated message, and /// // check the message id compile time. /// assert_eq!("Hello World!", fl!(loader, "hello-world")); /// ``` /// /// ## Individual Arguments /// /// ```ignore /// fl!( /// loader: FluentLanguageLoader, /// "message_id", /// arg1 = value, /// arg2 = "value", /// arg3 = function(), /// ... /// ) /// ``` /// /// This form of the `fl!()` macro allows individual arguments to be /// specified in the form `key = value` after the `message_id`. `key` /// needs to be a valid literal argument name, and `value` can be any /// expression that resolves to a type that implements /// `Into`. The `key`s will be checked at compile time to /// ensure that they match the arguments specified in original fluent /// message. /// /// ### Example /// /// ``` /// # use i18n_embed::{ /// # fluent::{fluent_language_loader, FluentLanguageLoader}, /// # LanguageLoader, /// # }; /// # use i18n_embed_fl::fl; /// # use rust_embed::RustEmbed; /// # #[derive(RustEmbed)] /// # #[folder = "i18n/"] /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader /// # .load_languages(&Localizations, &[loader.fallback_language()]) /// # .unwrap(); /// let calc_james = || "James".to_string(); /// pretty_assertions::assert_eq!( /// "Hello \u{2068}Bob\u{2069} and \u{2068}James\u{2069}!", /// // Invoke the fl!() macro to obtain the translated message, and /// // check the message id, and arguments at compile time. /// fl!(loader, "hello-arg-2", name1 = "Bob", name2 = calc_james()) /// ); /// ``` /// /// ## Arguments Hashmap /// /// ```ignore /// fl!( /// loader: FluentLanguageLoader, /// "message_id", /// args: HashMap< /// S where S: Into> + Clone, /// T where T: Into> + Clone> /// ) /// ``` /// /// With this form of the `fl!()` macro, arguments can be specified at /// runtime using a [HashMap](std::collections::HashMap), using the /// same signature as in /// [FluentLanguageLoader::get_args()](i18n_embed::fluent::FluentLanguageLoader::get_args()). /// When using this method of specifying argments, they are not /// checked at compile time. /// /// ### Example /// /// ``` /// # use i18n_embed::{ /// # fluent::{fluent_language_loader, FluentLanguageLoader}, /// # LanguageLoader, /// # }; /// # use i18n_embed_fl::fl; /// # use rust_embed::RustEmbed; /// # #[derive(RustEmbed)] /// # #[folder = "i18n/"] /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader /// # .load_languages(&Localizations, &[loader.fallback_language()]) /// # .unwrap(); /// use std::collections::HashMap; /// /// let mut args: HashMap<&str, &str> = HashMap::new(); /// args.insert("name", "Bob"); /// /// assert_eq!("Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", args)); /// ``` /// /// ## Attributes /// /// In all of the above patterns you can optionally include an `attribute_id` /// after the `message_id`, in which case `fl!` will attempt retrieving the specified /// attribute belonging to the specified message, optionally formatted with the provided arguments. /// /// ### Example /// /// ``` /// # use i18n_embed::{ /// # fluent::{fluent_language_loader, FluentLanguageLoader}, /// # LanguageLoader, /// # }; /// # use i18n_embed_fl::fl; /// # use rust_embed::RustEmbed; /// # #[derive(RustEmbed)] /// # #[folder = "i18n/"] /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader /// # .load_languages(&Localizations, &[loader.fallback_language()]) /// # .unwrap(); /// use std::collections::HashMap; /// /// let mut args: HashMap<&str, &str> = HashMap::new(); /// args.insert("name", "Bob"); /// /// assert_eq!("Hello \u{2068}Bob\u{2069}'s attribute!", fl!(loader, "hello-arg", "attr", args)); /// ``` #[proc_macro] #[proc_macro_error] pub fn fl(input: TokenStream) -> TokenStream { let input: FlMacroInput = parse_macro_input!(input as FlMacroInput); let fluent_loader = input.fluent_loader; let message_id = input.message_id; let manifest = find_crate::Manifest::new().expect("Error reading Cargo.toml"); let current_crate_package = manifest.crate_package().expect("Error parsing Cargo.toml"); let domain = current_crate_package.name; let domain_data = if let Some(domain_data) = DOMAINS.get(&domain) { domain_data } else { let crate_paths = i18n_config::locate_crate_paths() .unwrap_or_else(|error| panic!("fl!() is unable to locate crate paths: {}", error)); let config_file_path = &crate_paths.i18n_config_file; let config = i18n_config::I18nConfig::from_file(config_file_path).unwrap_or_else(|err| { abort! { proc_macro2::Span::call_site(), format!( "fl!() had a problem reading i18n config file {config_file_path:?}: {err}" ); help = "Try creating the `i18n.toml` configuration file."; } }); let fluent_config = config.fluent.unwrap_or_else(|| { abort! { proc_macro2::Span::call_site(), format!( "fl!() had a problem parsing i18n config file {config_file_path:?}: \ there is no `[fluent]` subsection." ); help = "Add the `[fluent]` subsection to `i18n.toml`, \ along with its required `assets_dir`."; } }); let assets_dir = Path::new(&crate_paths.crate_dir).join(fluent_config.assets_dir); let assets = FileSystemAssets::new(assets_dir); let fallback_language: LanguageIdentifier = config.fallback_language; let loader = FluentLanguageLoader::new(&domain, fallback_language.clone()); loader .load_languages(&assets, &[&fallback_language]) .unwrap_or_else(|err| match err { i18n_embed::I18nEmbedError::LanguageNotAvailable(file, language_id) => { if fallback_language != language_id { panic!( "fl!() encountered an unexpected problem, \ the language being loaded (\"{0}\") is not the \ `fallback_language` (\"{1}\")", language_id, fallback_language ) } abort! { proc_macro2::Span::call_site(), format!( "fl!() was unable to load the localization \ file for the `fallback_language` \ (\"{fallback_language}\"): {file}" ); help = "Try creating the required fluent localization file."; } } _ => panic!( "fl!() had an unexpected problem while \ loading language \"{0}\": {1}", fallback_language, err ), }); let data = DomainSpecificData { loader, _assets: assets, }; DOMAINS.entry(domain.clone()).or_insert(data).downgrade() }; let message_id_string = match &message_id { syn::Lit::Str(message_id_str) => { let message_id_str = message_id_str.value(); Some(message_id_str) } unexpected_lit => { emit_error! { unexpected_lit, "fl!() `message_id` should be a literal rust string" }; None } }; let attr = input.attr; let attr_str; let attr_lit = match &attr { FlAttr::Attr(literal) => match literal { syn::Lit::Str(string_lit) => { attr_str = Some(string_lit.value()); Some(literal) } unexpected_lit => { attr_str = None; emit_error! { unexpected_lit, "fl!() `message_id` should be a literal rust string" }; None } }, FlAttr::None => { attr_str = None; None } }; // If we have already confirmed that the loader has the message. // `false` if we haven't checked, or we have checked but no // message was found. let mut checked_loader_has_message = false; // Same procedure for attributes let mut checked_message_has_attribute = false; let gen = match input.args { FlArgs::HashMap(args_hash_map) => { if attr_lit.is_none() { quote! { #fluent_loader.get_args(#message_id, #args_hash_map) } } else { quote! { #fluent_loader.get_attr_args(#message_id, #attr_lit, #args_hash_map) } } } FlArgs::None => { if attr_lit.is_none() { quote! { #fluent_loader.get(#message_id) } } else { quote! { #fluent_loader.get_attr(#message_id, #attr_lit) } } } FlArgs::KeyValuePairs { specified_args } => { let mut arg_assignments = proc_macro2::TokenStream::default(); for (key, value) in &specified_args { arg_assignments = quote! { #arg_assignments args.insert(#key, #value.into()); } } if attr_lit.is_none() { if let Some(message_id_str) = &message_id_string { checked_loader_has_message = domain_data .loader .with_fluent_message(message_id_str, |message: FluentMessage<'_>| { check_message_args(message, &specified_args); }) .is_some(); } let gen = quote! { #fluent_loader.get_args_concrete( #message_id, { let mut args = std::collections::HashMap::new(); #arg_assignments args }) }; gen } else { if let Some(message_id_str) = &message_id_string { if let Some(attr_id_str) = &attr_str { let attr_res = domain_data.loader.with_fluent_message( message_id_str, |message: FluentMessage<'_>| match message.get_attribute(attr_id_str) { Some(attr) => { check_attribute_args(attr, &specified_args); true } None => false, }, ); checked_loader_has_message = attr_res.is_some(); checked_message_has_attribute = attr_res.unwrap_or(false); } } let gen = quote! { #fluent_loader.get_attr_args_concrete( #message_id, #attr_lit, { let mut args = std::collections::HashMap::new(); #arg_assignments args }) }; gen } } }; if let Some(message_id_str) = &message_id_string { if !checked_loader_has_message && !domain_data.loader.has(message_id_str) { let suggestions = fuzzy_message_suggestions(&domain_data.loader, message_id_str, 5).join("\n"); let hint = format!( "Perhaps you are looking for one of the following messages?\n\n\ {suggestions}" ); emit_error! { message_id, format!( "fl!() `message_id` validation failed. `message_id` \ of \"{0}\" does not exist in the `fallback_language` (\"{1}\")", message_id_str, domain_data.loader.current_language(), ); help = "Enter the correct `message_id` or create \ the message in the localization file if the \ intended message does not yet exist."; hint = hint; }; } else if let Some(attr_id_str) = &attr_str { if !checked_message_has_attribute && !&domain_data.loader.has_attr(message_id_str, attr_id_str) { let suggestions = &domain_data .loader .with_fluent_message(message_id_str, |message| { fuzzy_attribute_suggestions(&message, attr_id_str, 5).join("\n") }) .unwrap(); let hint = format!( "Perhaps you are looking for one of the following attributes?\n\n\ {suggestions}" ); emit_error! { attr_lit, format!( "fl!() `attribute_id` validation failed. `attribute_id` \ of \"{0}\" does not exist in the `fallback_language` (\"{1}\")", attr_id_str, domain_data.loader.current_language(), ); help = "Enter the correct `attribute_id` or create \ the attribute associated with the message in the localization file if the \ intended attribute does not yet exist."; hint = hint; }; } } } gen.into() } fn fuzzy_message_suggestions( loader: &FluentLanguageLoader, message_id_str: &str, n_suggestions: usize, ) -> Vec { let mut scored_messages: Vec<(String, usize)> = loader.with_message_iter(loader.fallback_language(), |message_iter| { message_iter .map(|message| { ( message.id.name.to_string(), strsim::levenshtein(message_id_str, message.id.name), ) }) .collect() }); scored_messages.sort_by_key(|(_message, score)| *score); scored_messages.truncate(n_suggestions); scored_messages .into_iter() .map(|(message, _score)| message) .collect() } fn fuzzy_attribute_suggestions( message: &FluentMessage<'_>, attribute_id_str: &str, n_suggestions: usize, ) -> Vec { let mut scored_attributes: Vec<(String, usize)> = message .attributes() .map(|attribute| { ( attribute.id().to_string(), strsim::levenshtein(attribute_id_str, attribute.id()), ) }) .collect(); scored_attributes.sort_by_key(|(_attr, score)| *score); scored_attributes.truncate(n_suggestions); scored_attributes .into_iter() .map(|(attribute, _score)| attribute) .collect() } fn check_message_args( message: FluentMessage<'_>, specified_args: &HashMap>, ) { if let Some(pattern) = message.value() { let mut args = Vec::new(); args_from_pattern(pattern, &mut args); let args_set: HashSet<&str> = args.into_iter().collect(); let key_args: Vec = specified_args .keys() .map(|key| { let arg = key.value(); if !args_set.contains(arg.as_str()) { let available_args: String = args_set .iter() .map(|arg| format!("`{arg}`")) .collect::>() .join(", "); emit_error! { key, format!( "fl!() argument `{0}` does not exist in the \ fluent message. Available arguments: {1}.", &arg, available_args ); help = "Enter the correct arguments, or fix the message \ in the fluent localization file so that the arguments \ match this macro invocation."; }; } arg }) .collect(); let key_args_set: HashSet<&str> = key_args.iter().map(|v| v.as_str()).collect(); let unspecified_args: Vec = args_set .iter() .filter_map(|arg| { if !key_args_set.contains(arg) { Some(format!("`{arg}`")) } else { None } }) .collect(); if !unspecified_args.is_empty() { emit_error! { proc_macro2::Span::call_site(), format!( "fl!() the following arguments have not been specified: {}", unspecified_args.join(", ") ); help = "Enter the correct arguments, or fix the message \ in the fluent localization file so that the arguments \ match this macro invocation."; }; } } } fn check_attribute_args( attr: FluentAttribute<'_>, specified_args: &HashMap>, ) { let pattern = attr.value(); let mut args = Vec::new(); args_from_pattern(pattern, &mut args); let args_set: HashSet<&str> = args.into_iter().collect(); let key_args: Vec = specified_args .keys() .map(|key| { let arg = key.value(); if !args_set.contains(arg.as_str()) { let available_args: String = args_set .iter() .map(|arg| format!("`{arg}`")) .collect::>() .join(", "); emit_error! { key, format!( "fl!() argument `{0}` does not exist in the \ fluent attribute. Available arguments: {1}.", &arg, available_args ); help = "Enter the correct arguments, or fix the attribute \ in the fluent localization file so that the arguments \ match this macro invocation."; }; } arg }) .collect(); let key_args_set: HashSet<&str> = key_args.iter().map(|v| v.as_str()).collect(); let unspecified_args: Vec = args_set .iter() .filter_map(|arg| { if !key_args_set.contains(arg) { Some(format!("`{arg}`")) } else { None } }) .collect(); if !unspecified_args.is_empty() { emit_error! { proc_macro2::Span::call_site(), format!( "fl!() the following arguments have not been specified: {}", unspecified_args.join(", ") ); help = "Enter the correct arguments, or fix the attribute \ in the fluent localization file so that the arguments \ match this macro invocation."; }; } } fn args_from_pattern(pattern: &Pattern, args: &mut Vec) { pattern.elements.iter().for_each(|element| { if let PatternElement::Placeable { expression } = element { args_from_expression(expression, args) } }); } fn args_from_expression(expr: &Expression, args: &mut Vec) { match expr { Expression::Inline(inline_expr) => { args_from_inline_expression(inline_expr, args); } Expression::Select { selector, variants } => { args_from_inline_expression(selector, args); variants.iter().for_each(|variant| { args_from_pattern(&variant.value, args); }) } } } fn args_from_inline_expression(inline_expr: &InlineExpression, args: &mut Vec) { match inline_expr { InlineExpression::FunctionReference { id: _, arguments: call_args, } => { args_from_call_arguments(call_args, args); } InlineExpression::TermReference { id: _, attribute: _, arguments: Some(call_args), } => { args_from_call_arguments(call_args, args); } InlineExpression::VariableReference { id } => args.push(id.name), InlineExpression::Placeable { expression } => args_from_expression(expression, args), _ => {} } } fn args_from_call_arguments(call_args: &CallArguments, args: &mut Vec) { call_args.positional.iter().for_each(|expr| { args_from_inline_expression(expr, args); }); call_args.named.iter().for_each(|named_arg| { args_from_inline_expression(&named_arg.value, args); }) } i18n-embed-fl-0.7.0/tests/fl_macro.rs000064400000000000000000000045721046102023000153620ustar 00000000000000use i18n_embed::{ fluent::{fluent_language_loader, FluentLanguageLoader}, LanguageLoader, }; use i18n_embed_fl::fl; use rust_embed::RustEmbed; use std::collections::HashMap; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; #[test] fn with_args_hashmap() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); let mut args: HashMap<&str, &str> = HashMap::new(); args.insert("name", "Bob"); pretty_assertions::assert_eq!("Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", args)); } #[test] fn with_args_hashmap_expr() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); let args_expr = || { let mut args: HashMap<&str, &str> = HashMap::new(); args.insert("name", "Bob"); args }; pretty_assertions::assert_eq!( "Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", args_expr()) ); } #[test] fn with_loader_expr() { let loader = || { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); loader }; pretty_assertions::assert_eq!("Hello World!", fl!(loader(), "hello-world")); } #[test] fn with_one_arg_lit() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); pretty_assertions::assert_eq!( "Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", name = "Bob") ); } #[test] fn with_attr() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); pretty_assertions::assert_eq!("Hello, attribute!", fl!(loader, "hello-attr", "text")); } #[test] fn with_attr_and_args() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_languages(&Localizations, &[loader.fallback_language()]) .unwrap(); pretty_assertions::assert_eq!( "Hello \u{2068}Bob\u{2069}'s attribute!", fl!(loader, "hello-arg", "attr", name = "Bob") ); }