Browse Source

Add format args support

pull/2/head
Baptiste Gelez 3 years ago
parent
commit
7bb2631e5f
  1. 6
      Cargo.toml
  2. 61
      README.md
  3. 7
      gettext-utils/Cargo.toml
  4. 58
      gettext-utils/src/lib.rs
  5. 75
      src/lib.rs
  6. 5
      tests/main.rs

6
Cargo.toml

@ -8,5 +8,7 @@ edition = "2018"
proc-macro = true
[dependencies]
# syn = "0.15"
quote = "0.6"
gettext-utils = { path = "gettext-utils" }
[workspace]
members = ["gettext-utils"]

61
README.md

@ -0,0 +1,61 @@
# Gettext macros
A few proc-macros to help you internationalize your Rust apps.
## How does it works?
There are three macros:
- `configure_i18n`, that should be in your `build.rs` file. It
allows you to configure multiple translation domains, where to store
their .pot file and which langage they support.
- `init_i18n`, that should be called in your crate root. It tells which
domain to use for this crate.
- `i18n`, that translates a given message
The advantage of these macros is that they allow you to work with multiple translation
domains (for instance, one for each of your workspace's crate), and that they automatically
generate a .pot file for these domains.
## Example
*build.rs*
```rust
use gettext_macros::configure_i18n;
fn main() {
// Configure two different translation domains, with different locales
configure_i18n!("my_app", ar, en, de, fr, it, ja); // This one will have its translations stored in ./po
configure_i18n!("another_domain", "po/other_domain", en, de, ja); // This one in ./po/other_domain
}
```
*main.rs*
```rust
// The translations for this module and its submodules will be
// loaded from the "my_app" domain.
init_i18n!("my_app");
fn init_catalog() -> gettext::Catalog {
// return the correct catalog for the user's language,
// usually with another crate.
}
fn main() {
let catalog = init_catalog();
println!("{}", i18n!(catalog, "Hello, world!"));
let name = "Jane";
println!("{}", i18n!(catalog, "Hello, {}!"; name));
let message_count = 42;
println!("{}", i18n!(catalog, "You have one new message", "You have {0} new messages"; message_count));
}
```
## TODO
- Code cleanup
- Format args checking
- Use package name as default domain
- Add build functions to gettext-utils

7
gettext-utils/Cargo.toml

@ -0,0 +1,7 @@
[package]
name = "gettext-utils"
version = "0.1.0"
authors = ["Baptiste Gelez <baptiste@gelez.xyz>"]
edition = "2018"
[dependencies]

58
gettext-utils/src/lib.rs

@ -0,0 +1,58 @@
#[derive(Debug)]
#[doc(hidden)]
pub enum FormatError {
UnmatchedCurlyBracket,
InvalidPositionalArgument,
}
#[doc(hidden)]
pub fn try_format<'a>(
str_pattern: &'a str,
argv: &[::std::boxed::Box<dyn ::std::fmt::Display + 'a>],
) -> ::std::result::Result<::std::string::String, FormatError> {
use ::std::fmt::Write;
use ::std::iter::Iterator;
//first we parse the pattern
let mut pattern = vec![];
let mut vars = vec![];
let mut finish_or_fail = false;
for (i, part) in str_pattern.split('}').enumerate() {
if finish_or_fail {
return ::std::result::Result::Err(FormatError::UnmatchedCurlyBracket);
}
if part.contains('{') {
let mut part = part.split('{');
let text = part.next().unwrap();
let arg = part.next().ok_or(FormatError::UnmatchedCurlyBracket)?;
if part.next() != ::std::option::Option::None {
return ::std::result::Result::Err(FormatError::UnmatchedCurlyBracket);
}
pattern.push(text);
vars.push(
argv.get::<usize>(if arg.len() > 0 {
arg.parse()
.map_err(|_| FormatError::InvalidPositionalArgument)?
} else {
i
})
.ok_or(FormatError::InvalidPositionalArgument)?,
);
} else {
finish_or_fail = true;
pattern.push(part);
}
}
//then we generate the result String
let mut res = ::std::string::String::with_capacity(str_pattern.len());
let mut pattern = pattern.iter();
let mut vars = vars.iter();
while let ::std::option::Option::Some(text) = pattern.next() {
res.write_str(text).unwrap();
if let ::std::option::Option::Some(var) = vars.next() {
res.write_str(&format!("{}", var)).unwrap();
}
}
::std::result::Result::Ok(res)
}

75
src/lib.rs

@ -1,8 +1,8 @@
#![feature(proc_macro_hygiene, proc_macro_quote, proc_macro_span)]
#![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};
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};
fn is(t: &TokenTree, ch: char) -> bool {
@ -37,8 +37,9 @@ pub fn i18n(input: TokenStream) -> TokenStream {
for _ in 0..(catalog.len() + 1) { input.next(); }
let message = input.next().unwrap();
let plural = match input.next().clone() {
let plural = match input.clone().next() {
Some(t) => if is(&t, ',') {
input.next();
input.next()
} else {
None
@ -47,13 +48,31 @@ pub fn i18n(input: TokenStream) -> TokenStream {
};
let mut format_args = vec![];
println!("{}, input is {:?}", message.to_string(), input.clone().collect::<Vec<_>>());
if let Some(TokenTree::Punct(p)) = input.next().clone() {
if p.as_char() == ';' {
loop {
format_args.push(TokenStream::from_iter(input.clone().take_while(|t| !is(t, ','))));
if input.next().is_none() {
println!("looking for fa, {:?}", input.clone().collect::<Vec<_>>());
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()));
}
}
}
@ -65,9 +84,8 @@ pub fn i18n(input: TokenStream) -> TokenStream {
msgid {}
msgid_plural {}
msgstr ""
"#, file.to_str().unwrap(), line, message, pl).into_bytes());
let count = format_args.into_iter().next().expect("Item count should be specified").clone();
"#, 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!(
.ngettext($message, $pl, $count)
))
@ -76,12 +94,29 @@ msgstr ""
# {}:{}
msgid {}
msgstr ""
"#, file.to_str().unwrap(), line, message).into_bytes());
"#, file.to_str().unwrap(), line, message).into_bytes()).expect("Couldn't write message to .pot");
res.extend(quote!(
.gettext($message)
))
}
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;
} else {
args.push(TokenTree::Punct(Punct::new(',', Spacing::Alone)));
}
args.extend(quote!(Box::new($arg)));
}
let mut fargs = TokenStream::new();
fargs.extend(args);
let res = quote!({
::gettext_utils::try_format($res, &[$fargs]).expect("Error while formatting message")
});
println!("emit {}", res);
res
}
@ -153,7 +188,8 @@ pub fn configure_i18n(input: TokenStream) -> TokenStream {
}
// write base .pot
let mut pot = OpenOptions::new().write(true).truncate(true).open(path + ".pot").expect("Couldn't open .pot file");
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");
pot.write_all(&format!(r#"msgid ""
msgstr ""
"Project-Id-Version: {}\n"
@ -167,7 +203,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
"#, domain).into_bytes());
"#, domain).into_bytes()).expect("Couldn't init .pot file");
quote!({})
}
@ -182,7 +218,7 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
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();
@ -198,7 +234,7 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
TokenStream::from_iter(langs.into_iter().map(|l| vec![l, TokenTree::Punct(Punct::new(',', Spacing::Alone))]).flatten()),
));
quote!(
mod __i18n {
pub mod __i18n {
pub static DOMAIN: &'static str = $domain_tok;
pub static PO_DIR: &'static str = $dir;
@ -209,16 +245,3 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
}
)
}
/*
fn parse_format_args()
#[proc_macro]
pub fn configure_i18n(input)
////////
fn build() {
configure_i18n!("foo", "po/foo");
configure_i18n!("bar"); // default to "po"
}*/

5
tests/main.rs

@ -1,4 +1,4 @@
#![feature(proc_macro_hygiene, decl_macro)]
#![feature(proc_macro_hygiene, decl_macro, uniform_paths)]
use gettext_macros::*;
@ -21,10 +21,13 @@ fn build() {
init_i18n!("test");
pub mod i18n {}
#[test]
fn main() {
let cat = Catalog;
let x = i18n!(cat, "Hello");
let b = i18n!(cat, "Singular", "Plural"; 0);
println!("{} {}", x, b);
println!("{}", i18n!(cat, "Woohoo, it {}"; "works"));
}
Loading…
Cancel
Save