Skip to content

Commit

Permalink
refactor(i18n): 🚚 refactored to prepare for future multi-translator s…
Browse files Browse the repository at this point in the history
…upport
  • Loading branch information
arctic-hen7 committed Sep 9, 2021
1 parent 89fa00e commit 24f4362
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/perseus/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ error_chain! {
links {
ConfigManager(crate::config_manager::Error, crate::config_manager::ErrorKind);
TranslationsManager(crate::translations_manager::Error, crate::translations_manager::ErrorKind);
Translator(crate::translator::Error, crate::translator::ErrorKind);
Translator(crate::translator::errors::Error, crate::translator::errors::ErrorKind);
}
// We work with many external libraries, all of which have their own errors
foreign_links {
Expand Down
4 changes: 3 additions & 1 deletion packages/perseus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod client_translations_manager;
pub mod config_manager;
mod decode_time_str;
pub mod errors;
mod locales;
mod macros;
/// Utilities for serving your app. These are platform-agnostic, and you probably want an integration like [perseus-actix-web](https://crates.io/crates/perseus-actix-web).
pub mod serve;
Expand All @@ -58,8 +59,9 @@ pub use crate::build::{build_app, build_template, build_templates_for_locale};
pub use crate::client_translations_manager::ClientTranslationsManager;
pub use crate::config_manager::{ConfigManager, FsConfigManager};
pub use crate::errors::{err_to_status_code, ErrorCause};
pub use crate::locales::Locales;
pub use crate::serve::{get_page, get_render_cfg};
pub use crate::shell::{app_shell, ErrorPages};
pub use crate::template::{States, StringResult, StringResultWithCause, Template, TemplateMap};
pub use crate::translations_manager::{FsTranslationsManager, TranslationsManager};
pub use crate::translator::{Locales, Translator};
pub use crate::translator::{Translator, TRANSLATOR_FILE_EXT};
22 changes: 22 additions & 0 deletions packages/perseus/src/locales.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Defines app information about i18n, specifically about which locales are supported.
#[derive(Clone)]
pub struct Locales {
/// The default locale, which will be used as a fallback if the user's locale can't be extracted. This will be built for at build-time.
pub default: String,
/// All other supported locales, which will all be built at build time.
pub other: Vec<String>,
}
impl Locales {
/// Gets all the supported locales by combining the default, and other.
pub fn get_all(&self) -> Vec<&String> {
let mut vec: Vec<&String> = vec![&self.default];
vec.extend(&self.other);

vec
}
/// Checks if the given locale is supported.
pub fn is_supported(&self, locale: &str) -> bool {
let locales = self.get_all();
locales.iter().any(|l| *l == locale)
}
}
2 changes: 1 addition & 1 deletion packages/perseus/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ macro_rules! define_get_translations_manager {
$crate::FsTranslationsManager::new(
"../translations".to_string(),
all_locales,
"ftl".to_string(),
$crate::TRANSLATOR_FILE_EXT.to_string(),
)
.await
}
Expand Down
2 changes: 1 addition & 1 deletion packages/perseus/src/translations_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async fn get_translations_str_and_cache(

/// The default translations manager. This will store static files in the specified location on disk. This should be suitable for
/// nearly all development and serverful use-cases. Serverless is another matter though (more development needs to be done). This
/// mandates that translations be stored as files named as the locale they describe (e.g. 'en-US.ftl').
/// mandates that translations be stored as files named as the locale they describe (e.g. 'en-US.ftl', 'en-US.json', etc.).
#[derive(Clone)]
pub struct FsTranslationsManager {
root_path: String,
Expand Down
34 changes: 34 additions & 0 deletions packages/perseus/src/translator/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pub use error_chain::bail;
use error_chain::error_chain;

// This has its own error management because the user might be implementing translators themselves
error_chain! {
errors {
/// For when a translation ID doesn't exist.
TranslationIdNotFound(id: String, locale: String) {
description("translation id not found for current locale")
display("translation id '{}' not found for locale '{}'", id, locale)
}
/// For when the given string of translations couldn't be correctly parsed
TranslationsStrSerFailed(locale: String, err: String) {
description("given translations string couldn't be parsed")
display("given translations string for locale '{}' couldn't be parsed: '{}'", locale, err)
}
/// For when the given locale was invalid. This takes an error because different i18n systems may have different requirements.
InvalidLocale(locale: String, err: String) {
description("given locale was invalid")
display("given locale '{}' was invalid: '{}'", locale, err)
}
/// For when the translation of a message failed for some reason generally.
TranslationFailed(id: String, locale: String, err: String) {
description("message translation failed")
display("translation of message with id '{}' into locale '{}' failed: '{}'", id, locale, err)
}
/// For when the we couldn't arrive at a translation for some reason. This might be caused by an invalid variant for a compound
/// message.
NoTranslationDerived(id: String, locale: String) {
description("no translation derived for message")
display("no translation could be derived for message with id '{}' in locale '{}'", id, locale)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,76 +1,24 @@
use error_chain::{bail, error_chain};
use crate::translator::errors::*;
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
use std::rc::Rc;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};

/// Defines app information about i18n, specifically about which locales are supported.
#[derive(Clone)]
pub struct Locales {
/// The default locale, which will be used as a fallback if the user's locale can't be extracted. This will be built for at build-time.
pub default: String,
/// All other supported locales, which will all be built at build time.
pub other: Vec<String>,
}
impl Locales {
/// Gets all the supported locales by combining the default, and other.
pub fn get_all(&self) -> Vec<&String> {
let mut vec: Vec<&String> = vec![&self.default];
vec.extend(&self.other);

vec
}
/// Checks if the given locale is supported.
pub fn is_supported(&self, locale: &str) -> bool {
let locales = self.get_all();
locales.iter().any(|l| *l == locale)
}
}

// This has its own error management because the user might be implementing translators themselves
error_chain! {
errors {
/// For when a translation ID doesn't exist.
TranslationIdNotFound(id: String, locale: String) {
description("translation id not found for current locale")
display("translation id '{}' not found for locale '{}'", id, locale)
}
/// For when the given string of translations couldn't be correctly parsed
TranslationsStrSerFailed(locale: String, err: String) {
description("given translations string couldn't be parsed")
display("given translations string for locale '{}' couldn't be parsed: '{}'", locale, err)
}
/// For when the given locale was invalid. This takes an error because different i18n systems may have different requirements.
InvalidLocale(locale: String, err: String) {
description("given locale was invalid")
display("given locale '{}' was invalid: '{}'", locale, err)
}
/// For when the translation of a message failed for some reason generally.
TranslationFailed(id: String, locale: String, err: String) {
description("message translation failed")
display("translation of message with id '{}' into locale '{}' failed: '{}'", id, locale, err)
}
/// For when the we couldn't arrive at a translation for some reason. This might be caused by an invalid variant for a compound
/// message.
NoTranslationDerived(id: String, locale: String) {
description("no translation derived for message")
display("no translation could be derived for message with id '{}' in locale '{}'", id, locale)
}
}
}
/// The file extension used by the Fluent translator, which expects FTL files.
pub const FLUENT_TRANSLATOR_FILE_EXT: &str = "ftl";

/// Manages translations on the client-side for a single locale using Mozilla's [Fluent](https://projectfluent.org/) syntax. This
/// should generally be placed into an `Rc<T>` and referred to by every template in an app. You do NOT want to be cloning potentially
/// thousands of translations!
///
/// Fluent supports compound messages, with many variants, which can specified here using the form `[id].[variant]` in a translation ID,
/// as a `.` is not valid in an ID anyway, and so can be used as a delimiter. More than one dot will result in an error.
pub struct Translator {
pub struct FluentTranslator {
/// Stores the internal Fluent data for translating. This bundle directly owns its attached resources (translations).
bundle: Rc<FluentBundle<FluentResource>>,
/// The locale for which translations are being managed by this instance.
locale: String,
}
impl Translator {
impl FluentTranslator {
/// Creates a new translator for a given locale, passing in translations in FTL syntax form.
pub fn new(locale: String, ftl_string: String) -> Result<Self> {
let resource = FluentResource::try_new(ftl_string)
Expand Down Expand Up @@ -197,12 +145,3 @@ impl Translator {
Rc::clone(&self.bundle)
}
}

/// A super-shortcut for translating stuff. Your translator must be named `translator` for this to work.
// FIXME
#[macro_export]
macro_rules! t {
($id:literal, $translator:expr) => {
$translator.translate($id)
};
}
11 changes: 11 additions & 0 deletions packages/perseus/src/translator/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Errors for translators. These are separate so new translators can easily be created in a modular fashion.
pub mod errors;
mod fluent;

// We export each translator by name
pub use fluent::{FluentTranslator, FLUENT_TRANSLATOR_FILE_EXT};

// And then we export defaults using feature gates
// TODO feature-gate these lines
pub use FluentTranslator as Translator;
pub use FLUENT_TRANSLATOR_FILE_EXT as TRANSLATOR_FILE_EXT;

0 comments on commit 24f4362

Please sign in to comment.