1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! ctl10n (compile time localization) provides you a simple way to embed messages
//! into binary file without embedding them into source. Internally, ctl10n generates
//! simple `macro_rules!` macro `tr!()` from provided TOML file with strings.
//! # Basic usage
//! Put following to your `build.rs`:
//! ```no_run
//! fn main() {
//!     println!("cargo:rerun-if-changed:build.rs");
//!     println!("cargo:rerun-if-chaged:strings.toml");
//!     if let Err(err) = ctl10n::convert_default_strings_file() {
//!         panic!("{}", err);
//!     }
//! }
//! ```
//! This will generate file `$OUT_DIR/strings.rs` from `strings.toml`.
//! TOML file with strings must be table where all values are strings. Example `strings.toml`:
//! ```toml
//! message = "Some message"
//! message-with-args = "Some message with {arg}"
//! ```
//! You should include `strings.rs` somewhere (for example, in `lib.rs`) to use generated
//! macro. You can do this by calling macro `ctl10n::include_strings!()` or manually,
//! using `include!()`.
//! After including macro it can be used like this:
//! ```ignore
//! ctl10n::include_strings!();
//!
//! fn main() {
//!     // `tr!()` with one argument will be translated to string literal
//!     println!(tr!("message"));
//!     println!(tr!("message-with-args"), arg = "foobar");
//!     // `tr!()` with multiple arguments will be translated to formatted `&String`
//!     println!("{}", tr!("message-with-args", arg = "foobaz"))
//! }
//! ```
//! Output of this code (assuming `strings.toml` from above):
//! ```text
//! Some message
//! Some message with foobar
//! Some message with foobaz
//! ```
//! Trying to use unknown key or wrong format arguments is compile-time error.


use std::fs;
use std::env;
use std::fmt::Display;
use std::io::{Read, Write};
use std::path::Path;

use quote::quote;

mod error;
pub use crate::error::{Error, Result};

mod toml_parser;
use toml_parser::parse_toml;

/// Include `tr!()` macro from generated file to current namespace.
/// If called without arguments includes file `$OUT_DIR/strings.rs`.
/// If called with one argument includes corresponding file in `$OUT_DIR`.
#[macro_export]
macro_rules! include_strings {
    () => { include!(concat!(env!("OUT_DIR"), "/strings.rs")); };
    ($filename:tt) => { include!(concat!(env!("OUT_DIR"), "/", $filename)); };
}

/// Convert TOML string to Rust source code with `tr!()` macro
pub fn gen_strings_macro(input: &str) -> Result<String> {
    let strings = parse_toml(input)?;
    let kv: Vec<(&str, &str)> = strings.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect();
    let keys = kv.iter().map(|(fst, _)| fst);
    let values = kv.iter().map(|(_, snd)| snd);

    let result = quote! {
        macro_rules! clt10n_tr_inner {
            #( (#keys) => { #values } );*;
            ($key:tt) => {
                compile_error!(concat!("There is no string for key `", stringify!($key), "`"))
            };
        }

        macro_rules! tr {
            ($key:tt) => { clt10n_tr_inner!($key) };
            ($key:tt, $( $args:tt )* ) => { &format!(clt10n_tr_inner!($key), $( $args )* ) };
        }
    };
    Ok(result.to_string())
}

/// Convert given TOML file to Rust source code in given location, providing
/// macro `tr!()`
pub fn convert_strings_file(toml_file: impl AsRef<Path> + Display, rs_file: impl AsRef<Path>) -> Result<()> {
    let mut input_file = fs::File::open(toml_file)?;
    let mut input = String::new();
    input_file.read_to_string(&mut input)?;
    let code = gen_strings_macro(&input)?;
    let mut output_file = fs::OpenOptions::new().write(true).create(true).open(rs_file)?;
    output_file.write(&code.as_bytes())?;
    Ok(())
}

/// Convert file `strings.toml` in current diretory to file `strings.rs` in `$OUT_DIR`
/// # Panics
/// If environment variable `OUT_DIR` is not set. You should call this function only
/// from `build.rs` script
pub fn convert_default_strings_file() -> Result<()> {
    convert_strings_file(
        "strings.toml",
        Path::new(&env::var("OUT_DIR").unwrap()).join("strings.rs")
    )
}