diff --git a/Cargo.toml b/Cargo.toml index 606e5fc..d2c8b7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Jason Lee "] build = "build.rs" categories = ["localization", "internationalization"] description = "Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts." -edition = "2018" +edition = "2021" exclude = ["crates", "tests"] keywords = ["i18n", "yml", "localization", "internationalization"] license = "MIT" @@ -13,25 +13,15 @@ repository = "https://github.com/longbridgeapp/rust-i18n" version = "2.2.1" [dependencies] -anyhow = {version = "1", optional = true} -clap = {version = "2.32", optional = true} -itertools = {version = "0.10.3", optional = true} once_cell = "1.10.0" -quote = {version = "1", optional = true} -rust-i18n-extract = {path = "./crates/extract", version = "2.1.0", optional = true} rust-i18n-support = {path = "./crates/support", version = "2.1.0"} rust-i18n-macro = {path = "./crates/macro", version = "2.1.0"} -serde = "1" -serde_derive = "1" -toml = "0.7.4" [dev-dependencies] foo = {path = "examples/foo"} criterion = "0.5" lazy_static = "1" - -[features] -default = ["rust-i18n-extract", "clap", "anyhow", "quote", "itertools"] +serde_yaml = "0.8" [build-dependencies] globwalk = "0.8.1" @@ -41,13 +31,9 @@ regex = "1" name = "app" test = true -[[bin]] -name = "cargo-i18n" -path = "src/main.rs" -required-features = ["default"] - [workspace] members = [ + "crates/cli", "crates/extract", "crates/support", "crates/macro", diff --git a/README.md b/README.md index dee8c6f..8ed5a54 100644 --- a/README.md +++ b/README.md @@ -29,25 +29,32 @@ rust-i18n = "2" Load macro and init translations in `lib.rs` or `main.rs`: -```rs +```rust,no_run // Load I18n macro, for allow you use `t!` macro in anywhere. #[macro_use] extern crate rust_i18n; - +# fn main() { +# fn v1() { // Init translations for current crate. i18n!("locales"); +# } +# fn v2() { // Or just use `i18n!`, default locales path is: "locales" in current crate. i18n!(); +# } +# fn v3() { // Config fallback missing translations to "en" locale. // Use `fallback` option to set fallback locale. i18n!("locales", fallback = "en"); +# } +# } ``` Or you can import by use directly: -```rs +```rust,no_run // You must import in each files when you wants use `t!` macro. use rust_i18n::t; @@ -149,13 +156,16 @@ This is useful when you use [GitHub Copilot](https://github.com/features/copilot Import the `t!` macro from this crate into your current scope: -```rs +```rust,no_run use rust_i18n::t; ``` Then, simply use it wherever a localized string is needed: -```rs +```rust,no_run +# fn _rust_i18n_translate(locale: &str, key: &str) -> String { todo!() } +# fn main() { +use rust_i18n::t; t!("hello"); // => "Hello world" @@ -173,13 +183,14 @@ t!("messages.hello", locale = "zh-CN", name = "Jason", count = 2); t!("messages.hello", locale = "zh-CN", "name" => "Jason", "count" => 3 + 2); // => "你好,Jason (5)" +# } ``` ### Current Locale You can use `rust_i18n::set_locale` to set the global locale at runtime, so that you don't have to specify the locale on each `t!` invocation. -```rs +```rust rust_i18n::set_locale("zh-CN"); let locale = rust_i18n::locale(); @@ -192,7 +203,17 @@ Since v2.0.0 rust-i18n support extend backend for cusomize your translation impl For example, you can use HTTP API for load translations from remote server: -```rs +```rust,no_run +# pub mod reqwest { +# pub mod blocking { +# pub struct Response; +# impl Response { +# pub fn text(&self) -> Result> { todo!() } +# } +# pub fn get(_url: &str) -> Result> { todo!() } +# } +# } +# use std::collections::HashMap; use rust_i18n::Backend; pub struct RemoteI18n { @@ -213,20 +234,28 @@ impl RemoteI18n { impl Backend for RemoteI18n { fn available_locales(&self) -> Vec<&str> { - return self.trs.keys().collect(); + return self.trs.keys().map(|k| k.as_str()).collect(); } fn translate(&self, locale: &str, key: &str) -> Option<&str> { // Write your own lookup logic here. // For example load from database - return self.trs.get(locale)?.get(key); + return self.trs.get(locale)?.get(key).map(|k| k.as_str()); } } ``` Now you can init rust_i18n by extend your own backend: -```rs +```rust,no_run +# struct RemoteI18n; +# impl RemoteI18n { +# fn new() -> Self { todo!() } +# } +# impl rust_i18n::Backend for RemoteI18n { +# fn available_locales(&self) -> Vec<&str> { todo!() } +# fn translate(&self, locale: &str, key: &str) -> Option<&str> { todo!() } +# } rust_i18n::i18n!("locales", backend = RemoteI18n::new()); ``` diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..2cbdd86 --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +description = "cargo-i18n tool for the rust-i18n crate." +edition = "2021" +license = "MIT" +name = "rust-i18n-cli" +readme = "../../README.md" +repository = "https://github.com/longbridgeapp/rust-i18n" +version = "2.1.0" + +[dependencies] +anyhow = "1" +clap = { version = "4.1.14", features = ["derive"] } +itertools = "0.11.0" +rust-i18n-support = { path = "../support", version = "2.1.0" } +rust-i18n-extract = { path = "../extract", version = "2.1.0" } +serde = { version = "1", features = ["derive"] } +toml = "0.7.4" + +[[bin]] +name = "cargo-i18n" +path = "src/main.rs" diff --git a/src/config.rs b/crates/cli/src/config.rs similarity index 97% rename from src/config.rs rename to crates/cli/src/config.rs index 53743ac..883a433 100644 --- a/src/config.rs +++ b/crates/cli/src/config.rs @@ -4,6 +4,7 @@ //! See `Manifest::from_slice`. use itertools::Itertools; +use serde::{Deserialize, Serialize}; use std::fs; use std::io; use std::io::Read; @@ -138,7 +139,7 @@ fn test_load_default() { #[test] fn test_load() { let workdir = Path::new(env!["CARGO_MANIFEST_DIR"]); - let cargo_root = workdir.join("examples/foo"); + let cargo_root = workdir.join("../../examples/foo"); let cfg = load(&cargo_root).unwrap(); assert_eq!(cfg.default_locale, "en"); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..7da0a96 --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,62 @@ +use anyhow::Error; +use clap::{Args, Parser}; + +use std::{collections::HashMap, path::Path}; + +use rust_i18n_extract::{extractor, generator, iter}; +mod config; + +#[derive(Parser)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +enum CargoCli { + I18n(I18nArgs), +} + +#[derive(Args)] +#[command(author, version)] +// #[command(propagate_version = true)] +/// Rust I18n command to help you extract all untranslated texts from source code. +/// +/// It will iterate all Rust files in the source directory and extract all untranslated texts +/// that used `t!` macro. +/// Then it will generate a YAML file and merge with the existing translations. +/// +/// https://github.com/longbridgeapp/rust-i18n +struct I18nArgs { + /// Extract all untranslated I18n texts from source code + #[arg(default_value = "./")] + source: Option, +} + +fn main() -> Result<(), Error> { + let CargoCli::I18n(args) = CargoCli::parse(); + + let mut results = HashMap::new(); + + let source_path = args.source.expect("Missing source path"); + + let cfg = config::load(std::path::Path::new(&source_path))?; + + iter::iter_crate(&source_path, |path, source| { + extractor::extract(&mut results, path, source) + })?; + + let mut messages: Vec<_> = results.values().collect(); + messages.sort_by_key(|m| m.index); + + let mut has_error = false; + + let output_path = Path::new(&source_path).join(&cfg.load_path); + + let result = generator::generate(&output_path, &cfg.available_locales, messages.clone()); + if result.is_err() { + has_error = true; + } + + if has_error { + std::process::exit(1); + } + + Ok(()) +} diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index 7db57d3..e9e9d0c 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -20,5 +20,8 @@ serde_json = "1" serde_yaml = "0.8" syn = "2.0.18" +[dev-dependencies] +rust-i18n = { path = "../..", version = "*" } + [lib] proc-macro = true diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index 038d491..448e50c 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -45,10 +45,17 @@ impl Args { impl syn::parse::Parse for Args { /// Parse macro arguments. /// - /// ```ignore + /// ```no_run + /// # use rust_i18n::i18n; + /// # fn v1() { /// i18n!(); + /// # } + /// # fn v2() { /// i18n!("locales"); + /// # } + /// # fn v3() { /// i18n!("locales", fallback = "en"); + /// # } /// ``` /// /// Ref: https://docs.rs/syn/latest/syn/parse/index.html @@ -81,10 +88,17 @@ impl syn::parse::Parse for Args { /// /// Attribute `fallback` for set the fallback locale, if present `t` macro will use it as the fallback locale. /// -/// ```ignore +/// ```no_run +/// # use rust_i18n::i18n; +/// # fn v1() { /// i18n!(); +/// # } +/// # fn v2() { /// i18n!("locales"); +/// # } +/// # fn v3() { /// i18n!("locales", fallback = "en"); +/// # } /// ``` #[proc_macro] pub fn i18n(input: proc_macro::TokenStream) -> proc_macro::TokenStream { diff --git a/crates/support/src/backend.rs b/crates/support/src/backend.rs index 721638e..cbea62c 100644 --- a/crates/support/src/backend.rs +++ b/crates/support/src/backend.rs @@ -59,11 +59,14 @@ impl SimpleBackend { /// Add more translations for the given locale. /// - /// ```ignore - /// let trs = HashMap::::new(); - /// trs.insert("hello".into(), "Hello".into()); - /// trs.insert("foo".into(), "Foo bar".into()); - /// backend.add_translations("en", &data); + /// ```no_run + /// # use std::collections::HashMap; + /// # use rust_i18n_support::SimpleBackend; + /// # let mut backend = SimpleBackend::new(); + /// let mut trs = HashMap::<&str, &str>::new(); + /// trs.insert("hello", "Hello"); + /// trs.insert("foo", "Foo bar"); + /// backend.add_translations("en", &trs); /// ``` pub fn add_translations(&mut self, locale: &str, data: &HashMap<&str, &str>) { let data = data diff --git a/examples/app-load-path/Cargo.toml b/examples/app-load-path/Cargo.toml index e1dcb7f..606de2f 100644 --- a/examples/app-load-path/Cargo.toml +++ b/examples/app-load-path/Cargo.toml @@ -6,4 +6,4 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rust-i18n = { path = "../../../rust-i18n" } +rust-i18n = { path = "../.." } diff --git a/examples/app-workspace/app1/Cargo.toml b/examples/app-workspace/app1/Cargo.toml index 2bef59e..da15f9a 100644 --- a/examples/app-workspace/app1/Cargo.toml +++ b/examples/app-workspace/app1/Cargo.toml @@ -5,4 +5,4 @@ version = "0.1.0" [dependencies] example-base = { path = "../crates/example-base" } -rust-i18n = { path = "../../../../rust-i18n" } +rust-i18n = { path = "../../.." } diff --git a/examples/app-workspace/crates/example-base/Cargo.toml b/examples/app-workspace/crates/example-base/Cargo.toml index d70db44..bf76175 100644 --- a/examples/app-workspace/crates/example-base/Cargo.toml +++ b/examples/app-workspace/crates/example-base/Cargo.toml @@ -6,4 +6,4 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rust-i18n = { path = "../../../../../rust-i18n" } +rust-i18n = { path = "../../../.." } diff --git a/examples/foo/Cargo.toml b/examples/foo/Cargo.toml index 6013d9b..5caa65d 100644 --- a/examples/foo/Cargo.toml +++ b/examples/foo/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rust-i18n = { path = "../../../rust-i18n" } +rust-i18n = { path = "../.." } [package.metadata.i18n] available-locales = ["en", "zh-CN"] diff --git a/examples/share-locales-in-workspace/my-app1/Cargo.toml b/examples/share-locales-in-workspace/my-app1/Cargo.toml index 422a659..5edff0c 100644 --- a/examples/share-locales-in-workspace/my-app1/Cargo.toml +++ b/examples/share-locales-in-workspace/my-app1/Cargo.toml @@ -4,5 +4,5 @@ name = "my-app1" version = "0.1.0" [dependencies] -rust-i18n = { path = "../../../../rust-i18n" } +rust-i18n = { path = "../../.." } my-i18n.workspace = true diff --git a/examples/share-locales-in-workspace/my-app2/Cargo.toml b/examples/share-locales-in-workspace/my-app2/Cargo.toml index f833d96..ec3fe00 100644 --- a/examples/share-locales-in-workspace/my-app2/Cargo.toml +++ b/examples/share-locales-in-workspace/my-app2/Cargo.toml @@ -4,5 +4,5 @@ name = "my-app2" version = "0.1.0" [dependencies] -rust-i18n = { path = "../../../../rust-i18n" } +rust-i18n = { path = "../../.." } my-i18n.workspace = true diff --git a/examples/share-locales-in-workspace/my-i18n/Cargo.toml b/examples/share-locales-in-workspace/my-i18n/Cargo.toml index c332a13..e3dfecf 100644 --- a/examples/share-locales-in-workspace/my-i18n/Cargo.toml +++ b/examples/share-locales-in-workspace/my-i18n/Cargo.toml @@ -4,4 +4,4 @@ name = "my-i18n" version = "0.1.0" [dependencies] -rust-i18n = { path = "../../../../rust-i18n" } +rust-i18n = { path = "../../.." } diff --git a/src/lib.rs b/src/lib.rs index d67e164..14a793d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -#![allow(rustdoc::invalid_rust_codeblocks)] #![doc = include_str!("../README.md")] use once_cell::sync::Lazy; @@ -24,18 +23,22 @@ pub fn locale() -> String { /// Get I18n text /// -/// ```ignore +/// ```no_run +/// #[macro_use] extern crate rust_i18n; +/// # fn _rust_i18n_translate(locale: &str, key: &str) -> String { todo!() } +/// # fn main() { /// // Simple get text with current locale /// t!("greeting"); // greeting: "Hello world" => "Hello world" /// // Get a special locale's text /// t!("greeting", locale = "de"); // greeting: "Hallo Welt!" => "Hallo Welt!" /// /// // With variables -/// t!("messages.hello", "world"); // messages.hello: "Hello, {}" => "Hello, world" -/// t!("messages.foo", "Foo", "Bar"); // messages.foo: "Hello, {} and {}" => "Hello, Foo and Bar" +/// t!("messages.hello", name = "world"); // messages.hello: "Hello, {name}" => "Hello, world" +/// t!("messages.foo", name = "Foo", other ="Bar"); // messages.foo: "Hello, {name} and {other}" => "Hello, Foo and Bar" /// /// // With locale and variables -/// t!("messages.hello", locale = "de", "Jason"); // messages.hello: "Hallo, {}" => "Hallo, Jason" +/// t!("messages.hello", locale = "de", name = "Jason"); // messages.hello: "Hallo, {name}" => "Hallo, Jason" +/// # } /// ``` #[macro_export] #[allow(clippy::crate_in_macro_def)] @@ -91,8 +94,12 @@ macro_rules! t { /// Get available locales /// -/// ```ignore +/// ```no_run +/// #[macro_use] extern crate rust_i18n; +/// # pub fn _rust_i18n_available_locales() -> Vec<&'static str> { todo!() } +/// # fn main() { /// rust_i18n::available_locales!(); +/// # } /// // => ["en", "zh-CN"] /// ``` #[macro_export(local_inner_macros)] diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 951dac9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,71 +0,0 @@ -use anyhow::Error; -use clap::{App, Arg, SubCommand}; - -use std::{collections::HashMap, path::Path}; - -use rust_i18n_extract::{extractor, generator, iter}; -mod config; - -#[macro_use] -extern crate serde_derive; - -const APP_NAME: &str = "rust-i18n"; -const ABOUT: &str = r#"Rust I18n command for help you simply to extract all untranslated texts from soruce code. - -It will iter all Rust files in and extract all untranslated texts that used `t!` macro. -And then generate a YAML file and merge for existing texts. - -https://github.com/longbridgeapp/rust-i18n -"#; - -fn main() -> Result<(), Error> { - let extract_command = SubCommand::with_name("i18n") - .about("Extract all untranslated I18n texts from soruce code") - .version(clap::crate_version!()) - .arg( - Arg::with_name("source") - .help("Path of your Rust crate root and Cargo.toml") - .default_value("./"), - ); - - let app = App::new(APP_NAME) - .bin_name("cargo") - .about(ABOUT) - .subcommand(extract_command) - .get_matches(); - - let mut results = HashMap::new(); - - #[allow(clippy::single_match)] - match app.subcommand() { - ("i18n", Some(sub_m)) => { - let source_path = sub_m.value_of("source").expect("Missing source path"); - - let cfg = config::load(std::path::Path::new(source_path))?; - - iter::iter_crate(source_path, |path, source| { - extractor::extract(&mut results, path, source) - })?; - - let mut messages: Vec<_> = results.values().collect(); - messages.sort_by_key(|m| m.index); - - let mut has_error = false; - - let output_path = Path::new(source_path).join(&cfg.load_path); - - let result = - generator::generate(&output_path, &cfg.available_locales, messages.clone()); - if result.is_err() { - has_error = true; - } - - if has_error { - std::process::exit(1); - } - } - _ => {} - } - - Ok(()) -}