Skip to content

Commit

Permalink
Initial docs for fluent-fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
zbraniecki committed Feb 16, 2021
1 parent 50d26da commit 4d13b24
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 85 deletions.
2 changes: 1 addition & 1 deletion fluent-bundle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Fluent is a modern localization system designed to improve how software is translated.
//!
//! `fluent-bundle` is the mid-level component of the [Fluent Localization
//! `fluent-bundle` is a mid-level component of the [Fluent Localization
//! System](https://www.projectfluent.org).
//!
//! The crate builds on top of the low level [`fluent-syntax`](../fluent-syntax) package, and provides
Expand Down
1 change: 1 addition & 0 deletions fluent-fallback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ unic-langid = { version = "0.9" }
[dev-dependencies]
fluent-langneg = "0.13"
unic-langid = { version = "0.9", features = ["macros"] }
fluent-resmgr = { path = "../fluent-resmgr" }
139 changes: 61 additions & 78 deletions fluent-fallback/examples/simple-fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
use std::{env, fs, io, path::PathBuf, str::FromStr};

use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
use fluent_fallback::{
generator::{BundleGenerator, FluentBundleResult},
Localization,
};
use fluent_langneg::{negotiate_languages, NegotiationStrategy};

use unic_langid::LanguageIdentifier;
use unic_langid::{langid, LanguageIdentifier};

/// This helper struct holds the available locales and scheme for converting
/// This helper struct holds the scheme for converting
/// resource paths into full paths. It is used to customise
/// `fluent-fallback::SyncLocalization`.
struct Bundles {
Expand All @@ -43,113 +43,104 @@ struct Bundles {
/// 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 locales = vec![];

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)?;
for entry in res_dir {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
let langid: LanguageIdentifier = name.parse().expect("Parsing failed.");
locales.push(langid);
}
}
}
}
}

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)
}

static L10N_RESOURCES: &[&str] = &["simple.ftl"];

fn main() {
// 1. Get the command line arguments.
let args: Vec<String> = env::args().collect();
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.");

// 2. If the argument length is more than 1,
// take the second argument as a comma-separated
// list of requested locales.
let requested: Vec<LanguageIdentifier> = args.get(2).map_or(vec![], |arg| {
arg.split(",")
.map(|s| s.parse().expect("Parsing locale failed."))
.collect()
});

// 3. Negotiate it against the avialable ones
let default_locale: LanguageIdentifier = "en-US".parse().expect("Parsing failed.");
let available = get_available_locales().expect("Retrieving available locales failed.");
let resolved_locales = negotiate_languages(
negotiate_languages(
&requested,
&available,
Some(&default_locale),
NegotiationStrategy::Filtering,
);
)
.into_iter()
.cloned()
.collect()
}

// 4. Construct the path scheme for converting `locale` and `res_id` resource
// path into full path passed to OS for loading.
// Eg. ./examples/resources/{locale}/{res_id}
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-bundle");
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}");

// 5. Create a new Localization instance which will be used to maintain the localization
// context for this UI. `Bundles` provides the custom logic for obtaining resources.
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()).collect(),
true,
resolved_locales
.iter()
.map(|&l| l.to_owned())
.collect::<Vec<LanguageIdentifier>>(),
Bundles { res_path_scheme },
app_locales,
bundles,
);

let mut errors = vec![];

// 6. Check if the input is provided.
match args.get(1) {
Some(input) => {
// 7.1. Cast it to a number.
match isize::from_str(&input) {
Ok(i) => {
// 7.2. Construct a map of arguments
// to format the message.
let mut args = FluentArgs::new();
args.set("input", FluentValue::from(i));
args.set("value", FluentValue::from(collatz(i)));
// 7.3. Format the message.
let value = loc
.format_value_sync("response-msg", Some(&args), &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
Err(err) => {
let mut args = FluentArgs::new();
args.set("input", FluentValue::from(input.as_str()));
args.set("reason", FluentValue::from(err.to_string()));
let value = loc
.format_value_sync("input-parse-error-msg", Some(&args), &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
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 = loc
.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 = loc
.format_value_sync("input-parse-error-msg", Some(&args), &mut errors)
.unwrap()
.unwrap();
println!("{}", value);
}
},
None => {
let value = loc
.format_value_sync("missing-arg-error", None, &mut errors)
Expand Down Expand Up @@ -239,12 +230,4 @@ impl BundleGenerator for Bundles {
res_ids,
}
}

fn bundles_stream(
&self,
_locales: std::vec::IntoIter<LanguageIdentifier>,
_res_ids: Vec<String>,
) -> Self::Stream {
todo!()
}
}
17 changes: 11 additions & 6 deletions fluent-fallback/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ pub trait BundleGenerator {

fn bundles_iter(
&self,
locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
res_ids: Vec<String>,
) -> Self::Iter;
_locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
_res_ids: Vec<String>,
) -> Self::Iter {
unimplemented!();
}

fn bundles_stream(
&self,
locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
res_ids: Vec<String>,
) -> Self::Stream;
_locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
_res_ids: Vec<String>,
) -> Self::Stream {
unimplemented!();
}
}
91 changes: 91 additions & 0 deletions fluent-fallback/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,94 @@
//! Fluent is a modern localization system designed to improve how software is translated.
//!
//! `fluent-fallback` is a high-level component of the [Fluent Localization
//! System](https://www.projectfluent.org).
//!
//! The crate builds on top of the mid-level [`fluent-bundle`](../fluent-bundle) package, and provides an ergonomic API for highly flexible localization.
//!
//! The functionality of this level is complete, but the API itself is in the
//! early stages and the goal of being ergonomic is yet to be achieved.
//!
//! If the user is willing to work through the challenge of setting up the
//! boiler-plate that will eventually go away, `fluent-fallback` provides
//! a powerful abstraction around [`FluentBundle`](fluent_bundle::FluentBundle) coupled
//! with a localization resource management system.
//!
//! The main struct, [`Localization`], is a long-lived, reactive, multi-lingual
//! struct which allows for strong error recovery and locale
//! fallbacking, exposing synchronous and asynchronous ergonomic methods
//! for [`L10nMessage`](types::L10nMessage) retrieval.
//!
//! [`Localization`] is also an API that is to be used when designing bindings
//! to user interface systems, such as DOM, React, and others.
//!
//! # Example
//!
//! ```
//! use fluent_fallback::Localization;
//! use fluent_resmgr::ResourceManager;
//! use unic_langid::langid;
//!
//! let res_mgr = ResourceManager::new("./tests/resources/{locale}/".to_string());
//!
//! let loc = Localization::with_env(
//! vec![
//! "test.ftl".to_string(),
//! "test2.ftl".to_string()
//! ],
//! true,
//! vec![langid!("en-US")],
//! res_mgr,
//! );
//!
//! let mut errors = vec![];
//! let value = loc.format_value_sync("hello-world", None, &mut errors)
//! .expect("Failed to format a value");
//!
//! assert_eq!(value, Some("Hello World [en]".into()));
//! ```
//!
//! The above example is far from the ergonomical API style the Fluent project
//! is aiming for, but it represents the full scope of functionality intended
//! for the model.
//!
//! # Resource Management
//!
//! Resource management is one of the most complicated parts of a localization system.
//! In particular, modern software may have needs for both synchronous
//! and asynchronous I/O. That, in turn has a large impact on what can happen
//! in case of missing resources, or errors.
//!
//! Currently, [`Localization`] can be specialized over an implementation of
//! [`generator::BundleGenerator`] trait which provides a method to generate an
//! [`Iterator`] and [`Stream`].
//!
//! This is not very elegant and will likely be improved in the future, but for the time being, if
//! the customer doesn't need one of the modes, the unnecessary method should use the
//! `unimplemented!()` macro as its body.
//!
//! `fluent-resmgr` provides a simple resource manager which handles synchronous I/O
//! and uses local file system to store resources in a directory structure.
//!
//! That model is often sufficient and the user can either use `fluent-resmgr` or write
//! a similar API to provide the generator for [`Localization`].
//!
//! Alternatively, a much more sophisticated resource manager can be used. Mozilla
//! for its needs in Firefox uses [`L10nRegistry`](https://github.com/zbraniecki/l10nregistry-rs)
//! library which implements [`BundleGenerator`](generator::BundleGenerator).
//!
//! # Locale Management
//!
//! As a long lived structure, the [`Localization`] is intended to handle runtime locale
//! management.
//!
//! In the example above, [`Vec<LagnuageIdentifier>`](unic_langid::LanguageIdentifier)
//! provides a static list of locales that the [`Localization`] handles, but that's just the
//! simplest implementation of the [`env::LocalesProvider`], and one can implement
//! a much more sophisticated one that reacts to user or environment driven changes, and
//! called [`Localization::on_change`] to trigger a new locales to be used for the
//! next translation request.
//!
//! See [`env::LocalesProvider`] trait for an example of a reactive system implementation.
mod cache;
pub mod env;
mod errors;
Expand Down
2 changes: 2 additions & 0 deletions fluent-resmgr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod resource_manager;

pub use resource_manager::ResourceManager;

0 comments on commit 4d13b24

Please sign in to comment.