From 8f4b4156f38774fe6335cb9b6b7d188c1563c724 Mon Sep 17 00:00:00 2001 From: Baptiste Gelez Date: Sun, 20 Jan 2019 12:28:23 +0100 Subject: [PATCH] Big code simplification Also added i18n_domain!, compile_i18n! and include_i18n! --- .gitignore | 3 +- Cargo.toml | 1 + gettext-utils/src/lib.rs | 1 + src/lib.rs | 209 ++++++++++++++++++++++----------------- tests/main.rs | 28 ++---- 5 files changed, 129 insertions(+), 113 deletions(-) diff --git a/.gitignore b/.gitignore index 9382b24..09a48bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target **/*.rs.bk Cargo.lock -po \ No newline at end of file +po +translations \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9d1e2e6..d251d90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ proc-macro = true [dependencies] gettext-utils = { path = "gettext-utils" } +gettext = "0.3" [workspace] members = ["gettext-utils"] \ No newline at end of file diff --git a/gettext-utils/src/lib.rs b/gettext-utils/src/lib.rs index 4f1e60a..4d0aab0 100644 --- a/gettext-utils/src/lib.rs +++ b/gettext-utils/src/lib.rs @@ -56,3 +56,4 @@ pub fn try_format<'a>( } ::std::result::Result::Ok(res) } + diff --git a/src/lib.rs b/src/lib.rs index 90c4f78..29f514c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,8 @@ #![feature(proc_macro_hygiene, proc_macro_quote, proc_macro_span, uniform_paths)] extern crate proc_macro; - -use std::{env, io::{BufRead, Write}, fs::{create_dir_all, read, write, File, OpenOptions}, iter::FromIterator, path::{Path, PathBuf}}; -use proc_macro::{Delimiter, Group, Literal, Spacing, Punct, TokenStream, TokenTree, quote}; +use std::{env, io::{BufRead, Write}, fs::{create_dir_all, read, File, OpenOptions}, iter::FromIterator, path::Path, process::Command}; +use proc_macro::{Literal, Spacing, Punct, TokenStream, TokenTree, quote}; fn is(t: &TokenTree, ch: char) -> bool { match t { @@ -20,19 +19,9 @@ pub fn i18n(input: TokenStream) -> TokenStream { let file = span.source_file().path(); let line = span.start().line; - let mut domain = String::new(); let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); - let domain_meta = read(out_dir.join("domain_paths")).expect("Domain metadata not found. Make sure to call configure_i18n! before using i18n!"); - let mut lines = domain_meta.lines(); - loop { - domain = lines.next().unwrap().unwrap(); - if file.starts_with(lines.next().unwrap().unwrap()) { - break; - } - } - let out = read(out_dir.join(domain)).expect("Couldn't read output metadata"); - let pot_file = out.lines().next().unwrap().unwrap(); - let mut pot = OpenOptions::new().append(true).create(true).open(pot_file + ".pot").expect("Couldn't open .pot file"); + let domain = read(out_dir.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))).expect("Coudln't read domain, make sure to call init_i18n! before").lines().next().unwrap().unwrap(); + let mut pot = OpenOptions::new().append(true).create(true).open(format!("po/{0}/{0}.pot", domain)).expect("Couldn't open .pot file"); for _ in 0..(catalog.len() + 1) { input.next(); } let message = input.next().unwrap(); @@ -48,28 +37,22 @@ pub fn i18n(input: TokenStream) -> TokenStream { }; let mut format_args = vec![]; - println!("{}, input is {:?}", message.to_string(), input.clone().collect::>()); if let Some(TokenTree::Punct(p)) = input.next().clone() { if p.as_char() == ';' { loop { - println!("looking for fa, {:?}", input.clone().collect::>()); let mut tokens = vec![]; loop { if let Some(t) = input.next().clone() { if !is(&t, ',') { - println!("found tok"); tokens.push(t); } else { - println!("next"); break; } } else { - println!("end"); break; } } if tokens.is_empty() { - println!("tokens is empty"); break; } format_args.push(TokenStream::from_iter(tokens.into_iter())); @@ -83,7 +66,7 @@ pub fn i18n(input: TokenStream) -> TokenStream { # {}:{} msgid {} msgid_plural {} -msgstr "" +msgstr[0] "" "#, file.to_str().unwrap(), line, message, pl).into_bytes()).expect("Couldn't write message to .pot (plural)"); let count = format_args.clone().into_iter().next().expect("Item count should be specified").clone(); res.extend(quote!( @@ -102,7 +85,6 @@ msgstr "" } let mut args = vec![]; let mut first = true; - println!("found {} args: {:?}", format_args.len(), format_args); for arg in format_args { if first { first = false; @@ -116,44 +98,18 @@ msgstr "" let res = quote!({ ::gettext_utils::try_format($res, &[$fargs]).expect("Error while formatting message") }); - println!("emit {}", res); res } #[proc_macro] -pub fn configure_i18n(input: TokenStream) -> TokenStream { - let mut input = input.into_iter(); +pub fn init_i18n(input: TokenStream) -> TokenStream { + let mut input = input.into_iter(); let domain = match input.next() { Some(TokenTree::Literal(lit)) => lit.to_string().replace("\"", ""), Some(_) => panic!("Domain should be a str"), None => panic!("Expected a translation domain (for instance \"myapp\")"), }; let mut langs = vec![]; - let mut path = String::new(); - if let Some(t) = input.next() { - if is(&t, ',') { - match input.next() { - Some(TokenTree::Literal(l)) => { path = l.to_string().replace("\"", ""); } - Some(TokenTree::Ident(i)) => { - langs.push(i); - loop { - let next = input.next(); - if next.is_none() || !is(&next.unwrap(), ',') { - break; - } - match input.next() { - Some(TokenTree::Ident(i)) => { langs.push(i); }, - _ => panic!("Expected a language identifier") - } - } - }, - _ => panic!("Expected a language identifier or a path to store translations"), - } - } else { - panic!("Expected `,`") - } - }; - if let Some(t) = input.next() { if is(&t, ',') { match input.next() { @@ -175,21 +131,21 @@ pub fn configure_i18n(input: TokenStream) -> TokenStream { } else { panic!("Expected `,`") } - }; + } // emit file to include let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); - let out = out_dir.join(domain.clone()); + let out = out_dir.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo")); create_dir_all(out_dir).expect("Couldn't create output dir"); let mut out = File::create(out).expect("Metadata file couldn't be open"); - writeln!(out, "{}", path).expect("Couldn't write path"); + writeln!(out, "{}", domain).expect("Couldn't write domain"); for l in langs { - writeln!(out, "{}", l).expect("Couldn't write lang"); + writeln!(out, "{}", l).expect("Couldn't write lang"); } // write base .pot - create_dir_all(PathBuf::from(path.clone()).parent().unwrap()).expect("Couldn't create po dir"); - let mut pot = OpenOptions::new().write(true).create(true).truncate(true).open(path + ".pot").expect("Couldn't open .pot file"); + create_dir_all(format!("po/{}", domain)).expect("Couldn't create po dir"); + let mut pot = OpenOptions::new().write(true).create(true).truncate(true).open(format!("po/{0}/{0}.pot", domain)).expect("Couldn't open .pot file"); pot.write_all(&format!(r#"msgid "" msgstr "" "Project-Id-Version: {}\n" @@ -204,44 +160,115 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "#, domain).into_bytes()).expect("Couldn't init .pot file"); - quote!({}) + + quote!() } #[proc_macro] -pub fn init_i18n(input: TokenStream) -> TokenStream { - let domain_tok = input.into_iter().next().expect("Expected a domain"); +pub fn i18n_domain(_: TokenStream) -> TokenStream { + let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); + let domain = read(out_dir.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))).expect("Coudln't read domain, make sure to call init_i18n! before").lines().next().unwrap().unwrap(); + let tok = TokenTree::Literal(Literal::string(&domain)); + quote!($tok) +} - let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); - let code_file = domain_tok.span().source_file().path(); - let code_dir = code_file.parent().unwrap(); - let domain = domain_tok.to_string().replace("\"", ""); - write( - out_dir.join("domain_paths"), - String::from_utf8(read(out_dir.join("domain_paths")).unwrap_or_default()).unwrap() + &format!("{}\n{}\n", domain, code_dir.to_str().unwrap()) - ).expect("Couldn't update domain paths"); - let out = out_dir.join(domain.to_string().replace("\"", "")); - let meta = read(out).expect("Couldn't read metadata file"); - let mut lines = meta.lines(); - let dir = TokenTree::Literal(Literal::string(&lines.next().expect("Metadata file is not properly configured") - .expect("Couldn't read output dir location"))); +#[proc_macro] +pub fn compile_i18n(_: TokenStream) -> TokenStream { + let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); + let file = read(out_dir.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))).expect("Coudln't read domain, make sure to call init_i18n! before"); + let mut lines = file.lines(); + let domain = lines.next().unwrap().unwrap(); + let locales = lines.map(|l| l.unwrap()).collect::>(); - let mut langs = vec![]; - for lang in lines { - langs.push(TokenTree::Literal(Literal::string(&lang.expect("Couldn't read lang")))); - } - let langs = TokenTree::Group(Group::new( - Delimiter::Bracket, - TokenStream::from_iter(langs.into_iter().map(|l| vec![l, TokenTree::Punct(Punct::new(',', Spacing::Alone))]).flatten()), - )); - quote!( - pub mod __i18n { - pub static DOMAIN: &'static str = $domain_tok; - - pub static PO_DIR: &'static str = $dir; - - pub fn langs() -> ::std::vec::Vec<&'static str> { - vec!$langs - } + let pot_path = Path::new("po").join(domain.clone()).join(format!("{}.pot", domain)); + + for lang in locales { + let po_path = Path::new("po").join(format!("{}.po", lang.clone())); + if po_path.exists() && po_path.is_file() { + println!("Updating {}", lang.clone()); + // Update it + Command::new("msgmerge") + .arg("-U") + .arg(po_path.to_str().unwrap()) + .arg(pot_path.to_str().unwrap()) + .status() + .map(|s| { + if !s.success() { + panic!("Couldn't update PO file") + } + }) + .expect("Couldn't update PO file"); + } else { + println!("Creating {}", lang.clone()); + // Create it from the template + Command::new("msginit") + .arg(format!("--input={}", pot_path.to_str().unwrap())) + .arg(format!("--output-file={}", po_path.to_str().unwrap())) + .arg("-l") + .arg(lang.clone()) + .arg("--no-translator") + .status() + .map(|s| { + if !s.success() { + panic!("Couldn't init PO file (gettext returned an error)") + } + }) + .expect("Couldn't init PO file"); } - ) + + // Generate .mo + let po_path = Path::new("po").join(format!("{}.po", lang.clone())); + let mo_dir = Path::new("translations") + .join(lang.clone()) + .join("LC_MESSAGES"); + create_dir_all(mo_dir.clone()).expect("Couldn't create MO directory"); + let mo_path = mo_dir.join(format!("{}.mo", domain)); + + Command::new("msgfmt") + .arg(format!("--output-file={}", mo_path.to_str().unwrap())) + .arg(po_path) + .status() + .map(|s| { + if !s.success() { + panic!("Couldn't compile translations (gettext returned an error)") + } + }) + .expect("Couldn't compile translations"); + } + quote!() +} + +/// Use this macro to staticaly import translations into your final binary. +/// +/// ```rust,ignore +/// # //ignore because there is no translation file provided with rocket_i18n +/// # #[macro_use] +/// # extern crate rocket_i18n; +/// # use rocket_i18n::Translations; +/// let tr: Translations = include_i18n!(); +/// ``` +#[proc_macro] +pub fn include_i18n(_: TokenStream) -> TokenStream { + let out_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target/debug".into())).join("gettext_macros"); + let file = read(out_dir.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))).expect("Coudln't read domain, make sure to call init_i18n! before"); + let mut lines = file.lines(); + let domain = TokenTree::Literal(Literal::string(&lines.next().unwrap().unwrap())); + let locales = lines + .map(Result::unwrap) + .map(|l| { + let lang = TokenTree::Literal(Literal::string(&l)); + quote!({ + ::gettext::Catalog::parse( + &include_bytes!( + concat!(env!("CARGO_MANIFEST_DIR"), "/translations/", $lang, "/LC_MESSAGES/", $domain, ".mo") + )[..] + ).expect("Error while loading catalog") + }, ) + }).collect::(); + + quote!({ + vec![ + $locales + ] + }) } diff --git a/tests/main.rs b/tests/main.rs index 5c611af..cbe1745 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2,32 +2,18 @@ use gettext_macros::*; -struct Catalog; - -impl Catalog { - pub fn gettext(&self, msg: &'static str) -> &'static str { - msg - } - - pub fn ngettext(&self, msg: &'static str, _pl: &'static str, _count: i32) -> &'static str { - msg - } -} - -#[allow(dead_code)] -fn build() { - configure_i18n!("test", "po/test", fr, en, de); -} - -init_i18n!("test"); - -pub mod i18n {} +init_i18n!("test", fr, en, de, ja); #[test] fn main() { - let cat = Catalog; + let catalogs = include_i18n!(); + let cat = &catalogs[0]; let x = i18n!(cat, "Hello"); let b = i18n!(cat, "Singular", "Plural"; 0); println!("{} {}", x, b); println!("{}", i18n!(cat, "Woohoo, it {}"; "works")); + println!(i18n_domain!()); + } + +compile_i18n!();