Make it compile on stable
- Restore gettext_utils, to use it instead of runtime-fmt - Don't use some Span API that are only on Nightly - Introduce quote, syn and proc-macro2 to make parsing cleaner, easier and more correct in complex cases
This commit is contained in:
parent
843d068a38
commit
38f16d7418
6 changed files with 283 additions and 246 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "gettext-macros"
|
name = "gettext-macros"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
authors = ["Ana Gelez <ana@gelez.xyz>"]
|
authors = ["Plume contributors"]
|
||||||
description = "A few proc-macros to help internationalizing Rust applications"
|
description = "A few proc-macros to help internationalizing Rust applications"
|
||||||
repository = "https://github.com/Plume-org/gettext-macros"
|
repository = "https://github.com/Plume-org/gettext-macros"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
@ -12,4 +12,10 @@ proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gettext = "0.4"
|
gettext = "0.4"
|
||||||
runtime-fmt = "0.4"
|
gettext-utils = "0.1.0"
|
||||||
|
proc-macro2 = { version = "1.0.19", features = ["span-locations"] }
|
||||||
|
quote = "1.0.7"
|
||||||
|
syn = "1.0"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["gettext-utils"]
|
||||||
|
|
9
gettext-utils/Cargo.toml
Normal file
9
gettext-utils/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "gettext-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Plume contributors"]
|
||||||
|
description = "Utility crate for gettext-macros"
|
||||||
|
license = "GPL-3.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
66
gettext-utils/src/lib.rs
Normal file
66
gettext-utils/src/lib.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
//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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn basic_test() {
|
||||||
|
assert_eq!(super::try_format("Hello {}", &[Box::new("world")]).unwrap(), "Hello world");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
nightly-2019-08-28
|
|
441
src/lib.rs
441
src/lib.rs
|
@ -1,19 +1,19 @@
|
||||||
#![feature(proc_macro_hygiene, proc_macro_quote, proc_macro_span, external_doc)]
|
//! A set of macros to make i18n easier.
|
||||||
|
|
||||||
#![doc(include = "../README.md")]
|
|
||||||
|
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
use proc_macro::{
|
use proc_macro::TokenStream;
|
||||||
quote, token_stream::IntoIter as TokenIter, Delimiter, Literal, TokenStream, TokenTree,
|
use proc_macro2::{
|
||||||
|
token_stream::IntoIter as TokenIter, Literal, TokenTree,
|
||||||
};
|
};
|
||||||
|
use quote::quote;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
fs::{create_dir_all, read, File, OpenOptions},
|
fs::{create_dir_all, read, File, OpenOptions},
|
||||||
io::{BufRead, Read, Seek, SeekFrom, Write},
|
io::{BufRead, Read, Seek, SeekFrom, Write},
|
||||||
iter::FromIterator,
|
|
||||||
path::Path,
|
path::Path,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
|
use syn::Token;
|
||||||
|
|
||||||
fn is(t: &TokenTree, ch: char) -> bool {
|
fn is(t: &TokenTree, ch: char) -> bool {
|
||||||
match t {
|
match t {
|
||||||
|
@ -22,45 +22,7 @@ fn is(t: &TokenTree, ch: char) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_empty(t: &TokenTree) -> bool {
|
fn named_arg(mut input: TokenIter, name: &'static str) -> Option<proc_macro2::TokenStream> {
|
||||||
match t {
|
|
||||||
TokenTree::Literal(lit) => format!("{}", lit).len() == 2,
|
|
||||||
TokenTree::Group(grp) => {
|
|
||||||
if grp.delimiter() == Delimiter::None {
|
|
||||||
grp.stream()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|t| is_empty(&t))
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty_ts(t: &TokenStream) -> bool {
|
|
||||||
t.clone().into_iter().fold(true, |r, t| r && is_empty(&t))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trim(t: TokenTree) -> TokenTree {
|
|
||||||
match t {
|
|
||||||
TokenTree::Group(grp) => {
|
|
||||||
if grp.delimiter() == Delimiter::None {
|
|
||||||
grp.stream()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.expect("Unexpected empty expression")
|
|
||||||
} else {
|
|
||||||
TokenTree::Group(grp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x => x,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn named_arg(mut input: TokenIter, name: &'static str) -> Option<TokenStream> {
|
|
||||||
input.next().and_then(|t| match t {
|
input.next().and_then(|t| match t {
|
||||||
TokenTree::Ident(ref i) if i.to_string() == name => {
|
TokenTree::Ident(ref i) if i.to_string() == name => {
|
||||||
input.next(); // skip "="
|
input.next(); // skip "="
|
||||||
|
@ -97,7 +59,6 @@ struct Config {
|
||||||
domain: String,
|
domain: String,
|
||||||
make_po: bool,
|
make_po: bool,
|
||||||
make_mo: bool,
|
make_mo: bool,
|
||||||
write_loc: bool,
|
|
||||||
langs: Vec<String>,
|
langs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,17 +96,10 @@ impl Config {
|
||||||
.expect("IO error while reading config")
|
.expect("IO error while reading config")
|
||||||
.parse()
|
.parse()
|
||||||
.expect("Couldn't parse make_mo");
|
.expect("Couldn't parse make_mo");
|
||||||
let write_loc: bool = lines
|
|
||||||
.next()
|
|
||||||
.expect("Invalid config file. Make sure to call init_i18n! before this macro")
|
|
||||||
.expect("IO error while reading config")
|
|
||||||
.parse()
|
|
||||||
.expect("Couldn't parse write_loc");
|
|
||||||
Config {
|
Config {
|
||||||
domain,
|
domain,
|
||||||
make_po,
|
make_po,
|
||||||
make_mo,
|
make_mo,
|
||||||
write_loc,
|
|
||||||
langs: lines
|
langs: lines
|
||||||
.map(|l| l.expect("IO error while reading config"))
|
.map(|l| l.expect("IO error while reading config"))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -159,88 +113,20 @@ impl Config {
|
||||||
writeln!(out, "{}", self.domain).expect("Couldn't write domain");
|
writeln!(out, "{}", self.domain).expect("Couldn't write domain");
|
||||||
writeln!(out, "{}", self.make_po).expect("Couldn't write po settings");
|
writeln!(out, "{}", self.make_po).expect("Couldn't write po settings");
|
||||||
writeln!(out, "{}", self.make_mo).expect("Couldn't write mo settings");
|
writeln!(out, "{}", self.make_mo).expect("Couldn't write mo settings");
|
||||||
writeln!(out, "{}", self.write_loc).expect("Couldn't write location settings");
|
|
||||||
for l in self.langs.clone() {
|
for l in self.langs.clone() {
|
||||||
writeln!(out, "{}", l).expect("Couldn't write lang");
|
writeln!(out, "{}", l).expect("Couldn't write lang");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Message {
|
trait Message {
|
||||||
content: TokenStream,
|
fn writable(&self) -> bool;
|
||||||
plural: Option<TokenStream>,
|
fn content(&self) -> String;
|
||||||
context: Option<TokenTree>,
|
fn context(&self) -> Option<String>;
|
||||||
format_args: TokenStream,
|
fn plural(&self) -> Option<String>;
|
||||||
writable: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
fn write(&self) {
|
||||||
fn parse(mut input: TokenIter, str_only: bool) -> Message {
|
if !self.writable() {
|
||||||
let context = named_arg(input.clone(), "context");
|
|
||||||
if let Some(c) = context.clone() {
|
|
||||||
for _ in 0..(c.into_iter().count() + 3) {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let content = if str_only {
|
|
||||||
TokenStream::from_iter(vec![trim(
|
|
||||||
input.next().expect("Expected a message to translate"),
|
|
||||||
)])
|
|
||||||
} else {
|
|
||||||
let res: TokenStream = input
|
|
||||||
.clone()
|
|
||||||
.take_while(|t| !is(&t, ',') && !is(&t, ';'))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for _ in 0..(res.clone().into_iter().count()) {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let plural: Option<TokenStream> = match input.clone().next() {
|
|
||||||
Some(t) => {
|
|
||||||
if is(&t, ',') {
|
|
||||||
input.next();
|
|
||||||
Some(input.clone().take_while(|t| !is(t, ';')).collect())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(p) = plural.clone() {
|
|
||||||
for _ in 0..(p.into_iter().count() + 1) {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(t) = input.clone().next() {
|
|
||||||
if is(&t, ';') {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message {
|
|
||||||
context: context.and_then(|c| c.into_iter().next()),
|
|
||||||
plural,
|
|
||||||
format_args: input.collect(),
|
|
||||||
writable: content
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|t| match trim(t) {
|
|
||||||
TokenTree::Literal(_) => true,
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.unwrap_or(false),
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&self, location: Option<(std::path::PathBuf, usize)>) {
|
|
||||||
if !self.writable {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,43 +145,39 @@ impl Message {
|
||||||
pot.seek(SeekFrom::End(0))
|
pot.seek(SeekFrom::End(0))
|
||||||
.expect("IO error while seeking .pot file to end");
|
.expect("IO error while seeking .pot file to end");
|
||||||
|
|
||||||
let already_exists = is_empty_ts(&self.content)
|
let already_exists = self.content().is_empty()
|
||||||
|| contents.contains(&format!(
|
|| contents.contains(&format!(
|
||||||
"{}msgid {}",
|
r#"{}msgid "{}""#,
|
||||||
self.context
|
self.context()
|
||||||
.clone()
|
.clone()
|
||||||
.map(|c| format!("msgctxt {}\n", c))
|
.map(|c| format!(
|
||||||
|
r#"msgctxt "{}"
|
||||||
|
"#,
|
||||||
|
c))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
self.content
|
self.content()
|
||||||
));
|
));
|
||||||
if already_exists {
|
if already_exists {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let code_path = match location
|
let prefix = if let Some(c) = self.context() {
|
||||||
.clone()
|
format!(
|
||||||
.and_then(|(f, l)| f.clone().to_str().map(|s| (s.to_string(), l)))
|
r#"msgctxt "{}"
|
||||||
{
|
"#, c)
|
||||||
Some((ref path, line)) if !location.unwrap().0.is_absolute() => {
|
|
||||||
format!("#: {}:{}\n", path, line)
|
|
||||||
}
|
|
||||||
_ => String::new(),
|
|
||||||
};
|
|
||||||
let prefix = if let Some(c) = self.context.clone() {
|
|
||||||
format!("{}msgctxt {}\n", code_path, c)
|
|
||||||
} else {
|
} else {
|
||||||
code_path
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref pl) = self.plural {
|
if let Some(ref pl) = self.plural() {
|
||||||
pot.write_all(
|
pot.write_all(
|
||||||
&format!(
|
&format!(
|
||||||
r#"
|
r#"
|
||||||
{}msgid {}
|
{}msgid "{}"
|
||||||
msgid_plural {}
|
msgid_plural "{}"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
"#,
|
"#,
|
||||||
prefix, self.content, pl,
|
prefix, self.content(), pl,
|
||||||
)
|
)
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
)
|
)
|
||||||
|
@ -304,10 +186,10 @@ msgstr[0] ""
|
||||||
pot.write_all(
|
pot.write_all(
|
||||||
&format!(
|
&format!(
|
||||||
r#"
|
r#"
|
||||||
{}msgid {}
|
{}msgid "{}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"#,
|
"#,
|
||||||
prefix, self.content,
|
prefix, self.content(),
|
||||||
)
|
)
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
)
|
)
|
||||||
|
@ -316,6 +198,127 @@ msgstr ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct I18nCall {
|
||||||
|
catalog: syn::Expr,
|
||||||
|
context: Option<syn::LitStr>,
|
||||||
|
msg: syn::Expr,
|
||||||
|
plural: Option<syn::Expr>,
|
||||||
|
format_args: Option<syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod kw {
|
||||||
|
syn::custom_keyword!(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl syn::parse::Parse for I18nCall {
|
||||||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let catalog = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let context = if input.parse::<kw::context>().is_ok() {
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
let ctx = input.parse().ok();
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
ctx
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let msg = input.parse()?;
|
||||||
|
let plural = if input.parse::<Token![,]>().is_ok() {
|
||||||
|
input.parse().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let format_args = if input.parse::<Token![;]>().is_ok() {
|
||||||
|
syn::punctuated::Punctuated::parse_terminated(input).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(I18nCall {
|
||||||
|
catalog,
|
||||||
|
context,
|
||||||
|
msg,
|
||||||
|
plural,
|
||||||
|
format_args,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_str_lit(expr: &syn::Expr) -> Option<String> {
|
||||||
|
match *expr {
|
||||||
|
syn::Expr::Lit(syn::ExprLit { lit : syn::Lit::Str(ref s), attrs: _ }) => Some(s.value()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for I18nCall {
|
||||||
|
fn writable(&self) -> bool {
|
||||||
|
extract_str_lit(&self.msg).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(&self) -> String {
|
||||||
|
extract_str_lit(&self.msg).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context(&self) -> Option<String> {
|
||||||
|
self.context.as_ref().map(|c| c.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plural(&self) -> Option<String> {
|
||||||
|
self.plural.as_ref().and_then(extract_str_lit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TCall {
|
||||||
|
context: Option<syn::LitStr>,
|
||||||
|
msg: syn::LitStr,
|
||||||
|
plural: Option<syn::LitStr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl syn::parse::Parse for TCall {
|
||||||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let context = if input.parse::<kw::context>().is_ok() {
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
let ctx = input.parse().ok();
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
ctx
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = input.parse()?;
|
||||||
|
let plural = if input.parse::<Token![,]>().is_ok() {
|
||||||
|
input.parse().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(TCall {
|
||||||
|
context,
|
||||||
|
msg,
|
||||||
|
plural,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for TCall {
|
||||||
|
fn writable(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(&self) -> String {
|
||||||
|
self.msg.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context(&self) -> Option<String> {
|
||||||
|
self.context.as_ref().map(|c| c.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plural(&self) -> Option<String> {
|
||||||
|
self.plural.as_ref().map(|p| p.value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks a string as translatable
|
/// Marks a string as translatable
|
||||||
///
|
///
|
||||||
/// It only adds the given string to the `.pot` file, without translating it at runtime.
|
/// It only adds the given string to the `.pot` file, without translating it at runtime.
|
||||||
|
@ -357,26 +360,15 @@ msgstr ""
|
||||||
/// Where `$singular`, `$plural` and `$ctx` all are `str` literals (and not variables, expressions or literal of any other type).
|
/// Where `$singular`, `$plural` and `$ctx` all are `str` literals (and not variables, expressions or literal of any other type).
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn t(input: TokenStream) -> TokenStream {
|
pub fn t(input: TokenStream) -> TokenStream {
|
||||||
let span = input
|
let message = syn::parse_macro_input!(input as TCall);
|
||||||
.clone()
|
message.write();
|
||||||
.into_iter()
|
let msg = message.content();
|
||||||
.next()
|
|
||||||
.expect("Expected catalog")
|
|
||||||
.span();
|
|
||||||
let message = Message::parse(input.into_iter(), true);
|
|
||||||
message.write(
|
|
||||||
span.source_file()
|
|
||||||
.path()
|
|
||||||
.to_str()
|
|
||||||
.map(|p| (p.into(), span.start().line)),
|
|
||||||
);
|
|
||||||
let msg = message.content.clone();
|
|
||||||
if let Some(pl) = message.plural.clone() {
|
if let Some(pl) = message.plural.clone() {
|
||||||
quote!(
|
quote!(
|
||||||
($msg, $pl)
|
(#msg, #pl)
|
||||||
)
|
).into()
|
||||||
} else {
|
} else {
|
||||||
quote!($msg)
|
quote!(#msg).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,71 +452,45 @@ pub fn t(input: TokenStream) -> TokenStream {
|
||||||
/// you should add them at the end, after a colon, and seperate them with commas too.
|
/// you should add them at the end, after a colon, and seperate them with commas too.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn i18n(input: TokenStream) -> TokenStream {
|
pub fn i18n(input: TokenStream) -> TokenStream {
|
||||||
let span = input
|
let message = syn::parse_macro_input!(input as I18nCall);
|
||||||
.clone()
|
message.write();
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.expect("Expected catalog")
|
|
||||||
.span();
|
|
||||||
let mut input = input.into_iter();
|
|
||||||
let catalog = input
|
|
||||||
.clone()
|
|
||||||
.take_while(|t| !is(t, ','))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for _ in 0..(catalog.len() + 1) {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Message::parse(input, false);
|
let gettext_call = message.catalog.clone();
|
||||||
message.write(if Config::read().write_loc {
|
let content = message.msg;
|
||||||
span.source_file()
|
let gettext_call = if let Some(pl) = message.plural {
|
||||||
.path()
|
let count = message
|
||||||
.to_str()
|
|
||||||
.map(|p| (p.into(), span.start().line))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut gettext_call = TokenStream::from_iter(catalog);
|
|
||||||
let content = message.content;
|
|
||||||
if let Some(pl) = message.plural {
|
|
||||||
let count: TokenStream = message
|
|
||||||
.format_args
|
.format_args
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.and_then(|args| args.first().cloned());
|
||||||
.take_while(|x| match x {
|
|
||||||
TokenTree::Punct(p) if p.as_char() == ',' => false,
|
|
||||||
_ => true
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if let Some(c) = message.context {
|
if let Some(c) = message.context {
|
||||||
gettext_call.extend(quote!(
|
quote!(
|
||||||
.npgettext($c, $content, $pl, $count as u64)
|
#gettext_call.npgettext(#c, #content, #pl, #count as u64)
|
||||||
))
|
)
|
||||||
} else {
|
} else {
|
||||||
gettext_call.extend(quote!(
|
quote!(
|
||||||
.ngettext($content, $pl, $count as u64)
|
#gettext_call.ngettext(#content, #pl, #count as u64)
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(c) = message.context {
|
if let Some(c) = message.context {
|
||||||
gettext_call.extend(quote!(
|
quote!(
|
||||||
.pgettext($c, $content)
|
#gettext_call.pgettext(#c, #content)
|
||||||
))
|
)
|
||||||
} else {
|
} else {
|
||||||
gettext_call.extend(quote!(
|
quote!(
|
||||||
.gettext($content)
|
#gettext_call.gettext(#content)
|
||||||
))
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let fargs = message.format_args;
|
let fargs: syn::punctuated::Punctuated<proc_macro2::TokenStream, Token![,]> = message.format_args.unwrap_or_default().into_iter().map(|x| {
|
||||||
|
quote!(::std::boxed::Box::new(#x))
|
||||||
|
}).collect();
|
||||||
let res = quote!({
|
let res = quote!({
|
||||||
use runtime_fmt::*;
|
use gettext_utils::try_format;
|
||||||
rt_format!($gettext_call, $fargs).expect("Error while formatting message")
|
try_format(#gettext_call, &[#fargs]).expect("Error while formatting message")
|
||||||
});
|
});
|
||||||
// println!("{:#?}", res);
|
res.into()
|
||||||
res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This macro configures internationalization for the current crate
|
/// This macro configures internationalization for the current crate
|
||||||
|
@ -545,7 +511,7 @@ pub fn i18n(input: TokenStream) -> TokenStream {
|
||||||
/// With `.po` and `.mo` generation turned off, and without comments about string location in the `.pot`:
|
/// With `.po` and `.mo` generation turned off, and without comments about string location in the `.pot`:
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// init_i18n!("my_app", po = false, mo = false, location = false, de, en, eo, fr, ja, pl, ru);
|
/// init_i18n!("my_app", po = false, mo = false, de, en, eo, fr, ja, pl, ru);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Syntax
|
/// # Syntax
|
||||||
|
@ -565,6 +531,7 @@ pub fn i18n(input: TokenStream) -> TokenStream {
|
||||||
/// All the three boolean options are turned on by default. Also note that you may ommit one (or more) of them, but they should always be in this order.
|
/// All the three boolean options are turned on by default. Also note that you may ommit one (or more) of them, but they should always be in this order.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn init_i18n(input: TokenStream) -> TokenStream {
|
pub fn init_i18n(input: TokenStream) -> TokenStream {
|
||||||
|
let input = proc_macro2::TokenStream::from(input);
|
||||||
let mut input = input.into_iter();
|
let mut input = input.into_iter();
|
||||||
let domain = match input.next() {
|
let domain = match input.next() {
|
||||||
Some(TokenTree::Literal(lit)) => lit.to_string().replace("\"", ""),
|
Some(TokenTree::Literal(lit)) => lit.to_string().replace("\"", ""),
|
||||||
|
@ -572,7 +539,7 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
|
||||||
None => panic!("Expected a translation domain (for instance \"myapp\")"),
|
None => panic!("Expected a translation domain (for instance \"myapp\")"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (po, mo, location) = if let Some(n) = input.next() {
|
let (po, mo) = if let Some(n) = input.next() {
|
||||||
if is(&n, ',') {
|
if is(&n, ',') {
|
||||||
let po = named_arg(input.clone(), "po");
|
let po = named_arg(input.clone(), "po");
|
||||||
if let Some(po) = po.clone() {
|
if let Some(po) = po.clone() {
|
||||||
|
@ -588,19 +555,12 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = named_arg(input.clone(), "location");
|
(po, mo)
|
||||||
if let Some(location) = location.clone() {
|
|
||||||
for _ in 0..(location.into_iter().count() + 3) {
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(po, mo, location)
|
|
||||||
} else {
|
} else {
|
||||||
(None, None, None)
|
(None, None)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(None, None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut langs = vec![];
|
let mut langs = vec![];
|
||||||
|
@ -628,7 +588,6 @@ pub fn init_i18n(input: TokenStream) -> TokenStream {
|
||||||
domain: domain.clone(),
|
domain: domain.clone(),
|
||||||
make_po: po.map(|x| x.to_string() == "true").unwrap_or(true),
|
make_po: po.map(|x| x.to_string() == "true").unwrap_or(true),
|
||||||
make_mo: mo.map(|x| x.to_string() == "true").unwrap_or(true),
|
make_mo: mo.map(|x| x.to_string() == "true").unwrap_or(true),
|
||||||
write_loc: location.map(|x| x.to_string() == "true").unwrap_or(true),
|
|
||||||
langs,
|
langs,
|
||||||
};
|
};
|
||||||
conf.write();
|
conf.write();
|
||||||
|
@ -663,7 +622,7 @@ msgstr ""
|
||||||
)
|
)
|
||||||
.expect("Couldn't init .pot file");
|
.expect("Couldn't init .pot file");
|
||||||
|
|
||||||
quote!()
|
quote!().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gives you the translation domain for the current crate.
|
/// Gives you the translation domain for the current crate.
|
||||||
|
@ -681,7 +640,7 @@ msgstr ""
|
||||||
pub fn i18n_domain(_: TokenStream) -> TokenStream {
|
pub fn i18n_domain(_: TokenStream) -> TokenStream {
|
||||||
let domain = Config::read().domain;
|
let domain = Config::read().domain;
|
||||||
let tok = TokenTree::Literal(Literal::string(&domain));
|
let tok = TokenTree::Literal(Literal::string(&domain));
|
||||||
quote!($tok)
|
quote!(#tok).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compiles your internationalization files.
|
/// Compiles your internationalization files.
|
||||||
|
@ -792,7 +751,7 @@ pub fn compile_i18n(_: TokenStream) -> TokenStream {
|
||||||
.expect("Couldn't compile translations. Make sure msgfmt is installed");
|
.expect("Couldn't compile translations. Make sure msgfmt is installed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
quote!()
|
quote!().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use this macro to staticaly import translations into your final binary.
|
/// Use this macro to staticaly import translations into your final binary.
|
||||||
|
@ -821,17 +780,17 @@ pub fn include_i18n(_: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
let path = TokenTree::Literal(Literal::string(path.to_str().expect("Couldn't write MO file path")));
|
let path = TokenTree::Literal(Literal::string(path.to_str().expect("Couldn't write MO file path")));
|
||||||
quote!{
|
quote!{
|
||||||
($lang, ::gettext::Catalog::parse(
|
(#lang, ::gettext::Catalog::parse(
|
||||||
&include_bytes!(
|
&include_bytes!(
|
||||||
$path
|
#path
|
||||||
)[..]
|
)[..]
|
||||||
).expect("Error while loading catalog")),
|
).expect("Error while loading catalog")),
|
||||||
}
|
}
|
||||||
}).collect::<TokenStream>();
|
}).collect::<proc_macro2::TokenStream>();
|
||||||
|
|
||||||
quote!({
|
quote!({
|
||||||
vec![
|
vec![
|
||||||
$locales
|
#locales
|
||||||
]
|
]
|
||||||
})
|
}).into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
|
|
||||||
use gettext_macros::*;
|
use gettext_macros::*;
|
||||||
|
|
||||||
init_i18n!("test", fr, en, de, ja);
|
init_i18n!("test", fr, en, de, ja);
|
||||||
|
|
Loading…
Reference in a new issue