You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
797 lines
24 KiB
Rust
797 lines
24 KiB
Rust
//! A set of macros to make i18n easier.
|
|
|
|
extern crate proc_macro;
|
|
use proc_macro::TokenStream;
|
|
use proc_macro2::{
|
|
token_stream::IntoIter as TokenIter, Literal, TokenTree,
|
|
};
|
|
use quote::quote;
|
|
use std::{
|
|
env,
|
|
fs::{create_dir_all, read, File, OpenOptions},
|
|
io::{BufRead, Read, Seek, SeekFrom, Write},
|
|
path::Path,
|
|
process::{Command, Stdio},
|
|
};
|
|
use syn::Token;
|
|
|
|
fn is(t: &TokenTree, ch: char) -> bool {
|
|
match t {
|
|
TokenTree::Punct(p) => p.as_char() == ch,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn named_arg(mut input: TokenIter, name: &'static str) -> Option<proc_macro2::TokenStream> {
|
|
input.next().and_then(|t| match t {
|
|
TokenTree::Ident(ref i) if i.to_string() == name => {
|
|
input.next(); // skip "="
|
|
Some(
|
|
input
|
|
.take_while(|tok| match tok {
|
|
TokenTree::Punct(_) => false,
|
|
_ => true,
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
fn root_crate_path() -> std::path::PathBuf {
|
|
let path = env::var("CARGO_MANIFEST_DIR")
|
|
.expect("CARGO_MANIFEST_DIR is not set. Please use cargo to compile your crate.");
|
|
let path = Path::new(&path);
|
|
if path
|
|
.parent()
|
|
.expect("No parent dir")
|
|
.join("Cargo.toml")
|
|
.exists()
|
|
{
|
|
path.parent().expect("No parent dir").to_path_buf()
|
|
} else {
|
|
path.to_path_buf()
|
|
}
|
|
}
|
|
|
|
struct Config {
|
|
domain: String,
|
|
make_po: bool,
|
|
make_mo: bool,
|
|
langs: Vec<String>,
|
|
}
|
|
|
|
impl Config {
|
|
fn path() -> std::path::PathBuf {
|
|
Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| {
|
|
root_crate_path()
|
|
.join("target")
|
|
.join("debug")
|
|
.to_str()
|
|
.expect("Couldn't compute mo output dir")
|
|
.into()
|
|
}))
|
|
.join("gettext_macros")
|
|
.join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))
|
|
}
|
|
|
|
fn read() -> Config {
|
|
let config = read(Config::path())
|
|
.expect("Coudln't read domain, make sure to call init_i18n! before");
|
|
let mut lines = config.lines();
|
|
let domain = lines
|
|
.next()
|
|
.expect("Invalid config file. Make sure to call init_i18n! before this macro")
|
|
.expect("IO error while reading config");
|
|
let make_po: 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 make_po");
|
|
let make_mo: 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 make_mo");
|
|
Config {
|
|
domain,
|
|
make_po,
|
|
make_mo,
|
|
langs: lines
|
|
.map(|l| l.expect("IO error while reading config"))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn write(&self) {
|
|
// emit file to include
|
|
create_dir_all(Config::path().parent().unwrap()).expect("Couldn't create output dir");
|
|
let mut out = File::create(Config::path()).expect("Metadata file couldn't be open");
|
|
writeln!(out, "{}", self.domain).expect("Couldn't write domain");
|
|
writeln!(out, "{}", self.make_po).expect("Couldn't write po settings");
|
|
writeln!(out, "{}", self.make_mo).expect("Couldn't write mo settings");
|
|
for l in self.langs.clone() {
|
|
writeln!(out, "{}", l).expect("Couldn't write lang");
|
|
}
|
|
}
|
|
}
|
|
|
|
trait Message {
|
|
fn writable(&self) -> bool;
|
|
fn content(&self) -> String;
|
|
fn context(&self) -> Option<String>;
|
|
fn plural(&self) -> Option<String>;
|
|
|
|
fn write(&self) {
|
|
if !self.writable() {
|
|
return;
|
|
}
|
|
|
|
let config = Config::read();
|
|
|
|
let mut pot = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.open(format!("po/{0}/{0}.pot", config.domain))
|
|
.expect("Couldn't open .pot file");
|
|
|
|
let mut contents = String::new();
|
|
pot.read_to_string(&mut contents)
|
|
.expect("IO error while reading .pot file");
|
|
pot.seek(SeekFrom::End(0))
|
|
.expect("IO error while seeking .pot file to end");
|
|
|
|
let already_exists = self.content().is_empty()
|
|
|| contents.contains(&format!(
|
|
r#"{}msgid "{}""#,
|
|
self.context()
|
|
.clone()
|
|
.map(|c| format!(
|
|
r#"msgctxt "{}"
|
|
"#,
|
|
c))
|
|
.unwrap_or_default(),
|
|
self.content()
|
|
));
|
|
if already_exists {
|
|
return;
|
|
}
|
|
|
|
let prefix = if let Some(c) = self.context() {
|
|
format!(
|
|
r#"msgctxt "{}"
|
|
"#, c)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
if let Some(ref pl) = self.plural() {
|
|
pot.write_all(
|
|
&format!(
|
|
r#"
|
|
{}msgid "{}"
|
|
msgid_plural "{}"
|
|
msgstr[0] ""
|
|
"#,
|
|
prefix, self.content(), pl,
|
|
)
|
|
.into_bytes(),
|
|
)
|
|
.expect("Couldn't write message to .pot (plural)");
|
|
} else {
|
|
pot.write_all(
|
|
&format!(
|
|
r#"
|
|
{}msgid "{}"
|
|
msgstr ""
|
|
"#,
|
|
prefix, self.content(),
|
|
)
|
|
.into_bytes(),
|
|
)
|
|
.expect("Couldn't write message to .pot");
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
///
|
|
/// It only adds the given string to the `.pot` file, without translating it at runtime.
|
|
///
|
|
/// To translate it for real, you will have to use `i18n`. The advantage of this macro, is
|
|
/// that you mark a string as translatable without requiring a catalog to be available in scope.
|
|
///
|
|
/// # Return value
|
|
///
|
|
/// In case of a singular message, the message itself is returned.
|
|
///
|
|
/// For messages with a plural form, it is a tuple containing the singular form, and the plural one.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// #use gettext_macros::*;
|
|
/// // Let's say we can't have access to a Catalog at this point of the program
|
|
/// let msg = t!("Hello, world!");
|
|
/// let plural = t!("Singular", "Plural")
|
|
///
|
|
/// // Now, let's get a catalog, and translate these messages
|
|
/// let cat = get_catalog();
|
|
/// i18n!(cat, msg);
|
|
/// i18n!(cat, plural.0, plural.1; 57);
|
|
/// ```
|
|
///
|
|
/// # Syntax
|
|
///
|
|
/// This macro accepts the following syntaxes:
|
|
///
|
|
/// ```rust,ignore
|
|
/// t!($singular)
|
|
/// t!($singular, $plural)
|
|
/// t!(context = $ctx, $singular)
|
|
/// t!(context = $ctx, $singular, $plural)
|
|
/// ```
|
|
///
|
|
/// Where `$singular`, `$plural` and `$ctx` all are `str` literals (and not variables, expressions or literal of any other type).
|
|
#[proc_macro]
|
|
pub fn t(input: TokenStream) -> TokenStream {
|
|
let message = syn::parse_macro_input!(input as TCall);
|
|
message.write();
|
|
let msg = message.content();
|
|
if let Some(pl) = message.plural.clone() {
|
|
quote!(
|
|
(#msg, #pl)
|
|
).into()
|
|
} else {
|
|
quote!(#msg).into()
|
|
}
|
|
}
|
|
|
|
/// Marks a string as translatable and translate it at runtime.
|
|
///
|
|
/// It add the string to the `.pot` file and translate them at runtime, using a given `gettext::Catalog`.
|
|
///
|
|
/// # Return value
|
|
///
|
|
/// This macro returns the translated string.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This macro will panic if it the format string (of the translation) does not match the
|
|
/// format arguments that were given. For instance, if you have a string `Hello!`, that
|
|
/// is translated in Esperanto as `Saluton {name}!`, and that you call this function without
|
|
/// any format argument (as expected in the original English string), it will panic.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Basic usage:
|
|
///
|
|
/// ```rust,ignore
|
|
/// // cat is the gettext::Catalog containing translations for the current locale.
|
|
/// let cat = get_catalog();
|
|
/// i18n!(cat, "Hello, world!");
|
|
/// ```
|
|
///
|
|
/// Formatting a translated string:
|
|
///
|
|
/// ```rust,ignore
|
|
/// let name = "Peter";
|
|
/// i18n!(cat, "Hi {0}!"; name);
|
|
///
|
|
/// // Also works with multiple format arguments
|
|
/// i18n!(cat, "You are our {}th visitor! You won ${}!"; 99_999, 2);
|
|
/// ```
|
|
///
|
|
/// With a context, that will be shown to translators:
|
|
///
|
|
/// ```rust,ignore
|
|
/// let name = "Sophia";
|
|
/// i18n!(cat, context = "The variable is the name of the person being greeted", "Hello, {0}!"; name);
|
|
/// ```
|
|
///
|
|
/// Translating string that changes depending on a number:
|
|
///
|
|
/// ```rust,ignore
|
|
/// let flowers_count = 18;
|
|
/// i18n!(cat, "What a nice flower!", "What a nice garden!"; flowers_count);
|
|
/// ```
|
|
///
|
|
/// With all available options:
|
|
///
|
|
/// ```rust,ignore
|
|
/// let updates = 69;
|
|
/// i18n!(
|
|
/// cat,
|
|
/// context = "The notification when updates are available.",
|
|
/// "There is {} app update available."
|
|
/// "There are {} app updates available.";
|
|
/// updates
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// # Syntax
|
|
///
|
|
/// This macro expects:
|
|
///
|
|
/// - first, the expression to get the translation catalog to use
|
|
/// - then, optionally, the `context` named argument, that is a string that will be shown
|
|
/// to translators. It should be a `str` literal, because it needs to be known at compile time.
|
|
/// - the message to translate. It can either be a string literal, or an expression, but if you use the later
|
|
/// make sure that the string is correctly added to the `.pot` file with `t`.
|
|
/// - if this message has a plural version, it should come after. Here too, both string literals or other expressions
|
|
/// are allowed
|
|
///
|
|
/// All these arguments should be separated by commas.
|
|
///
|
|
/// If you want to pass format arguments to this macro, to have them inserted into the translated strings,
|
|
/// you should add them at the end, after a colon, and seperate them with commas too.
|
|
#[proc_macro]
|
|
pub fn i18n(input: TokenStream) -> TokenStream {
|
|
let message = syn::parse_macro_input!(input as I18nCall);
|
|
message.write();
|
|
|
|
let gettext_call = message.catalog.clone();
|
|
let content = message.msg;
|
|
let gettext_call = if let Some(pl) = message.plural {
|
|
let count = message
|
|
.format_args
|
|
.clone()
|
|
.and_then(|args| args.first().cloned());
|
|
if let Some(c) = message.context {
|
|
quote!(
|
|
#gettext_call.npgettext(#c, #content, #pl, #count as u64)
|
|
)
|
|
} else {
|
|
quote!(
|
|
#gettext_call.ngettext(#content, #pl, #count as u64)
|
|
)
|
|
}
|
|
} else {
|
|
if let Some(c) = message.context {
|
|
quote!(
|
|
#gettext_call.pgettext(#c, #content)
|
|
)
|
|
} else {
|
|
quote!(
|
|
#gettext_call.gettext(#content)
|
|
)
|
|
}
|
|
};
|
|
|
|
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!({
|
|
use gettext_utils::try_format;
|
|
try_format(#gettext_call, &[#fargs]).expect("Error while formatting message")
|
|
});
|
|
res.into()
|
|
}
|
|
|
|
/// This macro configures internationalization for the current crate
|
|
///
|
|
/// This macro expands to nothing, it just write your configuration to files
|
|
/// for other macros calls, and creates the `.pot` file if needed.
|
|
///
|
|
/// This macro should be called before (not in the program flow, but in the Rust parser flow) all other
|
|
/// internationalization macros.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Basic usage:
|
|
///
|
|
/// ```rust,ignore
|
|
/// init_i18n!("my_app", de, en, eo, fr, ja, pl, ru);
|
|
/// ```
|
|
/// With `.po` and `.mo` generation turned off, and without comments about string location in the `.pot`:
|
|
///
|
|
/// ```rust,ignore
|
|
/// init_i18n!("my_app", po = false, mo = false, de, en, eo, fr, ja, pl, ru);
|
|
/// ```
|
|
///
|
|
/// # Syntax
|
|
///
|
|
/// This macro expects:
|
|
///
|
|
/// - a string literal, that is the translation domain of your crate.
|
|
/// - optionally, the `po` named argument, that is a boolean literal to turn off `.po` generation from `.pot` in `compile_i18n`
|
|
/// - optionally, the `mo` named argument, that is a boolean literal too, to turn of `.po` compilation into `.mo` files in `compile_i18n`.
|
|
/// Note that if you turn this feature off, `include_i18n` won't work unless you manually generate the `.mo` files in
|
|
/// `target/TARGET/gettext_macros/LOCALE/DOMAIN.mo`.
|
|
/// - optionally, the `location` named argument, a boolean too, to avoid writing the location of the string in the source code to translation files.
|
|
/// Having this location available can be usefull if your translators know a bit of Rust and needs context about what they are translating, but it
|
|
/// also makes bigger diffs, because your `.pot` and `.po` files may be regenerated if a line number changes.
|
|
/// - then, the list of languages you want your app to be translated in, separated by commas. The languages are not string literals, but identifiers.
|
|
///
|
|
/// 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]
|
|
pub fn init_i18n(input: TokenStream) -> TokenStream {
|
|
let input = proc_macro2::TokenStream::from(input);
|
|
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 (po, mo) = if let Some(n) = input.next() {
|
|
if is(&n, ',') {
|
|
let po = named_arg(input.clone(), "po");
|
|
if let Some(po) = po.clone() {
|
|
for _ in 0..(po.into_iter().count() + 3) {
|
|
input.next();
|
|
}
|
|
}
|
|
|
|
let mo = named_arg(input.clone(), "mo");
|
|
if let Some(mo) = mo.clone() {
|
|
for _ in 0..(mo.into_iter().count() + 3) {
|
|
input.next();
|
|
}
|
|
}
|
|
|
|
(po, mo)
|
|
} else {
|
|
(None, None)
|
|
}
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
let mut langs = vec![];
|
|
match input.next() {
|
|
Some(TokenTree::Ident(i)) => {
|
|
langs.push(i.to_string());
|
|
loop {
|
|
let next = input.next();
|
|
if next.is_none() || !is(&next.expect("Unreachable: next should be Some"), ',') {
|
|
break;
|
|
}
|
|
match input.next() {
|
|
Some(TokenTree::Ident(i)) => {
|
|
langs.push(i.to_string());
|
|
}
|
|
_ => panic!("Expected a language identifier"),
|
|
}
|
|
}
|
|
}
|
|
None => {}
|
|
_ => panic!("Expected a language identifier"),
|
|
}
|
|
|
|
let conf = Config {
|
|
domain: domain.clone(),
|
|
make_po: po.map(|x| x.to_string() == "true").unwrap_or(true),
|
|
make_mo: mo.map(|x| x.to_string() == "true").unwrap_or(true),
|
|
langs,
|
|
};
|
|
conf.write();
|
|
|
|
// write base .pot
|
|
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"
|
|
"Report-Msgid-Bugs-To: \n"
|
|
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
"Language: \n"
|
|
"MIME-Version: 1.0\n"
|
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
"Content-Transfer-Encoding: 8bit\n"
|
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
|
"#,
|
|
domain
|
|
)
|
|
.into_bytes(),
|
|
)
|
|
.expect("Couldn't init .pot file");
|
|
|
|
quote!().into()
|
|
}
|
|
|
|
/// Gives you the translation domain for the current crate.
|
|
///
|
|
/// # Return value
|
|
///
|
|
/// A `'static str` containing the GetText domain of this crate.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// println!("The GetText domain is: {}", i18n_domain!());
|
|
/// ```
|
|
#[proc_macro]
|
|
pub fn i18n_domain(_: TokenStream) -> TokenStream {
|
|
let domain = Config::read().domain;
|
|
let tok = TokenTree::Literal(Literal::string(&domain));
|
|
quote!(#tok).into()
|
|
}
|
|
|
|
/// Compiles your internationalization files.
|
|
///
|
|
/// This macro expands to nothing, it just writes `.po` and `.mo` files.
|
|
///
|
|
/// You can configure its behavior with the `po` and `mo` options of `init_i18n`.
|
|
///
|
|
/// This macro should be called after (not in the program flow, but in the Rust parser flow) all other internationlaziton macros,
|
|
/// expected `include_i18n`.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// compile_i18n!();
|
|
/// ```
|
|
#[proc_macro]
|
|
pub fn compile_i18n(_: TokenStream) -> TokenStream {
|
|
let conf = Config::read();
|
|
let domain = &conf.domain;
|
|
|
|
let pot_path = root_crate_path()
|
|
.join("po")
|
|
.join(domain.clone())
|
|
.join(format!("{}.pot", domain));
|
|
|
|
for lang in conf.langs {
|
|
let po_path = root_crate_path()
|
|
.join("po")
|
|
.join(domain.clone())
|
|
.join(format!("{}.po", lang.clone()));
|
|
if conf.make_po {
|
|
if po_path.exists() && po_path.is_file() {
|
|
// Update it
|
|
Command::new("msgmerge")
|
|
.arg("-U")
|
|
.arg(po_path.to_str().expect("msgmerge: PO path error"))
|
|
.arg(pot_path.to_str().expect("msgmerge: POT path error"))
|
|
.stdout(Stdio::null())
|
|
.status()
|
|
.map(|s| {
|
|
if !s.success() {
|
|
panic!("Couldn't update PO file")
|
|
}
|
|
})
|
|
.expect("Couldn't update PO file. Make sure msgmerge is installed.");
|
|
} else {
|
|
println!("Creating {}", lang.clone());
|
|
// Create it from the template
|
|
Command::new("msginit")
|
|
.arg(format!(
|
|
"--input={}",
|
|
pot_path.to_str().expect("msginit: POT path error")
|
|
))
|
|
.arg(format!(
|
|
"--output-file={}",
|
|
po_path.to_str().expect("msginit: PO path error")
|
|
))
|
|
.arg("-l")
|
|
.arg(lang.clone())
|
|
.arg("--no-translator")
|
|
.stdout(Stdio::null())
|
|
.status()
|
|
.map(|s| {
|
|
if !s.success() {
|
|
panic!("Couldn't init PO file (gettext returned an error)")
|
|
}
|
|
})
|
|
.expect("Couldn't init PO file. Make sure msginit is installed.");
|
|
}
|
|
}
|
|
|
|
if conf.make_mo {
|
|
if !po_path.exists() {
|
|
panic!(
|
|
"{} doesn't exist. Make sure you didn't disabled po generation.",
|
|
po_path.display()
|
|
);
|
|
}
|
|
|
|
// Generate .mo
|
|
let mo_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| {
|
|
root_crate_path()
|
|
.join("target")
|
|
.join("debug")
|
|
.to_str()
|
|
.expect("Couldn't compute mo output dir")
|
|
.into()
|
|
}))
|
|
.join("gettext_macros")
|
|
.join(lang);
|
|
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().expect("msgfmt: MO path error")
|
|
))
|
|
.arg(po_path)
|
|
.stdout(Stdio::null())
|
|
.status()
|
|
.map(|s| {
|
|
if !s.success() {
|
|
panic!("Couldn't compile translations (gettext returned an error)")
|
|
}
|
|
})
|
|
.expect("Couldn't compile translations. Make sure msgfmt is installed");
|
|
}
|
|
}
|
|
quote!().into()
|
|
}
|
|
|
|
/// Use this macro to staticaly import translations into your final binary.
|
|
///
|
|
/// This macro won't work if ou set `mo = false` in `init_i18n`, unless you manually generate the `.mo` files in
|
|
/// `target/TARGET/gettext_macros/LOCALE/DOMAIN.mo`.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// let catalogs = include_i18n!();
|
|
/// catalog.into_iter()
|
|
/// .find(|(lang, _)| lang == "eo")
|
|
/// .map(|(_, catalog| println!("{}", i18n!(catalog, "Hello world!")));
|
|
/// ```
|
|
#[proc_macro]
|
|
pub fn include_i18n(_: TokenStream) -> TokenStream {
|
|
let conf = Config::read();
|
|
let locales = conf.langs.clone().into_iter().map(|l| {
|
|
let lang = TokenTree::Literal(Literal::string(&l));
|
|
let path = Config::path().parent().unwrap().join(l).join(format!("{}.mo", conf.domain));
|
|
|
|
if !path.exists() {
|
|
panic!("{} doesn't exist. Make sure to call compile_i18n! before include_i18n!, and check that you didn't disabled mo compilation.", path.display());
|
|
}
|
|
|
|
let path = TokenTree::Literal(Literal::string(path.to_str().expect("Couldn't write MO file path")));
|
|
quote!{
|
|
(#lang, ::gettext::Catalog::parse(
|
|
&include_bytes!(
|
|
#path
|
|
)[..]
|
|
).expect("Error while loading catalog")),
|
|
}
|
|
}).collect::<proc_macro2::TokenStream>();
|
|
|
|
quote!({
|
|
vec![
|
|
#locales
|
|
]
|
|
}).into()
|
|
}
|