Source code
Revision control
Copy as Markdown
Other Tools
//! This is an example of a simple application
//! which calculates the Collatz conjecture.
//!
//! The function itself is trivial on purpose,
//! so that we can focus on understanding how
//! the application can be made localizable
//! via Fluent.
//!
//! To try the app launch `cargo run --example simple-fallback NUM (LOCALES)`
//!
//! NUM is a number to be calculated, and LOCALES is an optional
//! parameter with a comma-separated list of locales requested by the user.
//!
//! Example:
//!
//! cargo run --example simple-fallback 123 de,pl
//!
//! If the second argument is omitted, `en-US` locale is used as the
//! default one.
use std::{env, fs, io, path::PathBuf, str::FromStr};
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
use fluent_fallback::{
generator::{BundleGenerator, FluentBundleResult},
types::ResourceId,
Localization,
};
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use rustc_hash::FxHashSet;
use unic_langid::{langid, LanguageIdentifier};
/// This helper struct holds the scheme for converting
/// resource paths into full paths. It is used to customise
/// `fluent-fallback::SyncLocalization`.
struct Bundles {
res_path_scheme: PathBuf,
}
/// This helper function allows us to read the list
/// of available locales by reading the list of
/// directories in `./examples/resources`.
///
/// It is expected that every directory inside it
/// has a name that is a valid BCP47 language tag.
fn get_available_locales() -> io::Result<Vec<LanguageIdentifier>> {
let mut dir = env::current_dir()?;
if dir.to_string_lossy().ends_with("fluent-rs") {
dir.push("fluent-fallback");
}
dir.push("examples");
dir.push("resources");
let res_dir = fs::read_dir(dir)?;
let locales = res_dir
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.filter_map(|dir| {
let file_name = dir.file_name();
let name = file_name.to_str()?;
Some(name.parse().expect("Parsing failed."))
})
.collect();
Ok(locales)
}
fn resolve_app_locales<'l>(args: &[String]) -> Vec<LanguageIdentifier> {
let default_locale = langid!("en-US");
let available = get_available_locales().expect("Retrieving available locales failed.");
let requested: Vec<LanguageIdentifier> = args.get(2).map_or(vec![], |arg| {
arg.split(",")
.map(|s| s.parse().expect("Parsing locale failed."))
.collect()
});
negotiate_languages(
&requested,
&available,
Some(&default_locale),
NegotiationStrategy::Filtering,
)
.into_iter()
.cloned()
.collect()
}
fn get_resource_manager() -> Bundles {
let mut res_path_scheme = env::current_dir().expect("Failed to retrieve current dir.");
if res_path_scheme.to_string_lossy().ends_with("fluent-rs") {
res_path_scheme.push("fluent-fallback");
}
res_path_scheme.push("examples");
res_path_scheme.push("resources");
res_path_scheme.push("{locale}");
res_path_scheme.push("{res_id}");
Bundles { res_path_scheme }
}
static L10N_RESOURCES: &[&str] = &["simple.ftl"];
fn main() {
let args: Vec<String> = env::args().collect();
let app_locales: Vec<LanguageIdentifier> = resolve_app_locales(&args);
let bundles = get_resource_manager();
let loc = Localization::with_env(
L10N_RESOURCES.iter().map(|&res| res.into()),
true,
app_locales,
bundles,
);
let bundles = loc.bundles();
let mut errors = vec![];
match args.get(1) {
Some(input) => match isize::from_str(&input) {
Ok(i) => {
let mut args = FluentArgs::new();
args.set("input", i);
args.set("value", collatz(i));
let value = bundles
.format_value_sync("response-msg", Some(&args), &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
Err(err) => {
let mut args = FluentArgs::new();
args.set("input", input.as_str());
args.set("reason", err.to_string());
let value = bundles
.format_value_sync("input-parse-error-msg", Some(&args), &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
},
None => {
let value = bundles
.format_value_sync("missing-arg-error", None, &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
}
}
/// Collatz conjecture calculating function.
fn collatz(n: isize) -> isize {
match n {
1 => 0,
_ => match n % 2 {
0 => 1 + collatz(n / 2),
_ => 1 + collatz(n * 3 + 1),
},
}
}
/// Bundle iterator used by BundleGeneratorSync implementation for Locales.
struct BundleIter {
res_path_scheme: String,
locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
res_ids: FxHashSet<ResourceId>,
}
impl Iterator for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn next(&mut self) -> Option<Self::Item> {
let locale = self.locales.next()?;
let res_path_scheme = self
.res_path_scheme
.as_str()
.replace("{locale}", &locale.to_string());
let mut bundle = FluentBundle::new(vec![locale]);
let mut errors = vec![];
for res_id in &self.res_ids {
let res_path = res_path_scheme.as_str().replace("{res_id}", &res_id.value);
let source = fs::read_to_string(res_path).unwrap();
let res = match FluentResource::try_new(source) {
Ok(res) => res,
Err((res, err)) => {
errors.extend(err.into_iter().map(Into::into));
res
}
};
bundle.add_resource(res).unwrap();
}
if errors.is_empty() {
Some(Ok(bundle))
} else {
Some(Err((bundle, errors)))
}
}
}
impl futures::Stream for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
todo!()
}
}
impl BundleGenerator for Bundles {
type Resource = FluentResource;
type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
type Iter = BundleIter;
type Stream = BundleIter;
fn bundles_iter(
&self,
locales: std::vec::IntoIter<LanguageIdentifier>,
res_ids: FxHashSet<ResourceId>,
) -> Self::Iter {
BundleIter {
res_path_scheme: self.res_path_scheme.to_string_lossy().to_string(),
locales,
res_ids,
}
}
}