From 2cc30748e5319a20cbf520d05edf9a116327546c Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Fri, 23 Jun 2023 17:32:34 +0300 Subject: [PATCH] Add Fluent function to Tera, link to data context (#21) * refactor: Reuse tera instance for all types of runs * deps: Add fluent templating crate as optional dependency * feat: Inject fluent function to tera * test: Test use of fluent functions in template * ci: Add new feature flags to test matrix * Fix ci --------- Co-authored-by: Chevdor --- .github/workflows/quick-check.yml | 10 +- Cargo.lock | 250 +++++++++++++++++++++++++++++- Cargo.toml | 5 + data/basic/fluent.tera | 1 + data/basic/fluent.yaml | 2 + data/locales/en-US/test.ftl | 1 + data/locales/tr-TR/test.ftl | 1 + src/main.rs | 45 +++++- src/opts.rs | 10 ++ tests/test.rs | 16 ++ 10 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 data/basic/fluent.tera create mode 100644 data/basic/fluent.yaml create mode 100644 data/locales/en-US/test.ftl create mode 100644 data/locales/tr-TR/test.ftl diff --git a/.github/workflows/quick-check.yml b/.github/workflows/quick-check.yml index 8004769..ce6c150 100644 --- a/.github/workflows/quick-check.yml +++ b/.github/workflows/quick-check.yml @@ -49,9 +49,15 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --all-features -- -D warnings quick_check-tests: + strategy: + fail-fast: false + matrix: + feature-flags: + - '' + - '--all-features' runs-on: ubuntu-latest steps: - name: Install Rust stable toolchain @@ -67,8 +73,10 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: ${{ matrix.feature-flags }} - name: Cargo check uses: actions-rs/cargo@v1 with: command: check + args: ${{ matrix.feature-flags }} diff --git a/Cargo.lock b/Cargo.lock index f6fdcb5..8d8c507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "assert_cmd" version = "2.0.11" @@ -128,9 +134,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" [[package]] name = "cc" @@ -330,6 +336,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -385,6 +402,83 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" +dependencies = [ + "thiserror", +] + +[[package]] +name = "fluent-templates" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3ef2c2152757885365abce32ddf682746062f1b6b3c0824a29fbed6ee4d080" +dependencies = [ + "arc-swap", + "fluent", + "fluent-bundle", + "fluent-langneg", + "fluent-syntax", + "flume", + "heck", + "ignore", + "intl-memoizer", + "lazy_static", + "log", + "once_cell", + "serde_json", + "snafu", + "tera", + "unic-langid", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -520,6 +614,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "intl-memoizer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -600,6 +713,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -781,6 +904,12 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.56" @@ -852,6 +981,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.13" @@ -881,12 +1016,24 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "scratch" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" + [[package]] name = "serde" version = "1.0.160" @@ -952,6 +1099,43 @@ dependencies = [ "deunicode", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "snafu" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0656e7e3ffb70f6c39b3c2a86332bb74aa3c679da781642590f3c1118c5045" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475b3bbe5245c26f2d8a6f62d67c1f30eb9fffeccee721c45d162c3ebbdf81b2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1010,6 +1194,7 @@ dependencies = [ "assert_cmd", "clap", "env_logger", + "fluent-templates", "log", "predicates", "serde", @@ -1063,6 +1248,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinystr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" +dependencies = [ + "displaydoc", +] + [[package]] name = "toml" version = "0.7.3" @@ -1097,6 +1291,15 @@ dependencies = [ "winnow", ] +[[package]] +name = "type-map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.16.0" @@ -1139,6 +1342,49 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" +[[package]] +name = "unic-langid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" +dependencies = [ + "unic-langid-impl", + "unic-langid-macros", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unic-langid-macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055e618bf694161ffff0466d95cef3e1a5edc59f6ba1888e97801f2b4ebdc4fe" +dependencies = [ + "proc-macro-hack", + "tinystr", + "unic-langid-impl", + "unic-langid-macros-impl", +] + +[[package]] +name = "unic-langid-macros-impl" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5cdec05b907f4e2f6843f4354f4ce6a5bebe1a56df320a49134944477ce4d8" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 1.0.109", + "unic-langid-impl", +] + [[package]] name = "unic-segment" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 12eef51..d7aec51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,14 @@ name = "teracli" readme = "README.md" version = "0.2.4" +[features] +default = [] +fluent = ["fluent-templates"] + [dependencies] clap = { version = "4", features = ["derive", "env", "unicode", "cargo"] } env_logger = "0.10" +fluent-templates = { version = "0.8", optional = true, default-features = false, features = ["tera"]} log = "0.4" serde = "1.0" serde_json = { version = "1.0", optional = false } diff --git a/data/basic/fluent.tera b/data/basic/fluent.tera new file mode 100644 index 0000000..d8cb604 --- /dev/null +++ b/data/basic/fluent.tera @@ -0,0 +1 @@ +{{ fluent(key="hello", name=en) }} {{ fluent(key="hello", lang="tr-TR", name=tr) }} diff --git a/data/basic/fluent.yaml b/data/basic/fluent.yaml new file mode 100644 index 0000000..968f3b5 --- /dev/null +++ b/data/basic/fluent.yaml @@ -0,0 +1,2 @@ +en: World +tr: Dünya diff --git a/data/locales/en-US/test.ftl b/data/locales/en-US/test.ftl new file mode 100644 index 0000000..b23f3e5 --- /dev/null +++ b/data/locales/en-US/test.ftl @@ -0,0 +1 @@ +hello = Hello { $name }! diff --git a/data/locales/tr-TR/test.ftl b/data/locales/tr-TR/test.ftl new file mode 100644 index 0000000..89898a7 --- /dev/null +++ b/data/locales/tr-TR/test.ftl @@ -0,0 +1 @@ +hello = Merhaba { $name }! diff --git a/src/main.rs b/src/main.rs index a57cf30..b407b58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,12 @@ use opts::*; use std::{fs::canonicalize, fs::File, io::Write, string::String}; use tera::{Context, Tera}; +#[cfg(feature = "fluent")] +use fluent_templates::{ArcLoader, FluentLoader, LanguageIdentifier}; + +#[cfg(feature = "fluent")] +use std::env; + fn main() -> Result<(), String> { env_logger::Builder::from_env(Env::default().default_filter_or("none")).init(); info!("Running {} v{}", crate_name!(), crate_version!()); @@ -30,13 +36,26 @@ fn main() -> Result<(), String> { path = canonicalize(opts.include_path.as_ref().unwrap()).unwrap(); } + #[cfg(feature = "fluent")] + let locale: LanguageIdentifier = match opts.locale.to_owned() { + Some(locale) => locale.parse(), + None => "und".parse(), + } + .unwrap(); + + #[cfg(feature = "fluent")] + let locales_path = match opts.locales_path.to_owned() { + Some(path) => path, + None => "./locales".into(), + }; + let mut wrapped_context = wrapped_context::WrappedContext::new(opts); wrapped_context.create_context(); let context: &Context = wrapped_context.context(); trace!("context:\n{:#?}", context); - let rendered; + let mut tera: Tera; if include { let mut dir = path.to_str().unwrap(); @@ -47,22 +66,32 @@ fn main() -> Result<(), String> { let glob = dir.to_owned() + "/**/*"; - let mut tera = match Tera::new(&glob) { + tera = match Tera::new(&glob) { Ok(t) => t, Err(e) => { println!("Parsing error(s): {e}"); ::std::process::exit(1); } }; + } else { + tera = Tera::default(); + } - if !autoescape { - tera.autoescape_on(vec![]); + if !autoescape { + tera.autoescape_on(vec![]) + }; + + #[cfg(feature = "fluent")] + if cfg!(feature = "fluent") { + let builder = + ArcLoader::builder(&locales_path, locale.clone()).customize(|bundle| bundle.set_use_isolating(false)); + if let Ok(locale_loader) = builder.build() { + let ftls = FluentLoader::new(locale_loader).with_default_lang(locale); + tera.register_function("fluent", ftls) } + }; - rendered = tera.render_str(&template, context).unwrap(); - } else { - rendered = Tera::one_off(&template, context, autoescape).unwrap(); - } + let rendered = tera.render_str(&template, context).unwrap(); if let Some(out_file) = output { debug!("Saving to {}", out_file.display()); diff --git a/src/opts.rs b/src/opts.rs index 6ea85a7..6016be7 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -19,6 +19,16 @@ pub struct Opts { #[clap(long, visible_alias = "inherit-path")] pub include_path: Option, + /// Option to set default locale used for Fluent localization functions. + #[cfg(feature = "fluent")] + #[clap(short, long)] + pub locale: Option, + + /// Option to define a path to Fluent resources used for localization. + #[cfg(feature = "fluent")] + #[clap(long, visible_alias = "locales-path")] + pub locales_path: Option, + /// Location of the context data. This file can be of the following type: /// json | toml | yaml. If you prefer to pass the data as stdin, use `--stdin` #[clap(index = 1, required_unless_present_any = &["stdin", "env_only"], conflicts_with = "env_only")] diff --git a/tests/test.rs b/tests/test.rs index 4002af5..b777f47 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -47,6 +47,22 @@ mod cli_tests { let assert = cmd.arg("-t").arg("data/basic/basic.tera").arg("data/basic/basic.toml").assert(); assert.success().stdout(predicate::str::contains("Bob likes orange")); } + + #[cfg(feature = "fluent")] + #[test] + fn it_proccess_fluent_functions() { + let mut cmd = Command::cargo_bin(env!("CARGO_BIN_EXE_tera")).unwrap(); + let assert = cmd + .arg("-l") + .arg("en-US") + .arg("--locales-path") + .arg("data/locales/") + .arg("-t") + .arg("data/basic/fluent.tera") + .arg("data/basic/fluent.yaml") + .assert(); + assert.success().stdout(predicate::str::contains("Hello World! Merhaba Dünya!")); + } } #[cfg(test)]