Internalization in Rust
# locales/en.ftl
hello = Hello, { name }!
// main.rs
use fluent_i18n::localize;
#[localize]
pub(crate) struct Localize;
fn main() {
println!("{}", tr!(hello, "en", "world")); // Hello, world!
}
// other/module.rs
use crate::tr;
fn your_function() {
println!("{}", tr!(hello, "en", "world"));
}
- Default folder is
locales
. -
in filenames are converted to_
, sohello-world
andhello_world
would be considered equivalent, and it would be an error.- Variables types not detected (I don't know how), only strings.
- Only supported files structure is
locales
├── en.ftl
└── *.ftl
- Languages accepted in form
"en"
,"en_US"
, etc. Case insensitive,_
required. - Languages will always fallback to global fallback. For example, if you have languages
["en", "ru"]
and calltr!(name, "ru_RU")
, it will fallback to"en"
. - Macros should be called at crate top.
- Default generation with simple .ftl files.
- Support selectors (docs.rs)
- Support attributes.
- Support terms as static methods.
- Macros parameters:
-
path
- path to folder with localizations. -
resolver
- how files are stored:files
-{path}/*.ftl
(current behaviour).folder
-{path}/{locale}/*.ftl
.
-
set_locale
- how to specify current locale:init
- set locale on startup.state
- store locale as state.call
- specify locale on each function call (current behaviour).
-
fallback
- global fallback locale. -
sources
- how to load locales sources. Probably this could be solved by providing macro for embedding, and regular struct for manual initialization.embed
- embed sources to binary (current behaviour).load
- load sources at startup from file system.
-
errors
- how to handle errors (probably not required):ignore
log
panic
(current behaviour)
-
- Fallback with respect to region (e.g.
"ru_RU"
->"ru"
).- Probably calculate fallbacks on compile time?
- Support defining not at crate top (now just not tested, probably this already works).
- Probably this just required generating correct
#vis
identifier insidetr!()
.
- Probably this just required generating correct
- More translation formats support (long-term).
Open an issue, probably interly wants this too!
For source files
# locales/en.ftl
hello-world = Hello, { $name }!
# locales/ru.ftl
hello-world = Привет, { $name }!
use interly::localize;
#[localize]
pub(crate) struct Localize;
fn main() {
println!("{}", tr!(hello_world, "en", "world"));
println!("{}", tr!(hello_world, "ru", "мир"));
println!("{}", tr!(hello_world, "ru-RU", "мир"));
}
Generated (unrelated parts removed):
// main.rs
pub(crate) struct Localize {
bundles: __interly::Bundles,
}
pub(crate) mod __interly {
use ::std::collections::HashMap;
use ::std::sync::Arc;
use ::interly::{
FluentArgs,
FluentBundle,
FluentResource,
IntlLangMemoizer,
LanguageIdentifier,
Lazy,
};
use super::Localize;
pub(super) type Bundles = HashMap<
LANG,
FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
>;
impl Localize {
const FALLBACK_LANG: LANG = LANG::EN;
pub(crate) fn init() -> Self {
use ::interly::unic_langid::langid;
let mut resources: HashMap<LanguageIdentifier, Arc<FluentResource>> = HashMap::new();
let mut locales = vec![];
let lang = langid!("en");
locales.push((lang.clone(), "en"));
resources
.insert(
lang,
Arc::new(
FluentResource::try_new("hello-world = Hello, { $name }!\n".to_string())
.expect("invalid ftl"),
),
);
let lang = langid!("ru");
locales.push((lang.clone(), "ru"));
resources
.insert(
lang,
Arc::new(
FluentResource::try_new("hello-world = Привет, { $name }!\n".to_string())
.expect("invalid ftl"),
),
);
let mut bundles = HashMap::new();
for lang in locales {
let mut bundle = FluentBundle::new_concurrent(vec![lang.0.clone()]);
let _ = bundle.add_resource(resources.get(&lang.0).unwrap().clone());
bundles.insert(lang.1.into(), bundle);
}
Self { bundles }
}
pub(crate) fn languages() -> Vec<&'static str> {
vec!["ru", "en"]
}
pub(crate) fn __format_msg(
&self,
msg_id: &'static str,
lang: LANG,
args: Option<&FluentArgs<'_>>,
) -> String {
let lang = lang.into();
let mut bundle = self.bundles.get(&lang).expect("no bundle");
if !bundle.has_message(msg_id) {
bundle = self
.bundles
.get(&Self::FALLBACK_LANG)
.expect("no fallback bundle");
}
let msg = bundle
.get_message(msg_id)
.expect("no message")
.value()
.expect("no value in message");
let mut errs = vec![];
bundle.format_pattern(msg, args, &mut errs).to_string()
}
pub(crate) fn hello_world(&self, lang: impl Into<LANG>, name: &str) -> String {
self.__format_msg(
"hello-world",
lang.into(),
Some(&FluentArgs::from_iter(vec![("name", name)])),
)
}
}
pub(crate) static LOCALIZE: Lazy<Localize> = Lazy::new(|| { Localize::init() });
#[derive(PartialEq, Eq, Hash)]
pub(crate) enum LANG {
EN,
RU,
}
impl From<&str> for LANG {
fn from(lang: &str) -> Self {
match lang.to_lowercase().as_str() {
"en" => Self::EN,
"ru" => Self::RU,
_ => Self::EN,
}
}
}
impl From<&::std::string::String> for LANG {
fn from(lang: &::std::string::String) -> Self {
lang.as_str().into()
}
}
}
#[allow(unused)]
macro_rules! tr {
($e:ident, $lang:expr) => {
tr!($e, $lang,)
};
($e:ident, $lang:expr, $($v:expr),*) => {
$crate::__interly::LOCALIZE.$e($lang, $($v),*)
};
}
fn main() {
println!("{}", crate::__interly::LOCALIZE.hello_world("en", "world"));
println!("{}", crate::__interly::LOCALIZE.hello_world("ru", "мир"));
println!("{}", crate::__interly::LOCALIZE.hello_world("ru-RU", "мир");
}
Output:
Hello, world!
Привет, мир!
Hello, мир!