From 24f4362c6abeb4b72ef499f32edc6349fda5891d Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Thu, 9 Sep 2021 13:55:13 +1000 Subject: [PATCH] =?UTF-8?q?refactor(i18n):=20=F0=9F=9A=9A=20refactored=20t?= =?UTF-8?q?o=20prepare=20for=20future=20multi-translator=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/perseus/src/errors.rs | 2 +- packages/perseus/src/lib.rs | 4 +- packages/perseus/src/locales.rs | 22 ++++++ packages/perseus/src/macros.rs | 2 +- packages/perseus/src/translations_manager.rs | 2 +- packages/perseus/src/translator/errors.rs | 34 +++++++++ .../{translator.rs => translator/fluent.rs} | 71 ++----------------- packages/perseus/src/translator/mod.rs | 11 +++ 8 files changed, 78 insertions(+), 70 deletions(-) create mode 100644 packages/perseus/src/locales.rs create mode 100644 packages/perseus/src/translator/errors.rs rename packages/perseus/src/{translator.rs => translator/fluent.rs} (69%) create mode 100644 packages/perseus/src/translator/mod.rs diff --git a/packages/perseus/src/errors.rs b/packages/perseus/src/errors.rs index 677f769aad..98b72f6187 100644 --- a/packages/perseus/src/errors.rs +++ b/packages/perseus/src/errors.rs @@ -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 { diff --git a/packages/perseus/src/lib.rs b/packages/perseus/src/lib.rs index f0bb5a3521..b9f3df315a 100644 --- a/packages/perseus/src/lib.rs +++ b/packages/perseus/src/lib.rs @@ -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; @@ -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}; diff --git a/packages/perseus/src/locales.rs b/packages/perseus/src/locales.rs new file mode 100644 index 0000000000..bb1ca4fe2a --- /dev/null +++ b/packages/perseus/src/locales.rs @@ -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, +} +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) + } +} diff --git a/packages/perseus/src/macros.rs b/packages/perseus/src/macros.rs index ffd1eea3be..f667761cc2 100644 --- a/packages/perseus/src/macros.rs +++ b/packages/perseus/src/macros.rs @@ -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 } diff --git a/packages/perseus/src/translations_manager.rs b/packages/perseus/src/translations_manager.rs index 7274e1766d..7d81d94b20 100644 --- a/packages/perseus/src/translations_manager.rs +++ b/packages/perseus/src/translations_manager.rs @@ -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, diff --git a/packages/perseus/src/translator/errors.rs b/packages/perseus/src/translator/errors.rs new file mode 100644 index 0000000000..79f66afe0f --- /dev/null +++ b/packages/perseus/src/translator/errors.rs @@ -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) + } + } +} diff --git a/packages/perseus/src/translator.rs b/packages/perseus/src/translator/fluent.rs similarity index 69% rename from packages/perseus/src/translator.rs rename to packages/perseus/src/translator/fluent.rs index dcb4edf520..22365ce43f 100644 --- a/packages/perseus/src/translator.rs +++ b/packages/perseus/src/translator/fluent.rs @@ -1,62 +1,10 @@ -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, -} -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` and referred to by every template in an app. You do NOT want to be cloning potentially @@ -64,13 +12,13 @@ error_chain! { /// /// 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>, /// 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 { let resource = FluentResource::try_new(ftl_string) @@ -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) - }; -} diff --git a/packages/perseus/src/translator/mod.rs b/packages/perseus/src/translator/mod.rs new file mode 100644 index 0000000000..c908f8aef9 --- /dev/null +++ b/packages/perseus/src/translator/mod.rs @@ -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;