1extern crate proc_macro;
4use proc_macro::TokenStream;
5use proc_macro2::{
6 token_stream::IntoIter as TokenIter, Literal, TokenTree,
7};
8use quote::quote;
9use std::{
10 env,
11 fs::{create_dir_all, read, File, OpenOptions},
12 io::{BufRead, Read, Seek, SeekFrom, Write},
13 path::Path,
14 process::{Command, Stdio},
15};
16use syn::Token;
17
18fn is(t: &TokenTree, ch: char) -> bool {
19 match t {
20 TokenTree::Punct(p) => p.as_char() == ch,
21 _ => false,
22 }
23}
24
25fn named_arg(mut input: TokenIter, name: &'static str) -> Option<proc_macro2::TokenStream> {
26 input.next().and_then(|t| match t {
27 TokenTree::Ident(ref i) if i.to_string() == name => {
28 input.next(); Some(
30 input
31 .take_while(|tok| match tok {
32 TokenTree::Punct(_) => false,
33 _ => true,
34 })
35 .collect(),
36 )
37 }
38 _ => None,
39 })
40}
41
42fn root_crate_path() -> std::path::PathBuf {
43 let path = env::var("CARGO_MANIFEST_DIR")
44 .expect("CARGO_MANIFEST_DIR is not set. Please use cargo to compile your crate.");
45 let path = Path::new(&path);
46 if path
47 .parent()
48 .expect("No parent dir")
49 .join("Cargo.toml")
50 .exists()
51 {
52 path.parent().expect("No parent dir").to_path_buf()
53 } else {
54 path.to_path_buf()
55 }
56}
57
58struct Config {
59 domain: String,
60 make_po: bool,
61 make_mo: bool,
62 langs: Vec<String>,
63}
64
65impl Config {
66 fn path() -> std::path::PathBuf {
67 Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| {
68 root_crate_path()
69 .join("target")
70 .join("debug")
71 .to_str()
72 .expect("Couldn't compute mo output dir")
73 .into()
74 }))
75 .join("gettext_macros")
76 .join(env::var("CARGO_PKG_NAME").expect("Please build with cargo"))
77 }
78
79 fn read() -> Config {
80 let config = read(Config::path())
81 .expect("Coudln't read domain, make sure to call init_i18n! before");
82 let mut lines = config.lines();
83 let domain = lines
84 .next()
85 .expect("Invalid config file. Make sure to call init_i18n! before this macro")
86 .expect("IO error while reading config");
87 let make_po: bool = lines
88 .next()
89 .expect("Invalid config file. Make sure to call init_i18n! before this macro")
90 .expect("IO error while reading config")
91 .parse()
92 .expect("Couldn't parse make_po");
93 let make_mo: bool = lines
94 .next()
95 .expect("Invalid config file. Make sure to call init_i18n! before this macro")
96 .expect("IO error while reading config")
97 .parse()
98 .expect("Couldn't parse make_mo");
99 Config {
100 domain,
101 make_po,
102 make_mo,
103 langs: lines
104 .map(|l| l.expect("IO error while reading config"))
105 .collect(),
106 }
107 }
108
109 fn write(&self) {
110 create_dir_all(Config::path().parent().unwrap()).expect("Couldn't create output dir");
112 let mut out = File::create(Config::path()).expect("Metadata file couldn't be open");
113 writeln!(out, "{}", self.domain).expect("Couldn't write domain");
114 writeln!(out, "{}", self.make_po).expect("Couldn't write po settings");
115 writeln!(out, "{}", self.make_mo).expect("Couldn't write mo settings");
116 for l in self.langs.clone() {
117 writeln!(out, "{}", l).expect("Couldn't write lang");
118 }
119 }
120}
121
122trait Message {
123 fn writable(&self) -> bool;
124 fn content(&self) -> String;
125 fn context(&self) -> Option<String>;
126 fn plural(&self) -> Option<String>;
127
128 fn write(&self) {
129 if !self.writable() {
130 return;
131 }
132
133 let config = Config::read();
134
135 let mut pot = OpenOptions::new()
136 .read(true)
137 .write(true)
138 .create(true)
139 .open(format!("po/{0}/{0}.pot", config.domain))
140 .expect("Couldn't open .pot file");
141
142 let mut contents = String::new();
143 pot.read_to_string(&mut contents)
144 .expect("IO error while reading .pot file");
145 pot.seek(SeekFrom::End(0))
146 .expect("IO error while seeking .pot file to end");
147
148 let already_exists = self.content().is_empty()
149 || contents.contains(&format!(
150 r#"{}msgid "{}""#,
151 self.context()
152 .clone()
153 .map(|c| format!(
154r#"msgctxt "{}"
155"#,
156 c))
157 .unwrap_or_default(),
158 self.content()
159 ));
160 if already_exists {
161 return;
162 }
163
164 let prefix = if let Some(c) = self.context() {
165 format!(
166r#"msgctxt "{}"
167"#, c)
168 } else {
169 String::new()
170 };
171
172 if let Some(ref pl) = self.plural() {
173 pot.write_all(
174 &format!(
175 r#"
176{}msgid "{}"
177msgid_plural "{}"
178msgstr[0] ""
179"#,
180 prefix, self.content(), pl,
181 )
182 .into_bytes(),
183 )
184 .expect("Couldn't write message to .pot (plural)");
185 } else {
186 pot.write_all(
187 &format!(
188 r#"
189{}msgid "{}"
190msgstr ""
191"#,
192 prefix, self.content(),
193 )
194 .into_bytes(),
195 )
196 .expect("Couldn't write message to .pot");
197 }
198 }
199}
200
201struct I18nCall {
202 catalog: syn::Expr,
203 context: Option<syn::LitStr>,
204 msg: syn::Expr,
205 plural: Option<syn::Expr>,
206 format_args: Option<syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>>,
207}
208
209mod kw {
210 syn::custom_keyword!(context);
211}
212
213impl syn::parse::Parse for I18nCall {
214 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
215 let catalog = input.parse()?;
216 input.parse::<Token![,]>()?;
217 let context = if input.parse::<kw::context>().is_ok() {
218 input.parse::<Token![=]>()?;
219 let ctx = input.parse().ok();
220 input.parse::<Token![,]>()?;
221 ctx
222 } else {
223 None
224 };
225 let msg = input.parse()?;
226 let plural = if input.parse::<Token![,]>().is_ok() {
227 input.parse().ok()
228 } else {
229 None
230 };
231 let format_args = if input.parse::<Token![;]>().is_ok() {
232 syn::punctuated::Punctuated::parse_terminated(input).ok()
233 } else {
234 None
235 };
236
237 Ok(I18nCall {
238 catalog,
239 context,
240 msg,
241 plural,
242 format_args,
243 })
244 }
245}
246
247fn extract_str_lit(expr: &syn::Expr) -> Option<String> {
248 match *expr {
249 syn::Expr::Lit(syn::ExprLit { lit : syn::Lit::Str(ref s), attrs: _ }) => Some(s.value()),
250 _ => None,
251 }
252}
253
254impl Message for I18nCall {
255 fn writable(&self) -> bool {
256 extract_str_lit(&self.msg).is_some()
257 }
258
259 fn content(&self) -> String {
260 extract_str_lit(&self.msg).unwrap_or_default().replace("\"", "\\\"").replace('\n', "\\n")
261 }
262
263 fn context(&self) -> Option<String> {
264 self.context.as_ref().map(|c| c.value())
265 }
266
267 fn plural(&self) -> Option<String> {
268 self.plural.as_ref().and_then(extract_str_lit)
269 }
270}
271
272struct TCall {
273 context: Option<syn::LitStr>,
274 msg: syn::LitStr,
275 plural: Option<syn::LitStr>,
276}
277
278impl syn::parse::Parse for TCall {
279 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
280 let context = if input.parse::<kw::context>().is_ok() {
281 input.parse::<Token![=]>()?;
282 let ctx = input.parse().ok();
283 input.parse::<Token![,]>()?;
284 ctx
285 } else {
286 None
287 };
288
289 let msg = input.parse()?;
290 let plural = if input.parse::<Token![,]>().is_ok() {
291 input.parse().ok()
292 } else {
293 None
294 };
295
296 Ok(TCall {
297 context,
298 msg,
299 plural,
300 })
301 }
302}
303
304impl Message for TCall {
305 fn writable(&self) -> bool {
306 true
307 }
308
309 fn content(&self) -> String {
310 self.msg.value().replace("\"", "\\\"").replace('\n', "\\n")
311 }
312
313 fn context(&self) -> Option<String> {
314 self.context.as_ref().map(|c| c.value())
315 }
316
317 fn plural(&self) -> Option<String> {
318 self.plural.as_ref().map(|p| p.value())
319 }
320}
321
322#[proc_macro]
362pub fn t(input: TokenStream) -> TokenStream {
363 let message = syn::parse_macro_input!(input as TCall);
364 message.write();
365 let msg = message.content();
366 if let Some(pl) = message.plural.clone() {
367 quote!(
368 (#msg, #pl)
369 ).into()
370 } else {
371 quote!(#msg).into()
372 }
373}
374
375#[proc_macro]
454pub fn i18n(input: TokenStream) -> TokenStream {
455 let message = syn::parse_macro_input!(input as I18nCall);
456 message.write();
457
458 let gettext_call = message.catalog.clone();
459 let content = message.msg;
460 let gettext_call = if let Some(pl) = message.plural {
461 let count = message
462 .format_args
463 .clone()
464 .and_then(|args| args.first().cloned());
465 if let Some(c) = message.context {
466 quote!(
467 #gettext_call.npgettext(#c, #content, #pl, #count as u64)
468 )
469 } else {
470 quote!(
471 #gettext_call.ngettext(#content, #pl, #count as u64)
472 )
473 }
474 } else {
475 if let Some(c) = message.context {
476 quote!(
477 #gettext_call.pgettext(#c, #content)
478 )
479 } else {
480 quote!(
481 #gettext_call.gettext(#content)
482 )
483 }
484 };
485
486 let fargs: syn::punctuated::Punctuated<proc_macro2::TokenStream, Token![,]> = message.format_args.unwrap_or_default().into_iter().map(|x| {
487 quote!(::std::boxed::Box::new(#x))
488 }).collect();
489 let res = quote!({
490 use gettext_utils::try_format;
491 try_format(#gettext_call, &[#fargs]).expect("Error while formatting message")
492 });
493 res.into()
494}
495
496#[proc_macro]
533pub fn init_i18n(input: TokenStream) -> TokenStream {
534 let input = proc_macro2::TokenStream::from(input);
535 let mut input = input.into_iter();
536 let domain = match input.next() {
537 Some(TokenTree::Literal(lit)) => lit.to_string().replace("\"", ""),
538 Some(_) => panic!("Domain should be a str"),
539 None => panic!("Expected a translation domain (for instance \"myapp\")"),
540 };
541
542 let (po, mo) = if let Some(n) = input.next() {
543 if is(&n, ',') {
544 let po = named_arg(input.clone(), "po");
545 if let Some(po) = po.clone() {
546 for _ in 0..(po.into_iter().count() + 3) {
547 input.next();
548 }
549 }
550
551 let mo = named_arg(input.clone(), "mo");
552 if let Some(mo) = mo.clone() {
553 for _ in 0..(mo.into_iter().count() + 3) {
554 input.next();
555 }
556 }
557
558 (po, mo)
559 } else {
560 (None, None)
561 }
562 } else {
563 (None, None)
564 };
565
566 let mut langs = vec![];
567 match input.next() {
568 Some(TokenTree::Ident(i)) => {
569 langs.push(i.to_string());
570 loop {
571 let next = input.next();
572 if next.is_none() || !is(&next.expect("Unreachable: next should be Some"), ',') {
573 break;
574 }
575 match input.next() {
576 Some(TokenTree::Ident(i)) => {
577 langs.push(i.to_string());
578 }
579 _ => panic!("Expected a language identifier"),
580 }
581 }
582 }
583 None => {}
584 _ => panic!("Expected a language identifier"),
585 }
586
587 let conf = Config {
588 domain: domain.clone(),
589 make_po: po.map(|x| x.to_string() == "true").unwrap_or(true),
590 make_mo: mo.map(|x| x.to_string() == "true").unwrap_or(true),
591 langs,
592 };
593 conf.write();
594
595 create_dir_all(format!("po/{}", domain)).expect("Couldn't create po dir");
597 let mut pot = OpenOptions::new()
598 .write(true)
599 .create(true)
600 .truncate(true)
601 .open(format!("po/{0}/{0}.pot", domain))
602 .expect("Couldn't open .pot file");
603 pot.write_all(
604 &format!(
605 r#"msgid ""
606msgstr ""
607"Project-Id-Version: {}\n"
608"Report-Msgid-Bugs-To: \n"
609"POT-Creation-Date: 2018-06-15 16:33-0700\n"
610"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
611"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
612"Language-Team: LANGUAGE <LL@li.org>\n"
613"Language: \n"
614"MIME-Version: 1.0\n"
615"Content-Type: text/plain; charset=UTF-8\n"
616"Content-Transfer-Encoding: 8bit\n"
617"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
618"#,
619 domain
620 )
621 .into_bytes(),
622 )
623 .expect("Couldn't init .pot file");
624
625 quote!().into()
626}
627
628#[proc_macro]
640pub fn i18n_domain(_: TokenStream) -> TokenStream {
641 let domain = Config::read().domain;
642 let tok = TokenTree::Literal(Literal::string(&domain));
643 quote!(#tok).into()
644}
645
646#[proc_macro]
661pub fn compile_i18n(_: TokenStream) -> TokenStream {
662 let conf = Config::read();
663 let domain = &conf.domain;
664
665 let pot_path = root_crate_path()
666 .join("po")
667 .join(domain.clone())
668 .join(format!("{}.pot", domain));
669
670 for lang in conf.langs {
671 let po_path = root_crate_path()
672 .join("po")
673 .join(domain.clone())
674 .join(format!("{}.po", lang.clone()));
675 if conf.make_po {
676 if po_path.exists() && po_path.is_file() {
677 Command::new("msgmerge")
679 .arg("-U")
680 .arg(po_path.to_str().expect("msgmerge: PO path error"))
681 .arg(pot_path.to_str().expect("msgmerge: POT path error"))
682 .stdout(Stdio::null())
683 .status()
684 .map(|s| {
685 if !s.success() {
686 panic!("Couldn't update PO file")
687 }
688 })
689 .expect("Couldn't update PO file. Make sure msgmerge is installed.");
690 } else {
691 println!("Creating {}", lang.clone());
692 Command::new("msginit")
694 .arg(format!(
695 "--input={}",
696 pot_path.to_str().expect("msginit: POT path error")
697 ))
698 .arg(format!(
699 "--output-file={}",
700 po_path.to_str().expect("msginit: PO path error")
701 ))
702 .arg("-l")
703 .arg(lang.clone())
704 .arg("--no-translator")
705 .stdout(Stdio::null())
706 .status()
707 .map(|s| {
708 if !s.success() {
709 panic!("Couldn't init PO file (gettext returned an error)")
710 }
711 })
712 .expect("Couldn't init PO file. Make sure msginit is installed.");
713 }
714 }
715
716 if conf.make_mo {
717 if !po_path.exists() {
718 panic!(
719 "{} doesn't exist. Make sure you didn't disabled po generation.",
720 po_path.display()
721 );
722 }
723
724 let mo_dir = Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| {
726 root_crate_path()
727 .join("target")
728 .join("debug")
729 .to_str()
730 .expect("Couldn't compute mo output dir")
731 .into()
732 }))
733 .join("gettext_macros")
734 .join(lang);
735 create_dir_all(mo_dir.clone()).expect("Couldn't create MO directory");
736 let mo_path = mo_dir.join(format!("{}.mo", domain));
737
738 Command::new("msgfmt")
739 .arg(format!(
740 "--output-file={}",
741 mo_path.to_str().expect("msgfmt: MO path error")
742 ))
743 .arg(po_path)
744 .stdout(Stdio::null())
745 .status()
746 .map(|s| {
747 if !s.success() {
748 panic!("Couldn't compile translations (gettext returned an error)")
749 }
750 })
751 .expect("Couldn't compile translations. Make sure msgfmt is installed");
752 }
753 }
754 quote!().into()
755}
756
757#[proc_macro]
771pub fn include_i18n(_: TokenStream) -> TokenStream {
772 let conf = Config::read();
773 let locales = conf.langs.clone().into_iter().map(|l| {
774 let lang = TokenTree::Literal(Literal::string(&l));
775 let path = Config::path().parent().unwrap().join(l).join(format!("{}.mo", conf.domain));
776
777 if !path.exists() {
778 panic!("{} doesn't exist. Make sure to call compile_i18n! before include_i18n!, and check that you didn't disabled mo compilation.", path.display());
779 }
780
781 let path = TokenTree::Literal(Literal::string(path.to_str().expect("Couldn't write MO file path")));
782 quote!{
783 (#lang, ::gettext::Catalog::parse(
784 &include_bytes!(
785 #path
786 )[..]
787 ).expect("Error while loading catalog")),
788 }
789 }).collect::<proc_macro2::TokenStream>();
790
791 quote!({
792 vec![
793 #locales
794 ]
795 }).into()
796}